Repository navigation
fix(metadata-protocol): SeedLoader asks the registry for a name column before probing it - #9172
Conversation
…he SeedLoader probes it (#9071) The external-key lookup chain spelled `where: { name }` unconditionally, so on an object with no `name` column the driver refused the filter (INVALID_FILTER) once per reference value, per pass — hundreds of ERROR lines per seeded boot. The driver is correct: a predicate on a column the object does not have never ran, so answering 'no rows' would be a lie. The fix is to stop asking, not to change the answer — the refusal path is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
…s WHERE matcher (#9071) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
…le specifier (#9071) An extensionless relative import does not resolve under moduleResolution NodeNext, adding a TS2835 to this package's shrink-only type-check ledger. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
Docs drift sweep — nothing owed by this changeSwept the whole The live question — does any page document the probe chain, its The three advisory pages score zero on every token: Two near-misses checked and both correct as they stand:
No pre-existing error found to report either — the pages that touch this area are right, Generated by Claude Code Generated by Claude Code |
Fixes #9071
What was wrong
SeedLoaderService.resolveFromDatabase()resolves a reference authored as a natural key bywalking a probe chain — the target dataset's declared
externalId, then the historicalnamedefault, then the internalid. Thenameleg was spelled unconditionally,including on objects that have no
namecolumn at all.On those objects the probe is not a cheap miss. The driver refuses it:
One refusal per reference value, per pass — hundreds of ERROR-level
Find operation failedlines on every seeded boot and every per-organization replay, each reading exactly like a
real failure everyone downstream has to learn to ignore.
What this does NOT do
The driver's refusal is correct and is left exactly as it is. A predicate naming a
column the object does not have never ran, so answering "no rows" would be a lie
(ADR-0110 D3 — a miss and a fault are different facts). Nothing here catches it more
quietly, downgrades it, lowers a log level or filters it out of the log. The fix is to
stop asking, not to change the answer.
The fix
Ask the metadata registry whether the target declares a
namecolumn before adding thatleg — through the same
resolveObjectDefinitionresolver the loader already builds itsreference graph from (metadata service first, then the engine's own schema registry). The
answer is memoised per
load(the question is asked once per unresolved reference value)and re-asked on the next one, since a publish between two loads can add the very column it
is about.
Three properties worth stating explicitly:
namecolumn that leg couldonly ever throw, never match — so removing it cannot take a resolution away from any
object. PM mechanism-assumption 1 ("does anything rely on the
nameleg answering?") isanswered structurally rather than by sampling: the leg is dropped in exactly the
population where it was incapable of answering, and kept everywhere else. The
both-directions test pins that.
earlier when a referenced target carries no dataset in this load and therefore keeps the
metadata-level
namedefault as its FIRST probe (buildReferenceMap). Same defectclass, same file, closed by the same guard rather than left for a second card.
registry can describe the object, the leg is kept — today's behaviour — rather than
narrowed on a fact nobody established.
On PM mechanism-assumption 2 (⚠️ do not assume the registry is importable): measured, and
it holds here — no new import was needed.
resolveObjectDefinitionis a private methodof
SeedLoaderServiceitself, already called once per object at graph-build time, andIMetadataServiceis a constructor argument. The alternative reading of the card — resolvethe object's ADR-0079
nameFieldand probe THAT instead of the literalname— wasrejected: on an object whose
nameFieldpoints at some other column it would probe acolumn never probed before and could resolve references that previously did not resolve,
which is the one thing the card says the fix must not do.
Tests
New:
packages/metadata-protocol/src/seed-loader-name-probe.test.ts. Its fake engineresolves WHERE columns against a declared column set and refuses what it cannot
resolve, with the driver's own
INVALID_FILTER/400envelope and first sentence — adouble that answered such a filter with an empty list would be looser than the producer it
stands in for and would make the whole file green over the defect. Every case counts the
refusals it raised.
Test-count direction: up — one new file, no existing case removed or rewritten. Both
directions are pinned: the leg disappears on an object with no
namecolumn (and thelookup still resolves through the leg that always answered it), it still fires and still
answers on an object that declares one, it is skipped in its first position too, it is
KEPT when nothing can describe the object, and it is re-asked on the next
load.Reproduction on a real boot
The card's observation came from a real
serveboot, so the proof is one too — the sameapp booted twice, once with each build of
@objectstack/metadata-protocol.Fixture.
examples/app-crmalready has the card's exact shape:crm_contactdeclaresno
namecolumn (first_name/last_name/full_name/email/account) and itsdataset is keyed by
email, whilecrm_activity.contactreferences it by that naturalkey. On a clean boot every reference hits the in-memory map, so the DB probe never runs —
so for the measurement, and only for it, the five
crm_activity.contactvalues werepointed at unseeded addresses (
ghost.*@lattice.example.com), which is what makes thelookup chain reach the database. That edit is a local experiment and is not in this PR;
examples/is untouched by the diff.Command.
pnpm dev:crm -- --fresh -p PORT(realserve, real sqlite driver, freshtempdir), 180 s window, log captured whole.
Find operation failed[sql-driver] INVALID_FILTERseed-loader.tsatorigin/main)28 ok / 5 errors,{"inserted":28,"updated":0,"skipped":0,"errored":5,"referencesDropped":0}28 ok / 5 errors,{"inserted":28,"updated":0,"skipped":0,"errored":5,"referencesDropped":0}Ten, from five rows across two passes — the card's "per row, per pass" exactly. Every one
of them was the same refusal:
The seed outcome is byte-identical across the two boots — same counters, same five
legitimately-unresolved references, same
[SeedLoader] Deferred reference UNRESOLVED after pass 2lines (those are the loader correctly reporting that the ghost addresses do notexist; they are a different thing from the noise and are untouched). That is PM
mechanism-assumption 1 answered empirically as well as structurally: nothing relied on
the
nameleg answering. What disappeared is exactly the ten provoked refusals.Verification
Local, at the exact head this body describes —
52f9fcba9, the final commit (unionre-run after it, not before).
pnpm --filter '@objectstack/metadata-protocol^...' build— the dependency closure first,in a fresh worktree.
pnpm --filter @objectstack/metadata-protocol test— 113 files / 1572 tests passed.npx vitest run src/seed-loader-name-probe.test.ts— 1 file / 5 tests passed.npx tsc --noEmitinpackages/metadata-protocol— 63 errors, identical to thefrozen DEBT ledger entry; the new file contributes none. (It contributed one TS2835
before its import was spelled
./seed-loader.js— an extensionless relative import doesnot resolve under
moduleResolution: NodeNext. Caught by re-running the ratchet, andfixed rather than baselined.)
node scripts/pm/dispatch-gates.mjsand all green:check:nul-bytes,check:changeset-gate-self-tests,check:cross-package-test-inputs,check:durability-log-level,check:objectui-changeset,check:engine-double-contract,check:where-matcher,check:query-options-erasure,check:type-check-coverage,check:test-source-alias, and the ratchetcheck:type-check-debt --re-measure(33 entries re-measured, none above its recordednumber).
Reverse verification — the fix committed first, then taken out with
git checkout origin/main -- packages/metadata-protocol/src/seed-loader.tsand restoredfrom the branch. Direction predicted before running, and observed: red, 3 of 5 cases
failing —
The two that stay green under the revert are exactly the two asserting preserved
behaviour (the leg still fires when the column is declared; the leg is kept when nothing
can describe the object) — which is the property this change claims.
Generated by Claude Code