Catalog drift: grounded-identity reconciliation
cr drift reconciles a Backstage/Cortex catalog's declared facts against the
code-observed graph. The rule is simple and absolute:
Drift is only asserted between facts that resolve to the same real graph node. Anything that cannot be grounded is unverifiable, never drift.
This replaced an earlier string set-diff that compared a declared ref's name
(trust-me-api) against an observed node's title (trustme). Those live in two
independent naming worlds and never matched, so the report invented drift. The
tempting fix, a normalizer that strips -api / dotted-FQN / case to force the
names equal, is a heuristic = silent bug: it quietly merges two distinct
entities or misses a renamed one, with no signal. We do not do that.
The three states (dependencies)
For each declared dependsOn ref d on a service S:
-
Resolve by exact identity. Find the node
NwhereN.catalogName = d OR N.name = d, among the labels adependsOncan target:Service | Library | Datastore | Cache | MessageBroker | MessageChannel. A ref grounds only on a single match (ambiguous multi-matches stay unverifiable, mirroringbindUnresolvedDependencies). Refs are already syntax-parsed to bare names at ingestion (normalizeDependencyRefstrips the Backstagekind:ns/prefix); no compare-time normalization happens. -
Classify:
state meaning score aligned dresolves toNandShas the edge toNnone grounded drift ( groundedMissing)dresolves toNbutShas no edge toNcounts observed-undeclared Shas an edge to an in-scope node no declared ref claimscounts unverifiable dresolves to no node in scopeoff-score
Ambiguity guard. An observed edge is only reported as observed-undeclared
when every declaration on S resolved. If any declaration is unverifiable,
an unmatched observed edge might BE that declaration under a name we can't
resolve, so it is unverifiable, not drift. This is why, for a service whose
catalog refs are mostly cross-repo (out of scope), the intra-repo sibling edges
do not show up as false "undeclared" drift.
API provides / consumes: dropped
There is no grounded key between a declared API ref and a code APIInterface:
catalog API entities in practice carry no spec.definition and no server URL, and
the API welders join on (method, path), never on catalog refs. So the API
dimension is not reported rather than fabricated by string-matching.
Re-introduction path: when catalog API entities carry an OAS spec.definition
or a server URL, resolve providesApis/consumesApis to the code APIInterface
through the existing (method, path) welder identity (see
api-endpoint-dedup.md), then add the dimension back as
a grounded three-state comparison, identical to dependencies.
Score and coverage
driftScore = round((1 - entitiesWithGroundedDrift / (totalCatalogEntities + orphans)) * 100)
verifiableCoverage = round(verifiedFacts / (verifiedFacts + unverifiedFacts) * 100)- Only entities with grounded drift (ghost, orphan, owner, system, grounded dependency drift) lower the score. Unverifiable facts are off-score: a service whose declarations we could not ground does not get punished for being out of scope.
verifiableCoverageis the share of declared dependency facts we could actually ground. It makes the limited check-scope explicit on the same screen, so a high alignment score on a single-repo run is not mistaken for "everything checks out" when most refs were simply unverifiable. Unverifiable facts upgrade to aligned/drift automatically once the referenced repo is also ingested (the node then exists) or a grounded API key appears.
Code map
src/graph/queries/drift-classify.ts: pure, DB-free functions forclassifyDependencyDrift,computeDriftScore, andcomputeVerifiableCoverage. Unit-tested.src/graph/queries/drift.ts:getDependencyReconciliation(resolve refs by exact identity, scan observed edges, classify) andgetCatalogDriftReport(aggregate, score, coverage). Ghost/orphan/owner/system are unchanged grounded dimensions.normalizeDependencyRefis no longer used here (it stays in ingestion).src/cli/commands/drift.ts: renders theDependency Drift(grounded) section, a dimmed off-scoreUnverifiablesection, and theVerifiable coverageline.
Tests
- Unit
tests/unit/graph/drift-queries.test.ts: covers the classifier, score, and coverage logic. - Integration
tests/integration/catalog-drift-grounding.test.ts: tests graph to drift conversion, all five states plus score plus coverage, on a controlled graph. - Eval pattern
tests/eval/patterns/catalog-drift-grounding/: pins that a realcatalog-info.yamlparses to the bare-name refs the resolver matches by.