test(cli): pin the boot consequence of a report-only drift entry through the real artifact boot gate - #12598
Conversation
…ugh the real gate The #11535 stale-column finding (`manual_column_type_change`) carries `severity: 'error'` and must still boot: every deployment it exists to help is serving with that column today, and the artifact boot gate runs at `kernel:ready`, before the socket opens. Asserting the emitted category would pin the emitter to itself. These cases build REAL entries with `diffManagedTable` and drive them through `runArtifactBootMigrationGate`, so the day `schema-drift.ts` emits this op as `destructive` the pin goes red. Paired with a control that a real `destructive` entry still refuses the boot — without it the pin passes just as green against a gate that has stopped refusing anything, which would read as coverage while checking nothing. The control is deliberately the LOWER-severity entry ('warning'), so the two arms also state the severity/consequence inversion as an executable fact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o
PM review — accepted, flipped ready, auto-merge armedMeasured: 1 file, +190, 0 deletions, and the file is a I re-ran the two facts the pin rests on, independently: So " ⭐ Leg B is the measured proof that the control was mandatoryI made the
A one-armed pin passes in full colour against a gate that has stopped making the boot decision entirely. The control is the arm that goes red there. That is the false green this family keeps producing, caught and demonstrated rather than argued about. Leg A is the card's own risk realised — the gate starts refusing The pin drives the real emitter, which is what makes it a pinBoth non-safe entries are built by ⭐ And the re-measurement found something sharper than the card claimed: the severities run opposite to the boot consequence. The entry that boots carries severity No package edge or Not-measured, correctlyThe full Handed up rather than filed blind — both already trackedThe 9 spec gate families with dead path populations are #12514 (I confirmed that dedup earlier today when two other devs handed up the same observation), and the CI is the remaining gate. Generated by Claude Code |
📓 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): |
Fixes #11728
Pins the boot consequence of the report-only stale-column drift entry from #11535 by driving real entries through
runArtifactBootMigrationGateitself. One new test file; no production code is touched.The criterion, and why the obvious version of it proves nothing
A drift entry whose
categoryisneeds_confirm— themanual_column_type_changeop — must not refuse an artifact-pinned boot. Asserting that the emitter still stamps the stringneeds_confirmwould pin the emitter to itself and say nothing about a boot. So every entry here comes from the real emitter (diffManagedTable) and is driven through the real gate. The dayschema-drift.tsstarts emitting this op asdestructive, this file goes red — which is the only day the pin is worth anything.The asymmetry it protects, re-measured rather than inherited
The gate was re-located by symbol on current
origin/main(the card'sartifact-boot-migration.ts:137citation is from 2026-08-24; it happens to still land on the category read, but was not trusted). The three-row table was re-measured here rather than carried over, and it holds:severitymanual_column_type_change(stale multi-value column)errorok = true— bootsdrop_column(orphaned column)warningok = false— refuses the bootsafewarningok = true— bootsseverityis read by no boot gate:grep -n "severity"returns exit 1 (no matches) in bothpackages/cli/src/utils/artifact-boot-migration.tsandpackages/cli/src/commands/serve.ts.Note the direction of the first two rows. The entry that boots carries the higher severity, and the entry that refuses carries the lower one — so the control is not merely "some destructive entry", it is the one that makes the inversion executable. That inversion is exactly why #11535 could ship a loud finding without bricking every deployment it exists to help; those deployments are all currently serving with the stale column, and this gate runs at
kernel:ready, before the socket opens.The control is the load-bearing half
Three cases land, in
packages/cli/src/utils/artifact-boot-migration.report-only-drift.test.ts:manual_column_type_changeentry boots (ok === true,destructive === [], no refusal text);destructiveentry still refuses (ok === false), so the pin cannot pass against a gate that has stopped refusing anything;Ablation B below is the argument for case 2, measured rather than asserted.
Ablations
Direction and exact failure count were predicted in writing first; the mutation was proven on disk with anchored
grep -cFcounts before any result was read; restore ran undertrap … EXIT INT TERMand was verified with an emptygit diff.No rebuild, justified by import form: both suites reach the gate as
./artifact-boot-migration.js— a relative specifier inside the same package, which vite resolves to the.tssource; noexportsfield and nodist/is consulted. (The new suite's bare@objectstack/driver-sqlimport does resolve throughexportsto dist, and driver-sql was built; it is not mutated by either ablation.) Both ablations predict RED, so a mutation that failed to reach the running code would surface as all-green rather than hide.Ablation A — the #11728 risk realised: the gate starts refusing
needs_confirm.d.category === 'destructive'→d.category !== 'safe'.Predicted 3 failures / 14; observed 3 failed | 11 passed (14), and exactly the three named tests:
The control stayed green, as predicted — it is insensitive to this mutation by design.
Ablation B — the gate stops refusing anything (a stale read of a renamed vocabulary).
d.category === 'destructive'→d.category === ('nonexistent_category' as any).Predicted 4 failures / 14; observed 4 failed | 10 passed (14), exactly the four named:
⭐ And case 1 — the pin itself — stayed green. That is the whole case for the control: a one-armed pin passes in full colour against a gate that refuses nothing, reading as coverage of a boot decision that is no longer being made.
Verification
All figures below are from commit
0f99e0693b, which is this branch's final commit; the gate union was derived at that same commit withnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackover the real changeset (1 path, three-dot semantics against the merge base). Every exit code was captured with no pipe between the command and$?, and each gate's own verdict line is what was read.Green — 15 path-matched + 6 convention-triggered families, plus the affected package's own checks:
pnpm --filter @objectstack/cli typecheck— exit 0, script echoed (> tsc --noEmit)Test Files 2 passed (2) · Tests 14 passed (14)check:test-source-alias— "OK — 72 packages with tests scanned; 61 registered as still resolving a workspace dep throughdist/"check:engine-double-contract— "OK — 689 pinned, 134 in the DEBT ledger, 3 exempt"check:cross-package-test-inputs(+ the ci.yml spelling) — "OK: 20 package(s) read outside themselves, all declared"check:where-matcher,check:query-options-erasure— both "baseline key set verified against b72db01: no files added"check:type-check-debt --re-measure— "OK — 31 ledger entr(ies) re-measured, 1687 raw tsc error(s) total, none above its recorded number"check:type-check-coverage,check:nul-bytes,check:i18n,check:i18n-coverage,check:objectql-double-limit,check:page-declaration-shape,check:published-files,check:slot-lookup,check:type-source-resolution,check-ci-filter-parity,check-comment-mask-adoption,check-plugin-teardown-shape,check-affected-docs,check-drift-commentRed: none.
Declared narrowing: the full
@objectstack/clisuite (185 files / ~13 min) was not run locally — the two affected files were. CI runs the package suite in full.Two gates first reported COULD NOT MEASURE rather than red on this fresh worktree (
check:i18nandcheck:i18n-coverage, both naming unbuilt workspace output); both were re-measured green after building, and are reported above as measured, not as inferred.Scope
Clause ②: no accept/reject behaviour change. Evidence is the diff itself —
1 file changed, 190 insertions(+), and the one file is a.test.ts. No production file is touched, so there is no surface on which acceptance could move. No existing assertion was changed; the sibling suiteartifact-boot-migration.test.tsis untouched (0 deletions in the diff).skip-changeset: nothing published changes. A version bump here would announce a release that ships no behaviour, so the label is used rather than a fabricated changeset.The pin lives in
packages/clibecause the gate does, andpackages/drivers/driver-sqlmust not acquire a dependency on the CLI to observe it. The edge that makes this possible already runs the other way —@objectstack/clidepends on@objectstack/driver-sqland already imports its drift surface at run time from a test — so this adds no package edge and nothing to the shrink-onlyKNOWN_UNALIASED_TEST_IMPORTSledger.Generated by Claude Code
Generated by Claude Code