Resolve
Intelligence › Resolve (?tab=resolve) shows what the tenant’s registered sources actually contain:
- how their datasets group into data domains;
- how the sources are used;
- whether what a source declares about itself agrees with what its data shows.
It is ported from FACE’s Reconcile and Data Connectors pages, not from Atlas. It reuses FACE’s engine, github.com/org-runink/store/estate (connect, graph, access), rather than reimplementing it.
POST /api/connectors/status/refresh, which covers the connectors CORE publishes and never touches registry records. The agents (datagov included), core connection register and granting a source to the workspace all dial nothing.Two services, one set of documents
Since 2026-09-26 the page is the shared estate component from org-runink/ui (runink.ui.estate.v1.EstateService). It is mounted by internal/console/atlas_estate.go and drawn by EstatePage in flutter/lib/features/intelligence/views/estate_view.dart.
The older runink.core.atlas.v1.ResolveService (atlas_res.go) stays registered as a deprecated alias for one release. The CapEx pull dialog, the connection wizard’s “Test after saving” and the audit pages still call it. Both services read and write the same three stored documents, so each sees the other’s writes.
| EstateService | ResolveService (deprecated) | Class |
|---|---|---|
ListSources, GetSourceDescription, GetAccessPatterns, GetEstateMap, GetReconciliation | ListSources, GetSourceDescription, GetAccessPatterns, GetEstate, GetReconciliation | read |
TestSource, ExploreSource, ReadAccessPatterns, MapEstate | the same four | admin-write |
Reads never dial
The read RPCs answer from what the last dialing action committed. Nothing dials on a read, a timer or a page load. A test fails the engine if a read calls it.
The page re-reads GetEstate every 5 seconds only while a map run is RUNNING and the tab is on screen.
The four dialing actions
Each is admin-write (CORE_ATLAS_ADMINS). Each is audited under resource atlas-estate with the outcome, including refusals, and each sits behind a confirm dialog on the page that states what it dials and what it keeps.
| Action | Audit action | Budget (atlas_res.go) | What it does |
|---|---|---|---|
TestSource | atlas.resolve.test | 20 s | Checks that the source answers with its credentials |
ExploreSource | atlas.resolve.explore | 90 s | Reads the catalogue: datasets, columns, types, declared keys, view dependencies, and profile statistics where the engine has them |
ReadAccessPatterns | atlas.resolve.access | 90 s | Reads the source’s own access views over a window of window_days 1..90 |
MapEstate | atlas.resolve.map | 15 min per run, 30 s per source | Builds the estate domain map across sources. It returns at once and runs in the background, one run in flight per console |
A second map while one is running is refused: map run … started by … at … is still in flight; one run at a time.
Credentials are opened with the registry’s audited Credentials read for one engine call and then dropped. They are never stored, returned on the wire, logged or handed to an agent. Engine errors are scrubbed (resScrub) before they are recorded.
Choosing the runner
Every action, and CapexService.PullFeed, first decides the effective runner (resEffectiveRunner) from the request’s optional runner field:
runner on the request | Effective runner |
|---|---|
| absent (empty) | The connection’s binding. An empty binding means console |
console | Runink managed, explicitly, even for a connection bound to a self-hosted runner |
| a runner name | That registered runner |
A name must be a DNS label or console. Anything else gets runner … is not a runner name (a DNS label, or "console" for the Runink managed runner).
console dials in-process. The runner registry is not even read. Any other runner is refused until dispatch to runners exists (rollout step 6). No bundle is opened, the engine is not called, and there is no fallback to another runner. The refusal names the state it found:
| State found | Outcome | What the detail says |
|---|---|---|
| Not registered | runner_unknown | Enrol that runner, or choose console |
| Revoked | runner_revoked | Who revoked it and when |
| Pending (not enrolled yet) | runner_offline | Enrolment is not complete |
| Active, not connected | runner_offline | CORE has no live connection to its address |
| Active and connected | runner_offline | “connected; dispatch arrives in rollout step 6”. This never claims the runner is down |
| Registry unreadable | failed | “runner registry unavailable”. Never runner_unknown |
In a map run, the runner routes every source, and a refused source becomes a gap in the map. The effective runner, and whether it came from the action or the binding, is recorded in the audit detail and in ActionRecord.runner. Runners themselves are managed on DataEx › Runners.
The values boundary
atlas_res_estate.go is the boundary where values stop. It copies structure and counts field by field into record types that have no field for a cell, MIN/MAX, a sample row, a principal name or a credential. Two tests guard this: TestAtlasResolveEstateAdapterDropsCells and TestAtlasResolveRecordsCarryNoCellShapedField.
| Kept | Never kept |
|---|---|
| Datasets, columns, types, nullability, declared keys | Sampled values, sample rows |
| Row counts, NULL counts, DISTINCT counts (with their basis) | MIN / MAX / mean (a customer cell) |
| Access counts: reads, writes, distinct principals | Principal names |
| Outcome, time and actor of each action | Credentials |
Explore profiling limits. Where the engine supports statistics, Explore computes per-column aggregates over at most 25 datasets (resMaxStatDatasets). Only DISTINCT and NULL counts cross the boundary. Explore also keeps the statistics pass’s counted row count.
No confidence number. Domains carry an evidence class (declared, measured or weak) and every edge carries an evidence kind. A GUESS edge is drawn dotted in the warning tone and is never styled like a declared key.
Storage
The documents are metadoc kinds in the console’s appfs root (atlas_res_store.go):
| Document | Contents | Budget |
|---|---|---|
var/lib/atlas-estate | Per-source last action, access counts, map runs | 512 KiB |
var/lib/atlas-estate-map | The last committed map | 768 KiB |
var/lib/atlas-estate-desc-<h> | One source’s last Explore, with h = sha256(id)[:16] | 768 KiB |
What Resolve records as data lineage
When an Explore, ReadAccessPatterns or MapEstate commits, the console records what it observed as data lineage (datalineageresolve.go). This happens in process: there is no HTTP hop and no ingest token. The edges go into the same observed-lineage store the apps publish into, under the publisher core-resolve. The app ingest refuses that publisher name from anyone else with a 403.
| Edge kind | Between | From |
|---|---|---|
DECLARED_FOREIGN_KEY | dataset → dataset | Explore’s declared keys, and the map’s declared keys |
INFERRED_* placement | dataset → domain | The map, only for domains whose members come from two or more sources |
OBSERVED_ACCESS | dataset → principals | Access counts, only where a stated read or write count is above zero |
DECLARED_VIEW_DEPENDENCY | base → view | The source’s own catalogue: Postgres pg_depend, MySQL VIEW_TABLE_USAGE. The Lineage page draws these as “Declared derivations” |
Not recorded: GUESSED_COLUMN_OVERLAP (a guess is not an observation) and catalogue containment.
Each edge carries only qualified names, the kind, counts and provenance. It carries no constraint or column name, no detail text and no admin identity.
Edges are deduplicated source→destination. Each commit replaces its own scope’s claims (explore:<id>, access:<id>, map), and claims expire after 30 days. The Resolve lineage key is capped at 256 edges / 512 KiB, and the weakest edges are dropped first and counted.
A committed map also raises the playbook harness event estate / estate.mapped, with attributes domains, unplaced, gaps, drift, shadow and missing. See Playbooks.
Refusals you will see
- On a console with no sign-in:
this console has no sign-in configured, so dialing a tenant source would be unattributable — set GOOGLE_CLIENT_ID or CONSOLE_ADMIN_PASSWORD before testing, exploring or mapping sources. - On the page, the first
PermissionDenieddisables every dial and shows the console’s reason. AFailedPreconditionorInvalidArgumentis drawn verbatim and gates nothing else.
More in Troubleshooting.