fix(drivers): object-definition parameters declare the keys they are read for, plus a gate that sees subclass overrides - #16816
Conversation
…iver class family Adds scripts/check-object-def-param-keys.mjs and wires it into lint.yml. Committed BEFORE the repair so the gate's red-before-fix reading on the unmodified tree is anchored to a commit rather than to a working copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
… read for Widens SqlDriver's rotation chain and initObjects, and TursoDriver's initObjects override, to declare tenancy / indexes / lifecycle; deletes the five `(obj as any).<key>` casts that read them. Adds pin tests in all three driver packages, each with a TS2353 negative control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
…bclass-shadowed-declarations
…eclaration check-scripts-symbol-anchors could not resolve `#initObjects` in turso-driver.ts: `override async initObjects(` puts the name mid-line, which is the same wrapped-signature shape this gate exists for. Anchor `#TursoDriver` instead, and say why in the header. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
📓 Docs Drift CheckThis PR changes 2 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 12 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 8000b6688a29243f9f89f7241bbb26b09aa17c7d && git checkout 8000b6688a29243f9f89f7241bbb26b09aa17c7d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 68fd85a411036250755aec9807b1052c1955edc0 56faa7b9c411411a00e6c2d0cad8c379bb1bcc2e && git checkout -B drift-repro 68fd85a411036250755aec9807b1052c1955edc0 && git merge --no-ff 56faa7b9c411411a00e6c2d0cad8c379bb1bcc2e
node scripts/docs-audit/affected-docs.mjs --json 68fd85a411036250755aec9807b1052c1955edc0
|
…bclass-shadowed-declarations
Contract review (
|
Fixes #16711
Clause-②: yesAn object-definition parameter on the SQL driver family now DECLARES every key it is read for — and a gate holds the whole class, including the half that lives in another published package.
The class, and why the third instance stopped being counted as a third
SqlDrivertakes object definitions as inline object-literal parameter types and reads keys off them through(obj as any).KEYthat the literal does not list. Three instances were carded and repaired one key at a time:tenancy(#4311, August),indexes(#16570, this week), andlifecycle, the third. The escape is silent by construction — TypeScript's excess-property check fires on a fresh object literal and not on one bound to a variable first, so the same object is refused at one call site and accepted at another. The loud outcome (a compile error on a correct call) is the harmless one; the bad one is an author, or an AI reading the signature, concluding the key is not accepted and dropping it, at which point a declared UNIQUE is never synced and an ADR-0057 rotation policy is never armed, with nothing anywhere saying so.Triage ruled option B — class sweep plus a gate — and the reason was not thoroughness. Option A was already measured to leak:
TursoDriverOVERRIDESinitObjects, and an override does not inherit the base's parameter type, so #4311's fix was invisible from outside@objectstack/driver-sqlfor five weeks and #16570's would have escaped identically.The one sentence that decided the gate's shape
So
scripts/check-object-def-param-keys.mjsis not scoped to a file. It reads every tracked TypeScript source under a package'ssrc/(2,200 files, 431 classes at56faa7b9c4) and runs three arms:(x as any).KEYwherexis a parameter annotated with an inline object literal, or afor…ofbinding over an array of one, and that KEY is not in it. That second form matters: thelifecycleread is spelled exactly that way.It reads parameter lists as an AST, not with a regex, because the signature this class hides behind wraps across lines — the card's own PM comment records a single-line grep for
indexes|tenancyover the Turso override returning nothing and its control returning nothing too. That was a silence, not a negative.⭐ The gate goes RED on
TursoDriver's current signature BEFORE the fixThis is the deliverable that makes this option B rather than option A, and it is 验收口径 item 2. Measured on
83863b2dfe— an otherwise unmodifiedorigin/maincheckout, the gate script the only file added, no repair applied:That commit is in this branch's history (
b4a9788cc4), pushed before the repair, so the reading is reproducible rather than quoted. After the repair the same command printsOK — 2200 source file(s), 431 class(es), 2 override parameter position(s) compared.and exits 0 (re-run at56faa7b9c4, this branch's final commit).⭐ A gate that is only ever run green after the repair is indistinguishable from no gate, so the red does not depend on that one-off run:
--self-testembedsTursoDriver.initObjects's pre-repair signature verbatim as a permanent firing control and asserts it goes red for both keys, alongside the wrapped-signature shape and both directions of every arm.TursoDriver.registerRemoteFieldMetadataread(obj as any).tenancyoff a parameter declaring{ name; fields? }— the same class, one method over, in the same override's call path. It was not in the card's census (which enumeratedsql-driver.tsonly) and not in the PM comment (which measured the override's signature). Arm B found it. Fixed in the same pass, since it is the same defect class on a file this PR already holds.What widened
driver-sqlrotateShardstenancy,indexesdriver-sqlensureRotationtenancy,indexesdriver-sqlensureShardTableindexesdriver-sqlinitObjectslifecycledriver-tursoinitObjects(override)tenancy,indexes,lifecycledriver-tursoregisterRemoteFieldMetadatatenancyAll three rotation links carry the keys, not just the leaf that reads them:
rotateShards → ensureRotation → ensureShardTableall receive the same caller object, and declaring the keys only on the leaf would leave the two links above still narrowing the value in flight, so a fresh literal handed to the public entry point would still have been refused.Five
(obj as any).KEYcasts deleted, including the residual one indetectManagedDriftwhose parameter had declaredindexesall along.as any).insql-driver.tsgoes 8 → 4; the four survivors all readthis.configor a local, none is an object-definition parameter.The three packages'
.d.ts, measured on the built declarationsdriver-sql(source) — every widened signature present indist/index.d.ts:driver-turso(override — widened too) —dist/index.d.tsre-declaresinitObjectsexactly once, carrying all five keys:driver-sqlite-wasm(inherits — asserted, not assumed) — itsdist/index.d.tsopensimport { SqlDriver, SqlDriverConfig } from '@objectstack/driver-sql'and re-declares none of the five members:Its whole class body is
name,version,isSqlite,supportsWalJournal,wasmConfig,beforeExitHandler, the constructor,toKnexConfig,connect,disconnect,flush.src/sqlite-wasm-16711-inherited-object-def-keys.test.tspins the inheritance inside that package's own tsc program so it stays measured.⭐ The negative control (验收口径 item 4)
A "fix" that set these parameters to
any, or bolted on an index signature, would turn every item above green while deleting the whole layer of type protection. Each of the three new pin tests carries a compile-time-only export whose@ts-expect-errordirectives ARE the assertion —tscfails the file with TS2578 the moment a misspelling starts being accepted:driver-sql:lifecycl,indexs,tenancyy, plus two keys nobody declares, acrossinitObjects/rotateShards/registerObjectMetadatadriver-turso:tenancyy,indexs, and an undeclared key on the overridedriver-sqlite-wasm:tenancyyand an undeclared key on the inherited doorpnpm typecheckis green for all three packages, which is what says every one of those directives still fired. And--listFilesproves the instrument reaches them: each pin test is in its package's tsc program (1 hit each, against a control pattern scoring 0).16711, so a naivegrep -c 16711over--listFilesreturns the whole file count.The two recorded workarounds
Both were read before being touched, as the standing instruction requires.
sql-driver-11794-richtext-text-family.test.ts— the hoist comment. Its fossil gives TWO reasons: mirroring driver-sql: the platform-objects schema does not sync onto MySQL — unbounded string fields become TEXT, which MySQL refuses to index #11374'sboundedObject()authoring shape, andindexesriding "beyond its narrow parameter type". The second clause EXPIRED when driver-sql:initObjects/registerObjectMetadatastill omitindexesfrom a parameter type they read it through — the shape #4311 fixed fortenancy, one key over #16570 declared the key — before this PR, not because of it. The hoist is kept and the expired clause is rewritten as a dated record, saying plainly that an inline literal would compile today and that the pin which must stay inline is the new one. Overturning the first reason is not this card's to do, and that suite is about column widths.sql-driver-15479-shadow-plain-unique-duplicates.test.ts— threeas anycasts oninitObjectsarguments. Nothing in that file's fossil defends them; they exist because the parameter did not declareindexes. All three removed, with a comment saying why, and the file typechecks and its calls are now ordinarily checked.Changeset —
minor× 3, and the text I rejected.github/workflows/pr-automation.yml's WHICH LEVEL block governs:This adds newly accepted keys on published methods of published packages, so it is squarely that. Rejected:
patch— AGENTS.md's "a bug fix in a released package takes a patch changeset" is the floor againstnone, not a ceiling, and the block above says the act wins when the commit type disagrees. Rejected: theskip-changesetlabel (route 2) — this diff changes published.d.tsbytes in two packages, so it releases something. Rejected:major— refused during the launch window, and a widening is not breaking.driver-sqlite-wasmgets an entry too, decided rather than defaulted: its own source did not change, but its published accept set moves for its consumers through the inherited.d.ts, and a consumer reading only that package's changelog would otherwise never learn it — which is this card's own defect one layer up. All three are in the samefixedgroup, so the version is identical either way; the entry buys the changelog line.check-changeset-no-majorcarries no information about the level here. ItsPUBLISHED_SOURCE_PATH = /^packages\/([^/]+)\/src\//cannot match a nested package dir, sopackages/drivers/*is invisible to its LEVEL axis. That regex is #16713, ruled indomain:devxand out of scope here — this PR does not address it, and the ruling requires one single change to cover it together with #16692.Verification
Everything below was re-run at
56faa7b9c4, this branch's final commit, after mergingorigin/main.pnpm --filter @objectstack/driver-sql --filter @objectstack/driver-turso --filter @objectstack/driver-sqlite-wasm typecheck— exit 0, all threeDone. This is the instrument for the negative control: every@ts-expect-errorabove still fired, or tsc would have failed the file with TS2578.driver-sql11 passed / 3 skipped over 4 files (the three skips are the named live-Postgres / live-MySQL cells of the D-A3 driver axis, absentOS_TEST_*_URL),driver-turso2/2,driver-sqlite-wasm2/2.sql-driver-16570-init-objects-indexes-param.test.tsre-run green.sql-driver-15479-…test.ts, one of the two workaround files, is a whole-file live-MySQL cell and SKIPS in this container. Its threeas anyremovals are verified bytsconly; its runtime is CI's live-MySQL job. Stated rather than implied.npx eslint --no-inline-config .over the whole tree: 6355 files, 0 errors, 0 warnings, exit 0. The population is read from eslint's own--format jsonoutput, not estimated.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 99, re-derived after adding the gate so its own family appears (pnpm check:object-def-param-keysis in the list). All 99 were run, each exit code captured before any pipe. Reconciliation, verbatim:check:dual-build-cjs-loads,check:type-check-debt), which is NOT MEASURED and not a pass. Both were re-run afterpnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'(72/72 tasks successful) and are green: dual-build at 104/67/620/1 against floors 90/58/520/1, and--re-measure: OK — 5 ledger entr(ies) re-measured, 55 raw tsc error(s) total, none above its recorded number.验收备注 — the ruling's five items, with dispositions
sql-driver.ts. ✅ Arm A, over the whole workspace corpus; the base class is resolved by name across packages, and a duplicated base name is reportedambiguousrather than guessed.TursoDriverbefore the fix. ✅ Reproduced above at commitb4a9788cc4(exit 1, 5 problems), and made permanent as a--self-testfixture carrying the pre-repair signature verbatim..d.tschecked; the inheriting one asserted, ⛔ not assumed. ✅ Read off the built declarations, quoted above, and pinned indriver-sqlite-wasm's own tsc program.any, no index signature. ✅ Three@ts-expect-errorbatteries, plus arm C, which refuses both escape hatches mechanically.initObjects/registerObjectMetadatastill omitindexesfrom a parameter type they read it through — the shape #4311 fixed fortenancy, one key over #16570 / PR fix(driver-sql): declare theindexeskeyinitObjects/registerObjectMetadataalready read #16710, and driver-sql: the hash-shadow NULL-safe arm still hand-rolls duplicate-group formatting, the drift formatDuplicateGroups exists to prevent #16289 is untouched — both remain open and are not addressed by this PR.Out of scope, noted and not filed
ensureRotationstill declares nolifecycleandregisterObjectMetadatanolifecycle, deliberately: neither reads it. The sweep's criterion is the card's own — keys actually read — and widening on sibling-consistency instead would be a different rule with a different boundary. Raised as an open question in the report rather than decided here.packages/cli/src/commands/generate-string-family-width.pin.test.tsdeclaresclass DriverOracle extends SqlDriver, a test double that WIDENS (it republishes protected members). The gate's corpus is production sources, so it is out of scan scope; widening doubles are not this class's failure mode. Observation only.a15e006125carries a stray!in its conventional-commit type. This change is a WIDENING and is not breaking; the PR title and the threeminorchangesets are the authority, and a force-push to correct a message is not available here.