Repository navigation
fix(metadata-protocol): a seed reference written blank ('' or whitespace) is no reference, by the engine's isMissing - #24502
Conversation
…e engine's isMissing The seed loader skipped a reference only on undefined/null, so a lookup written '' or whitespace was resolved as a natural key, deferred, and reported 'Deferred reference unresolved after pass 2' (errored, success false) for a row that had landed. The single-reference skip, the multi-value element skip (pass 1 and pass 2) and the pointer-pair skip now ask @objectstack/core's isMissing, the rule the engine's insert door and required check use. A blank element of a multiple lookup is dropped like a null hole, so the other elements still land. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxmveGHL9EpPEGVggMSzDB
…ader and on a real SqlDriver; changeset Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxmveGHL9EpPEGVggMSzDB
…nk pins load-bearing Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxmveGHL9EpPEGVggMSzDB
…s already-pinned engine double Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxmveGHL9EpPEGVggMSzDB
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a15fdce9cb1663f279bf7077daf7fc181c51fc70 && git checkout a15fdce9cb1663f279bf7077daf7fc181c51fc70
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7e58e0a27998194640c145344b7519f1ccc9fad4 3e5746dd679759abb14ab095650eddc03204ad96 && git checkout -B drift-repro 7e58e0a27998194640c145344b7519f1ccc9fad4 && git merge --no-ff 3e5746dd679759abb14ab095650eddc03204ad96
node scripts/docs-audit/affected-docs.mjs --json 7e58e0a27998194640c145344b7519f1ccc9fad4 |
Contract reviewServed-tier: What this record read:
① Derived judgmentsOne blank rule, reused, not copied. Four sites, and each is a site that judges "has a reference value":
The claim's fence named the first, the pass-1 element and the pointer; the pass-2 element is inside the same file and is needed. The deferred value is the whole authored array, so without it a blank element beside a forward key would still be reported unresolved. Its ablation pin is red. Accepted. The single reference: left as written, which is right. A skipped blank is neither resolved nor reported. The engine then decides it:
Ruling B on #20311 makes The multi-value element: dropped, which is right. A blank element names no row by the engine's rule. Dropping it matches the The pointer half: one answer changes, in the blank rule's direction. A whitespace-only id with an empty type half used to be refused as un-addressable; now it is skipped, exactly as What does not move:
The pins: real controls.
② Semver levelOne ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #24459. 4 files, +378/−7. Not governed. |
| Scope | Every site in seed-loader.ts that judges "has a reference value" now asks isMissing from @objectstack/core: the single reference, each multi-value element in pass 1 and in pass 2, and the referenceVia id half. There is no second definition. A blank single reference is left for the engine. A blank multi-value element is dropped like a null hole. |
| Review | TIER_DEFAULT record 6263609121, PASS on this head. |
| Declaration | Clause-②: no on the claim, the PR body and the changeset (@objectstack/metadata-protocol patch). |
| Cross-lane | The real-driver pin in packages/runtime is declared to domain:cli: 6263574303 on #6024 and 6263581601 on #22648. |
| Gates | Dev, on 3e5746dd67: 64 derived, 64 run, 64 exit 0. Core test:repo exit 0. |
| CI | At this read on 3e5746dd67: every check completed, none failed. |
Disposition:
open_questions: both accepted (record ①).- A blank multi-value element is dropped.
- A blank single reference is left as written, so the stored
''is what the insert door stores for the same payload. [Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311's ruling B reads''as empty.
deviations: all accepted (record ③).out_of_scope_findings: the string-stored lookup blank is noted, not filed.
Landing: ordinary queue (pr_ready + automerge_enable through the relay). Fixes #24459 then closes the card.
Generated by Claude Code
Fixes #24459
Clause-②: no (no published accept set or surface moves: the row was already written, and only the false "unresolved" report and its
success: falsego away; owes the seat'sTIER_DEFAULTreview of record before the queue)Dispatched under claim 6261694593 (
domain:engineseat 1, sessionsession_01JtSQQ3EWsbNPzyYMug1tXy); dev sessionsession_01KxmveGHL9EpPEGVggMSzDB.What changed
The seed loader decided "this reference has a value" with
undefined/nullonly, while the engine's blank rule,isMissing(@objectstack/core,packages/core/src/utils/required-emptiness.ts), also calls''and whitespace-only strings missing. Every site inpackages/metadata-protocol/src/seed-loader.tsthat judges a reference value now asksisMissing(imported, not re-spelled):recordRefsloop (if (isMissing(fieldValue)) continue;)undefined/nullisMissingvalue; left as written for the enginemultiple: truereference, pass 1null/undefinedholeisMissingholeresolveDeferredUpdates)referenceViapointer id halfundefined/null/''isMissingvalue (whitespace-only joins'')Other "has a value" sites in the file were read and left alone, on purpose:
externalIdKeyjudges the record's own natural-key components (not a reference); the organization stamps compare== nullonorganization_id, which is resolved by its own path;formatAttemptedis display only. No change toisMissing, the engine's required check, or any error code.A blank element inside a multi-valued reference: dropped (decided by the four axes)
main, one blank element made the whole field defer and fail, so the row landed with none of its associations (books: nullfor['Dune', '', ' ', 'Emma']). Dropping the hole lands[Dune, Emma], which is what the author wrote.nullholes were already dropped, andisMissingis the platform's single definition of "no value", so blank holes follow the same rule. Leaving the whole value as written for the engine is not possible, because the other elements are natural keys that must be resolved first.Measured (real
ObjectQL+ realSqlDriveron better-sqlite3,SeedLoaderService,multiPass: true)Seed:
blank_ref_book(Dune,Emma) andblank_ref_loan(loan-real→'Dune',loan-empty→'',loan-whitespace→' ',loan-multi→books: ['Dune', '', ' ', 'Emma']). Plus a required lookup written''.main(01fafdee, before)success: false, inserted 6,errored: 3: threeDeferred reference unresolved after pass 2: …book = '' / ' ' / …books = '' → blank_ref_book.title not foundsuccess: true, inserted 6,errored: 0, no errorssuccess: false, skipped 6,errored: 3(same three)success: true, skipped 6, updated 0,errored: 0loan-empty/loan-whitespace.booknull(the deferral deleted the key)''/' ': left as written;isMissingtrue (see Acceptance notes)loan-multi.booksnull: the whole association was lost[Dune.id, Emma.id]loan-real.book(control)Dune.idDune.id''errored: 2:Failed to write … record #0 (name=loan-required-empty): book is requiredandDeferred reference unresolved after pass 2: …book = '' → …errored: 1:Failed to write … record #0 (name=loan-required-empty): book is requiredonly; row not writtenPins
packages/runtime/src/seed-blank-reference-real-driver.integration.test.ts(new): the table above, on the real driver. It lives inruntimebecause that package already has the real-driver seed harness and thedriver-sqldependency (seed-multi-value-lookup-real-driver.integration.test.tsis its sibling).metadata-protocolhas neither. This is outside the claim's "pins beside it" fence, and the reason is the measurement the dispatch asked for.packages/metadata-protocol/src/seed-loader-multi-value-reference.test.ts(6 new cases in a newdescribe): single''and whitespace skipped and left as written (not deferred, not dropped); a blank element dropped in pass 1 (no back-fillupdate); a blank element dropped in pass 2 (a forward self-reference, so it is proven by the back-fillupdate); a whitespace pointer skipped like'', including the un-addressable branch, plus a live-pair control; a control showing that a real key resolves and an unknown key still errors. These cases reuse that file's already-pinned engine double, socheck:engine-double-contractneeds no ledger edit. A new file with its own double was tried first, and the gate reported it.git grepforDeferred reference unresolved/UNRESOLVEDfinds no test that pins the old blank behaviour (the only hits are the loader,CHANGELOG.md, and these new pins). Existing''seed values in tests are a field-definitionreference: ''(seed-loader-reference-carrier.test.ts) and an unrelated ownership claim (claim-seed-ownership.test.ts). No pin needed flipping.Ablations (committed fix first,
scripts/ablation-replace.mjs, trap restore, restore proven by blob == HEAD and an empty porcelain)src/resolution (19-case file)::1464single makes the 2 single-blank cases red; pass-1 element makes the pass-1 element case red; pass-2 element makes the pass-2 case red; pointer makes the pointer case red. Each restores to 19/19 green.dist/resolution (runtimereads@objectstack/metadata-protocolthroughexports): all three reference skips set back toundefined/null, rebuilt,ablation-dist-preflightshows the markers present indist/, and both real-driver cases go red. After restore and rebuild,--absentpasses and both cases are green.:1464alone was a void reading, in two ways. (a) The first marker did not reachdist/, because the build re-spellsundefinedasvoid 0; the preflight reported this, so a string-literal marker was used. (b) With the marker live, the real-driver file stayed green, because the pass-1 element skip still drops a scalar''(it is wrapped as a one-element list) and the insert lands NULL. The single-site difference (left as written vs dropped) is pinned by the unit cases above. The faithful "remove the skip" is all three sites together, and that run is the red reading above.Verification (final head
3e5746dd6, aftergit merge origin/mainatce2e08ee3and a fullturbo build, 76/76)pnpm --filter @objectstack/metadata-protocol test: exit 0, 247 files passed (3 skipped), 20698 tests passed.pnpm --filter @objectstack/metadata-protocol typecheck: exit 0.pnpm --filter @objectstack/runtime typecheck: exit 0 (check:test-typecheckOK).pnpm --filter @objectstack/runtime test: exit 0, 422 files, 6637 tests passed (pre-merge headf749e65d1; post-merge, the two real-driver seed files were re-run: 5/5).pnpm --filter @objectstack/core test:repo: exit 0, 6 files, 82 tests.node scripts/pm/dispatch-gates.mjs --commands(no paths, 4 changed paths vs merge basece2e08ee3): 64 commands, all exit 0.--ranreconciliation: 64 derived, 64 run, 0 NOT-MEASURED (a derived zero: every line carries its exit code).check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET, 39 packages withoutdist/); it was re-run after the full build and exited 0.Acceptance notes
normalizeBlankTypedValues) deliberately does not null a blank string-stored value: its doc lists "lookup ids" among the columns where''is a value, and calls what "empty" means there "a separate question, not answered here". So an optional lookup seeded''stores''(and whitespace stores' '), exactly as a directengine.insertof the same payload does. Every reader that asksisMissingsees it as empty, and the required check refuses it. Before this change the deferral deleted the key, so the row stored NULL. Carrier: none. This records the engine's own declared open question; it is not filed here.Generated by Claude Code