test(cli): the option-B acceptance pin — a two-package fixture booted through every entry path with the flattened collections absent - #15039
Conversation
… through every entry path (#15004) Reader program 1/4 of the ADR-0130 D4 option-B ruling (#14512 comment 5528589044). Option B removes the flattened top-level collections from a multi-package artifact so `packages[]` carries each definition once; the ruled order is readers first, emitter last, and the failure mode that order exists to contain is a reader nobody enumerated — silent, because the collection is simply absent and nothing throws. This commit adds the pin that makes that loud, and nothing else: three new test files, zero production files. - `test/fixtures/option-b-collection-zoo.ts` — two ordinary `defineStack` packages carrying one member of every collection family, composed with `manifest: 'preserve'`, in both shapes. The key set separating the shapes is DERIVED from `ObjectStackDefinitionSchema` ∩ `AssembledPackageBodySchema` rather than transcribed, so a collection family added later joins the probe automatically. - `test/fixtures/option-b-reader-probe.ts` — the instrument. Every row calls a reader this repo ships, or boots a real kernel carrying the real `AppPlugin` and reports what that plugin handed to a subsystem. No row reads `bundle.<collection>` and calls it a measurement. - `test/option-b-reader-acceptance.pin.test.ts` — the pin. `OPTION_B_LOSSES` records exactly which subsystems lose their collection today; the assertion is set equality, shrink-only, audited in both directions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
…st tsc program (#15004) `check:type-source-resolution` went red on the pin: `@objectstack/runtime`, `@objectstack/objectql` and `@objectstack/plugin-security` were newly reached by `tsconfig.test.json` and resolved through each package's `exports` map to `dist/index.d.ts` — a build artifact. For a pin whose whole subject is the readers the reader program is about to CHANGE, a type verdict about the last `pnpm build` is the vacuous-green direction that gate exists to refuse. Fixed the way the gate prescribes for a dep exposed through an EXISTING program: `paths` rules, never a widening of its shrink-only registry. Three star-less (exact-match) rules, so `@objectstack/objectql/core` keeps resolving through the package's own `exports` map untouched. Measured, because the registry's own notes record `paths` billing other packages' diagnostics into the consumer's ledger (PR #12570): with the workspace closure built, `pnpm --filter @objectstack/cli check:test-typecheck` reports 3 files / 28 errors / 6 pinned signatures — byte for byte the pre-existing `test-typecheck-debt.json`. Zero new diagnostics. Also corrects the pin header's reverse-verification count to the measured 24 and records the `effect: 'writes'` -> `effect: 'pure'` row, a loss no presence-check would have found. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
📓 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): |
复审 PASS —— 两个 open question 都裁给实施席的建议,并且探针的形态比我在卡上写的要求更好PM 席(epic #14122,session 我在卡上写错了一条验收,实施席给的形态是对的卡面要求「探针今天在 option-B 腿必须红」。照字面做,这个 pin 会一直红到整个读取半程序完成——那样它根本合不进去,后面三张卡也就没有可依附的前置。 实施席把它换成了集合相等的 shrink-only 台账( 这既满足我要的「必须能区分」,又保住了「可合入、并且逐行推绿」。我的原措辞被更好的形态取代了,记在这里而不是默默接受。 另外值得记的一条设计:没有一行是把被测读取重写一遍——每行要么调用仓库真正出货的 reader( 两种形状之间的键集是从 Open question 1 —
|
Fixes #15004
Reader program 1/4 of the ADR-0130 D4 option-B ruling (#14512 comment 5528589044, maintainer 2026-09-03, decision batch #23). This is the acceptance pin the ruling says the reader half does not land without.
Production files touched: zero. Three new test files plus one test-layer tsc config, which a gate required — see "The one file outside
test/" below.What landed
packages/cli/test/fixtures/option-b-collection-zoo.tsdefineStackpackages — an App and a module that depends on it — carrying one member of every collection family, split across BOTH so no reader passes by looking at one. Composed withmanifest: 'preserve', in the two shapes.packages/cli/test/fixtures/option-b-reader-probe.tsos buildwrites it (reallowerCallables, real parse, real sibling ESM runtime module), then runs every reader over both shapes.packages/cli/test/option-b-reader-acceptance.pin.test.tsOPTION_B_LOSSES, a shrink-only ledger asserted by set equality, plus the additive baseline and the anti-vacuity control.The option-B shape is DERIVED, not transcribed
The key set separating the shapes is
ObjectStackDefinitionSchemashape keys INTERSECTAssembledPackageBodySchemashape keys — "the collections a package owns" — and its complement is the seven artifact-envelope keyspackages/spec/src/assembled-package-body.test.tsclassifies. A collection family added to the stack schema next month joins this probe automatically instead of sitting silently outside it. #14877 is to publish that key set as an export; when it lands this fixture should read it instead of deriving it, and nothing else changes.Measured, matching the enumeration in #14512 comment 5523603341 exactly:
Every row calls a real reader. None re-implements one.
A row either invokes a reader this repo ships and reports its return value, or boots a real kernel carrying the real
AppPluginand reports what that plugin HANDED to a subsystem. No row readsbundle.COLLECTIONand calls that a measurement — a row shaped like the read it is watching stays red forever after the reader beside it is fixed, and a gate that cannot go green gets deleted.AppPluginreadsjobs/data/translations/datasources/datasourceMapping/objectsinline inside a 950-linestart(), with no exported reader to call, and the fold cards 2/4 and 3/4 land could be at the constructor, at each read, or anywhere between. What cannot move is the far side: the plugin has to hand the collection to a subsystem, and the subsystems are ordinary kernel services. The probe registers those services and records what arrives, so those rows measure the contract rather than the code.Both entry paths, all five boundaries
They share no seam, so covering one would be half a pin.
loadArtifactBundlecollectBundleActions/collectBundleHooks/collectBundleFunctionEntriesand a bootedAppPluginrun over the returned bundleos serve/os devconfig-module loadappSecurityPluginOptionsand a bootedAppPlugin, over the from-source composed configos buildconfig-module loados migrate's own secondloadConfigAppPluginwrap is the B2 boot rowsresolve-project-databaseresolveStandaloneDatabasecalled directly on the written artifact — this is the row that answersconfig-datasource:probe_primarytoday andunified-defaultunder option BcreateStandaloneStackis driven end to end as well, so B5 and B1 are exercised in the order and the composition a real standalone boot runs them in.The red leg — reverse-verified, per the card's acceptance
Run on
origin/main33681eaefwithOPTION_B_LOSSESemptied (the mutation was confirmed on disk — 24 ledger lines to 0 with the empty literal present — before the run, and the file restored fromHEADafter it):24 subsystems, across all three packages the program scopes. The other five tests — the additive BASELINE, the
packages[]control, the shape derivation, the ledger-has-no-phantom-rows check and the boundary-coverage check — all pass in that SAME run, which is what makes the red a discrimination rather than a broken fixture.Two measured corrections to the enumeration, both in its favour
functionsis not lost on the compiled path — it is CORRUPTED, which no presence-check would have caught.mergeRuntimeModulewrites the sibling ESM module's handler map ontobundle.functionsunconditionally, so the CALLABLES survive an option-B artifact. What does not survive is what the function declared about itself: the top level carried{ handler, effect: 'writes' }, the module supplies a bare callable, andnormalizeFlowFunctionEntrydefaults it toeffect: 'pure'. A declared writer comes back through this reader as a pure function — Ascriptnode's function is contractually pure, but nothing enforces it — a function that writes makes its run under-report #4396's silent un-declaring in a third spelling, with its writes counted as none. A row COUNTING entries here reports 1 to 1 and reads as coverage; the committed row asserts the VALUE.AppPlugin'sobjectsloss is wider than "the seeder".connectDeclaredis handedbundle.objectsalongsidebundle.datasources, so under option B the auto-connect call is not made at all and both collections go dark together.Why the losses are LEDGERED rather than simply asserted away
The card's acceptance is that the option-B leg is red today. A permanently red test cannot land, so the red is held the way every other measured-state gate in this repo holds one:
OPTION_B_LOSSESrecords exactly which rows lose, and the pin asserts set EQUALITY. That gives four directions at once, and the last two are what a bareexpect(...).toBe(0)could not give:packages[]by 2/4, 3/4 or 4/4 goes RED, naming the ledger line to DELETE — the ledger shrinks one subsystem at a time, and empty is what "the program is done" means;The ledger is shrink-only, and the failure text says so at the point of failure: adding a line is never how a red build is made green.
The anti-vacuity control
MetadataPlugin/ObjectQLPluginregister throughresolveArtifactPackageOrderand therefore already readpackages[]. The pin asserts the SchemaRegistry holds['probe_account', 'probe_order']after the same boot in BOTH shapes and on BOTH entry paths. That is what proves the option-B fixture really carries every definition underpackages[], so everyLOSTabove is a reader losing a collection and never a fixture that shipped an empty package.The one file outside
test/packages/cli/tsconfig.test.jsongains threepathsrules. It is not a production file — it is the test layer's own tsc program (include: ["test/**/*"],noEmit), andtsconfig.build.jsondoes not extend it — but it is outside the card's "a fixture and a test", so it is called out rather than buried.check:type-source-resolutionwent red on the pin:@objectstack/runtime,@objectstack/objectqland@objectstack/plugin-securitywere newly reached by that program and resolved through each package'sexportsmap todist/index.d.ts. For a pin whose entire subject is the readers this program is about to change, a type verdict about the lastpnpm buildis precisely the vacuous-green direction that gate exists to refuse. The gate's registry is shrink-only and widening it is explicitly not the remedy for a dep exposed through an EXISTING program;pathsis.Three star-less (exact-match) rules, deliberately:
@objectstack/objectqlpublishes a second subpath (./core) which keeps resolving through its ownexportsmap untouched.Measured, because that registry's own notes record
pathsbilling other packages' diagnostics into the consumer's ledger: with the workspace closure built,pnpm --filter @objectstack/cli check:test-typecheckreports3 file(s) / 28 error(s) / 6 pinned signature(s)— byte for byte the pre-existingtest-typecheck-debt.json. Zero new diagnostics.What this pin does NOT reach, stated rather than implied
Three reads in the enumeration are expressions inline inside oclif command bodies, with no exported reader and no service on the far side, so no probe short of running the command reaches them. They are named in the test file's header so the next reader does not mistake it for full coverage:
serve.tsconfig.objectsgating ObjectQL engine auto-registration, and the sibling gate for storage-driver auto-registration. The ARTIFACT half of both is covered —createStandaloneStacksurfacesobjectsprecisely so that path can drive them, and that row is in the table — but the from-source half needs a realos serve.dev.tsreadArtifactObjects(), a module-private function with its ownJSON.parse(readFileSync(...)). Non-fatal; it goes permanently empty.compile.ts's union authoring-rule run, which under option B would judge an empty stack.Covering these by asserting
config.objectsin the pin would be the re-implementation trap above. Cards #15006 and #15007 own those sites, and a probe for them belongs beside whatever seam those cards introduce.Verification
Gate family derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsagainst the real change set (4 paths), re-derived after the tsconfig edit and identical. Everything below ran ate70392ba7, the final commit on this branch.pnpm --filter @objectstack/cli exec vitest run test/option-b-reader-acceptance.pin.test.tsTest Files 1 passed (1) · Tests 6 passed (6)pnpm --filter @objectstack/cli check:test-typecheckOK — 3 file(s) / 28 error(s) / 6 pinned signature(s), unchanged frommainpnpm lint(repo-wideeslint . --no-inline-config)pnpm check:type-source-resolutionOK — 118 tsc program(s) across 78 packagespnpm check:test-source-aliaspnpm check:type-check-coverage,check:type-check-debt218 raw tsc error(s) total, none above its recorded numberpnpm check:nul-bytes,check:cross-package-test-inputs,check:cli-test-child-env,check:engine-double-contract,check:published-files,check:doc-authoring,check:logger-receiver-detach,check:objectql-double-limit,check:page-declaration-shape,check:query-options-erasure,check:refd-timer-probe,check:slot-lookup,check:watch-hint-literal,check:where-matchercheck-ci-filter-parity,check-closing-keyword-parity,check-comment-mask-adoption,check-comment-mask-corpus,check-keyed-text-bounds,check-plugin-teardown-shape,check-shard-attestation,check-system-context-census,check-undeclared-dep-imports, bothdocs-auditgates)NOT MEASURED, declared with the unmet prerequisite rather than reported as passed:
scripts/check-test-completeness.mjs— exit 3,PREREQUISITE NOT MET: it grades a savedturbo run testlog, which only CI produces. Its own text says the family names it with no argument and that the local reading is NOT MEASURED.pnpm check:dual-build-cjs-loads— exit 3,PREREQUISITE NOT MET: it reads built output and 12 packages outside this card's build closure have nodist/. CI builds the whole tree.Declared narrowing: the
@objectstack/clivitest suite was not run whole — only this PR's own file. The narrowing is provable rather than hopeful: the diff ADDS three files and edits a tsc-only config, and vitest does not readtsconfigpaths(novite-tsconfig-pathsplugin inpackages/cli/vitest.config.ts, which this PR does not touch), so no existing test file's resolution moves. CI runs the suite regardless.skip-changesetis applied: this PR publishes nothing —packages/cli'sfilesfield shipsdistandbin, and every path here is undertest/or is anoEmittsc config.Next
OPTION_B_LOSSEShas 24 lines. Cards #15005 (@objectstack/runtime), #15006 (@objectstack/cli) and #15007 (@objectstack/plugin-security) each delete some of them. When it is empty the reader half is done and the emitter half on #14512 can land.🤖 Generated with Claude Code
https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
Generated by Claude Code