Uh oh!
There was an error while loading. Please reload this page.
fix(cli): close the declaration-boot write guard's two named boundaries — engine-held drivers and immediate DDL - #14505
Conversation
…d-and-count immediate DDL Engine-held drivers: shadow `registerDriver` on the engine instance the kernel publishes (`objectql` / `data`), arm each driver instance in place as it is registered and forward the SAME instance; reach already-held drivers through the engine's public accessors; restore on `disarm()`. Immediate DDL: `dropTable()` / `rotateShards()` get `execute()`'s treatment — forwarded, counted per driver/method/object, warned once per driver, named in the notes. One outcome-line rule: "a plan writes nothing" prints only when nothing was forwarded and nothing refused the override. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
📓 Docs Drift Check12 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 — 22 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 18c1fe0c7be7157ec85cdf5b2e710185a4201fdf && git checkout 18c1fe0c7be7157ec85cdf5b2e710185a4201fdf
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9c7d9d4b343f5ded5b51b757cb4251d1f68cd378 2382e708007576b9e241418d7a7460ea750e61ca && git checkout -B drift-repro 9c7d9d4b343f5ded5b51b757cb4251d1f68cd378 && git merge --no-ff 2382e708007576b9e241418d7a7460ea750e61ca
node scripts/docs-audit/affected-docs.mjs --json 9c7d9d4b343f5ded5b51b757cb4251d1f68cd378 |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14126
What
os migrate plan/os migrate applyboot host plugins for their declarations behind a write guard (#13332 / PR #14053) whose own census named two paths on which a declaration boot could still write while the notes printed "a plan writes nothing". Both are closed here, under ONE outcome-line rule, insidepackages/cli/src/utils/schema-migration-plugins.tsonly —packages/objectql/**andpackages/services/**are untouched.Residue 1 — engine-held drivers (the in-lane shape the dispatch ruled)
registerDriveron the engine INSTANCE the kernel publishes (objectql/data— one object, shadowed once, keyed by instance), with the same in-place own-property techniquearmDriveralready uses;disarm()deletes the own property so the instance resolves through its prototype again.getDefaultDriverName()+getDriverByName(), andregistry.getAllObjects()+getDriverForObject()(both scans; idempotent).via engine.NAME; thedriver.*scan runs first, so the default keeps thedriver.*label it always had.The engine-instance shadow measurement (the FORK CLAUSE did not fire).
ObjectQL.registerDriveris a prototype method (packages/objectql/src/engine.ts); the instance is neither frozen nor sealed —Object.isFrozen(engine) === false,Object.isExtensible(engine) === true, andregisterDriverresolving through the prototype are asserted on the real class in the pin. Every runtime caller reaches it by a call-time property lookup on the published instance:DatasourceConnectionService.connect()viathis.cfg.engine()→ctx.getService('data');AppPlugin'sdrivers.registerviactx.getService('objectql');memory-driver.tsviactx.engine.ql.registerDriver(this). No bound or copiedregisterDriverexists in runtime code — the only.registerDriver =in the repo is a runtime test patching the prototype. In the CLI boot,DefaultDatasourcePlugin.init()(whichdependencies-orders itself afterObjectQLPlugin.init(), whereobjectql/dataare published) registers the default THROUGH the engine and then republishes it asdriver.NAME, and the guard'soptionalDependenciesorders itsinit()after that — so when the guard arms, the engine holds exactly the default, already armed underdriver.*; no base-stack plugin registers a second driver. Every later registration — hostinit()s (Phase 1, after the guard),AppPlugin.start()→connectAll(Phase 2) — arrives through the shadow.What remains open, stated in the header census: an engine the kernel never publishes as a service, and a non-default driver registered BEFORE the guard armed that no registered object resolves to at either scan. Neither is reachable from the CLI without an engine change, and neither occurs in this repo's boot.
Residue 2 — immediate DDL (
dropTable/rotateShards)DRIVER_IMMEDIATE_DDL_METHODS = ['dropTable', 'rotateShards']:IDataDriver.dropTable()(a REQUIRED contract member,packages/spec/src/contracts/data-driver.ts) and driver-sql'srotateShards(objectDef, nowMs)(packages/drivers/driver-sql/src/sql-driver.ts); both runassertSchemaMutable, not the deferral.execute()has: FORWARDED (⛔ not refused — refusing DDL an operator's own hook asked for is a behaviour change beyond this card), counted per driver/method/object (rotateShardstakes the object DEFINITION, so its name is read off it), warned once per driver on stderr, named in the notes — and the claim is withheld.The outcome-line rule, decided once for every remaining path
disarm()appends " — a plan writes nothing" to the refusal line only whenrawExecutions.size === 0 && immediateDdl.size === 0 && unguardable.size === 0 && unshadowable.size === 0. Each of those four prints its own line naming what was forwarded (rawexecute(), immediate DDL) or what refused the override (a frozen driver member, an engine whoseregisterDrivercould not be shadowed) and says the run does NOT claim to have written nothing. A quiet boot — nothing refused, nothing forwarded, nothing unarmable — still returnsnull, so the notes of an embedder with no data plane, or of read/log-only hooks, are byte-identical to before.Tests
packages/cli/src/utils/schema-migration-plugins.declaration-boot-write-guard.test.ts(extended; the new block boots the REALObjectQLbehind stand-ins forObjectQLPluginandDefaultDatasourcePlugin) andpackages/cli/src/utils/schema-migrate.deferred-ddl.integration.test.ts(one case added, realSqlDriverover a real sqlite file throughbootSchemaStack):shadowedEngines === ['objectql']),engine.getDriverByName('reporting')is the very instance the host registered, the write is refused and reportedvia engine.reporting, log-only hooks still ran, the claim HELD, anddisarm()restoresObjectQL.prototype.registerDriverand the driver's own methods.archive_row) is armed throughgetDriverForObject(); its write is refusedvia engine.archive.driver.*default path unchanged: writes throughdriver.recordingand throughengine.getDriverForObject()to the same instance are refused under ONE label,driver.recording, count 2.dropTable()×2 androtateShards()×1 are forwarded (the recording driver ran them, their own return values came back), counted[dropTable on sys_old_table x2, rotateShards on sys_log x1], warned exactly once on stderr for the driver, named in the note, and the claim is withheld while the in-runcreate()control is still refused.null, indistinguishable from a quiet boot).Object.freeze, prototype methods still run, the write LANDS) is named inCould NOT guard …and withholds the claim although the other write was refused.registerDriverun-shadowable) is named inCould NOT shadow objectql.registerDriverand withholds the claim.disarm()→null, hooks untouched.dropTable('defer_widget')on the real sqlite driver during a composed declaration boot: the table is really gone afterwards (forwarded),composition.writeGuard.immediateDdlcounts it, the notes name it, and no note claims "a plan writes nothing".The seven pre-existing #13332 / #14053 pins are unchanged and green — including
THE FIX's "Refused 3 write(s) … a plan writes nothing" and theexecute()pin, which are the positive controls for the unchanged shapes.Ablation (on the committed tree, trap-restored): the one line in
scan()that callsshadowEnginereplaced by a marker — mutation proven on disk (anchor count 1 → 0, marker count 1, blob2f92252f…→d42d9e53…); result: 3 failed / 13 passed — exactly cases 2, 3 and 8 above went red (shadowedEnginescame back[], the archive write landed, noCould NOT shadowline), every residue-2 and control case stayed green; restore proven (git checkout HEAD -- PATH: on-disk blob ==HEADblob,git diff HEAD0 lines,git status --porcelainempty). The subject resolves from source (same-package relative import), so no dist rebuild leg applies to it; the dependency closure was built once before the runs.Base-source leg (the new tests against the pre-fix source at
8094834060, tree-onlygit restore --source=BASE, trap-restored): 7 failed / 9 passed — all seven new pins red (residue 1 ×2:shadowedEnginescame backundefined, the archive write landed; residue 2 ×2:guard.immediateDdlundefined, the note still printed "a plan writes nothing" over a dropped table; ONE RULE ×2; the embedder control assertingshadowedEngines), the seven pre-existing pins and the two fixture controls green; restore proven the same way (on-disk blob ==HEADblob,git diff HEAD0 lines, porcelain empty).Verification
See the report comment on #14126 for the exit-code table (test run, cli typecheck, the 35-family dispatch-gates union, eslint narrowing evidence), each read from the gate's own verdict line with the exit code captured before any pipe.
Out of scope / not done
packages/objectql/**andpackages/services/**are untouched (the in-lane shape did not need them).origin/mainwas not merged into this branch in the worktree (base8094834060;origin/main=00ff228fe0, 53 commits ahead, at the time of this PR). That delta touches neitherregisterDriver, nor this file, its tests or the changeset. It does move.github/workflows/lint.yml(addscheck:swallow-census-controls, a self-test ofmeasure-durability-swallow-family.mjs's control corpora, not a scan of this diff),release.yml, andscripts/pm/dispatch-gates.mjsitself — so the local gate derivation is from the base tree. CI on the merge ref and the queue's rebuilt generation are the authority for the joint tree.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Generated by Claude Code