test(metadata-protocol,objectql): key the protocol stub engines by table - #16651
Conversation
Eleven protocol harnesses gave their stub engine one flat row map and told the tables apart in exactly one place — the `insert` early-return for `sys_metadata_audit`. `find`/`findOne` ignored the table argument entirely, so every other table one save writes (`sys_metadata_history`, `sys_metadata_commit`) landed in the map that answered reads of `sys_metadata`. Each stub now holds a map from table NAME to that table's rows, reached through a `tableOf(name)` accessor, copying the shape `protocol.runtime-gate-stored-universe.test.ts` established. `rows` stays bound to `sys_metadata`, so existing assertions read the table they name. The `audit_skip` early-return goes with it: with the tables separated it has no job left, and it was a standing trap for any future assertion about audit rows written in these files. Two pins make the change a measurement rather than a rename — one per package. A DRAFT save writes `sys_metadata`, `sys_metadata_history` and `sys_metadata_audit`, all three addressed to the same `(type, name)`, and the journal rows carry no `state`; each pin asserts a `sys_metadata` read answers with the store row and nothing else, behind a firing control that the journals really were written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
…otocol-harness-table-keyed-stub
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
`tsc --noEmit` refused the metadata-protocol pin with TS2352: `Row` declares no index signature, so asserting `Row[]` to `Record<string, unknown>[]` is not a legal widening. `Partial<Row>` is legal AND true of these rows — a journal row carries `type` and `name` and carries no `state`, which is exactly what the assertions beside it read. The objectql pin's `any[]` is spelled the same way for the same reason, replacing a cast that typechecked while saying nothing. Vitest never type-checks, so the suites were green over code tsc refuses; the error was only ever reachable through the package's own `typecheck` script, which this branch had last run before either pin existed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
Patch round —
|
Fixes #16225
Eleven protocol test harnesses gave their stub engine one flat row map and told the tables apart in exactly one place — the
insertearly-return forsys_metadata_audit.find/findOneignored the table argument entirely, so every other table a save writes (sys_metadata_history,sys_metadata_commit) landed in the map that answered reads ofsys_metadata.Each stub now holds a map from table NAME to that table's rows, reached through a
tableOf(name)accessor — the shapeprotocol.runtime-gate-stored-universe.test.ts(#16223) established, copied rather than reinvented.rowsstays bound tosys_metadata, so every existing assertion reads the table it names.Why the remaining seven were worth touching
The defect was caught by the polarity of one assertion, not by anything structural. That is the whole argument: this shape does not produce a red someone investigates, it produces a green that closes a card.
The incident, reproduced and pinned
Driving one DRAFT
saveMetaItemthrough a converted harness and dumping each table:All three are addressed to the same
(type, name, organization_id), and the two journal rows carry nostate. A flat map hands them back to asys_metadataread; a harness that also modelssys_metadata.state's declareddefaultValue: 'active'— correctly, which is what made it convincing — serves a DRAFT-only artifact back as an ACTIVE metadata row.Two pins, one per package, assert that a
sys_metadataread answers with the store row and nothing else:packages/metadata-protocol/src/protocol.dashboard-dataset-publish-gate.test.tspackages/objectql/src/protocol-save-meta-repo-path.test.tsEach is guarded by a firing control (the save must really have written the journals, or the read below measures an empty universe) and asserts on store-only columns (
state,checksum) rather than on a row count. That last choice is load-bearing: a journal row is addressed to the same keykeyOfbuilds, so under a merged map it can simply OVERWRITE the store row and a count- or name-based assertion still reads one row of the right name. The first draft of the objectql pin was vacuous for exactly that reason and was rewritten.Ablation — one leg per package
Mutation:
tableOfre-merged to hand every caller the same map. On-disk proof taken both ways (deleted text1 -> 0, injected text present), blob hash compared againstHEAD:pathbefore and after.protocol.dashboard-dataset-publish-gate.test.tsbba0bd3e=HEAD,git diff HEADempty,git status --porcelainempty, 7 passedprotocol-save-meta-repo-path.test.ts00f32020=HEAD,git diff HEADempty,git status --porcelainempty, 14 passedThe metadata-protocol leg is the sharper reading: exactly one test in that file distinguishes the two shapes, and it is the one this PR adds. The four extra reds on the objectql leg come from the audit row colliding with the store row under that file's
keyOfonce theaudit_skipearly-return is gone as well — the ablation removes the whole change, not just half of it.Scope rule 2 — the shape population, measured
A population defined by a spelling is not a population defined by the shape, so the shape was measured directly. Both halves of the card's shape are read from each fake engine's OWN parameter name, so the expression is spelling-independent. Two references deliberately do not count as using the table argument:
assertEngineFindOnePredicate(p, opts)(checks the caller's predicate, never selects a row — this is why the eight read as table-aware to a naive grep) and thesys_metadata_auditearly-return itself.Script:
shape-census.mjs, run overpackages/metadata-protocol packages/objectql, 93 files declaring anasync insert.insertputs every table in one container (FLAT-WRITE)find/findOneanswer without asking which table (the full incident shape)if (_t === 'sys_metadata_audit') returnControls, both directions, from the same run: the reference fix
protocol.runtime-gate-stored-universe.test.ts,protocol.package-publish-audit-rows.test.tsandsys-metadata-repository.contract.test.tsare all table-keyed and are all absent from the before-listing; all 8 files the card names are present in it. A first version of the census reported 120 files and a second reported the reference fix itself as blind — both were parser bugs (the type-literal brace inPromiseobject return types read as the method body), found by those controls.Triage of the residue:
packages/objectql/src/hook-run-as.test.ts— a recording engine (insertpushes to aseenlog and returns a constant; there is no row map, and its parameter is_o, an object not a table). Not this family, not converted.protocol.delete-object-registry-unregister,protocol.delete-receipt-wording,protocol.driver-text-disclosure, plushook-run-as); their rows are pre-seeded and theirfindOnealready guardstable !== 'sys_metadata'. They are the 4 that remain, and they are false positives of the axis, not residue.protocol.container-issue-descent,protocol.invalid-metadata-422-face-inventory,protocol.save-union-issues— carry the flat map but answernull/[]from every read, so no row of one table can be served as a row of another. What the flat map still did there was makerows.sizethe total of every table one save writes. Same mechanical change, so converted here; the sub-shape is stated in each file's own header note.So the spelling did not under-count the incident shape — it coincides with it at 8 — but it did under-count the flat-map family by 3.
Scope rule 1 — reds
None. All 11 harnesses were green before conversion (162 tests) and green after; the full suites of both packages are green (2433 + 4802). Nothing was passing because a
sys_metadata_historyrow answered asys_metadataread, which matches the card's own statement that the flaw is latent. Nothing was re-merged, nothing was relaxed, no card was filed for a red because there was none.The
audit_skipearly-return is goneWith the tables separated it has no job left — audit rows land in
sys_metadata_audit, which no read in these files consults. Removing it also clears the trapprotocol.package-publish-audit-rows.test.tsnames in its own header: a suite that later wants to assert on audit rows written in one of these files would otherwise be born vacuous. No test asserts on a row id, so thenextIdshift it causes is unobservable (verified: zero matches forr_[0-9]in the eight).Clause-② — re-derived from the diff that exists
No. The diff is 11
*.test.tsfiles in two packages and nothing else. Nothing exported moves, no accept set widens or narrows, no published payload gains a key, no product code is touched.needs:contract-reviewnot hung. No product change was needed to make any converted harness honest.Changeset: test-only, nothing publishes ⇒
skip-changeset.Verification
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandswith no hand-listed paths, reconciled with--ran:Run reconciliation — 48 derived, 46 run, 2 NOT-MEASURED, 0 UNRUN.pnpm check:dual-build-cjs-loads— "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/. Runpnpm buildfirst. ⛔ This is NOT a pass: nothing was measured."pnpm check:type-check-debt— "check-type-check-coverage: PREREQUISITE NOT MET … --re-measure cannot run: 17 workspace dependencies of the ledgered packages have no built type entry point on disk." Its own report names this process's limit as 8240 MB with no OOM.pnpm check:engine-double-contractexit 0,pnpm check:test-source-aliasexit 0. No gate asked for a ledger row, so no ledger was touched.pnpm --filter @objectstack/metadata-protocol test— 169 files, 2433 passed, 10 skipped.pnpm --filter @objectstack/objectql test— 284 files, 4802 passed.pnpm --filter @objectstack/metadata-protocol --filter @objectstack/objectql typecheck— exit 0, andtsc --noEmit --listFilesconfirms the converted test files are inside the typecheck program (not excluded).eslint . --no-inline-configover the whole repo at220bfdba: 6301 files, 0 errors, 0 warnings, exit 0. No narrowing claimed — this is the whole population.🤖 Generated with Claude Code
https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
Generated by Claude Code