Skip to content

fix(drivers): object-definition parameters declare the keys they are read for, plus a gate that sees subclass overrides - #16816

Merged
os-zhuang merged 5 commits into
mainfrom
claude/issue-16711-subclass-shadowed-declarations
Sep 8, 2026
Merged

fix(drivers): object-definition parameters declare the keys they are read for, plus a gate that sees subclass overrides#16816
os-zhuang merged 5 commits into
mainfrom
claude/issue-16711-subclass-shadowed-declarations

Conversation

@os-musk

@os-musk os-musk commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16711

Clause-②: yes

An 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

SqlDriver takes object definitions as inline object-literal parameter types and reads keys off them through (obj as any).KEY that the literal does not list. Three instances were carded and repaired one key at a time: tenancy (#4311, August), indexes (#16570, this week), and lifecycle, 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: TursoDriver OVERRIDES initObjects, and an override does not inherit the base's parameter type, so #4311's fix was invisible from outside @objectstack/driver-sql for five weeks and #16570's would have escaped identically.

The one sentence that decided the gate's shape

⛔ 闸门若只盯 sql-driver.ts,#4311 与本次都拦不住

So scripts/check-object-def-param-keys.mjs is not scoped to a file. It reads every tracked TypeScript source under a package's src/ (2,200 files, 431 classes at 56faa7b9c4) and runs three arms:

  • A — override narrowing. For every class extending another class in the corpus, every method on both: each parameter position whose base type is an inline object literal must declare every key the base declares. Crosses package boundaries by construction.
  • B — undeclared cast read. (x as any).KEY where x is a parameter annotated with an inline object literal, or a for…of binding over an array of one, and that KEY is not in it. That second form matters: the lifecycle read is spelled exactly that way.
  • C — the escape hatches. An index signature on such a parameter, or an override replacing the base's literal with an opaque type, makes A and B vacuous. The gate refuses both directly rather than trusting a reviewer to notice. Measured 0 on this tree, so its ledger ships empty.

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|tenancy over 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 fix

This is the deliverable that makes this option B rather than option A, and it is 验收口径 item 2. Measured on 83863b2dfe — an otherwise unmodified origin/main checkout, the gate script the only file added, no repair applied:

$ node scripts/check-object-def-param-keys.mjs ; echo "EXIT=$?"
check-object-def-param-keys self-test: OK (2200 corpus file(s), every control fired)
check:object-def-param-keys: 5 problem(s)

  ── A · an override declares FEWER keys than the base it shadows ──
  packages/drivers/driver-turso/src/turso-driver.ts:1548  TursoDriver extends SqlDriver — initObjects(param 0)
      drops: tenancy, indexes
      base declares {name; fields; tenancy; indexes} at packages/drivers/driver-sql/src/sql-driver.ts:9948

  ── B · a key is READ off a parameter its own type does not declare ──
  packages/drivers/driver-sql/src/sql-driver.ts:9562  ensureShardTable: (obj as any).indexes
  packages/drivers/driver-sql/src/sql-driver.ts:9589  ensureShardTable: (obj as any).indexes
  packages/drivers/driver-sql/src/sql-driver.ts:10007  initObjects: (obj as any).lifecycle
  packages/drivers/driver-turso/src/turso-driver.ts:1323  registerRemoteFieldMetadata: (obj as any).tenancy
EXIT=1

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 prints OK — 2200 source file(s), 431 class(es), 2 override parameter position(s) compared. and exits 0 (re-run at 56faa7b9c4, 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-test embeds TursoDriver.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.

⚠️ Also found by the gate, and repaired: a fourth site nobody had carded

TursoDriver.registerRemoteFieldMetadata read (obj as any).tenancy off 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 enumerated sql-driver.ts only) 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

package method keys added
driver-sql rotateShards tenancy, indexes
driver-sql ensureRotation tenancy, indexes
driver-sql ensureShardTable indexes
driver-sql initObjects lifecycle
driver-turso initObjects (override) tenancy, indexes, lifecycle
driver-turso registerRemoteFieldMetadata tenancy

All three rotation links carry the keys, not just the leaf that reads them: rotateShards → ensureRotation → ensureShardTable all 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).KEY casts deleted, including the residual one in detectManagedDrift whose parameter had declared indexes all along. as any). in sql-driver.ts goes 8 → 4; the four survivors all read this.config or a local, none is an object-definition parameter.

The three packages' .d.ts, measured on the built declarations

driver-sql (source) — every widened signature present in dist/index.d.ts:

rotateShards(objectDef: { name: string; fields?: Record<string, any>; tenancy?: any; indexes?: any[]; lifecycle?: any; }, nowMs?: number)
protected ensureRotation(tableName: string, obj: { name: string; fields?: Record<string, any>; tenancy?: any; indexes?: any[]; }, )
protected ensureShardTable(shardName: string, obj: { fields?: Record<string, any>; tenancy?: any; indexes?: any[]; })
initObjects(objects: Array<{ name: string; fields?: Record<string, any>; tenancy?: any; indexes?: any[]; lifecycle?: any; }>)

driver-turso (override — widened too)dist/index.d.ts re-declares initObjects exactly once, carrying all five keys:

initObjects(objects: Array<{ name: string; fields?: Record<string, any>; tenancy?: any; indexes?: any[]; lifecycle?: any; }>)

driver-sqlite-wasm (inherits — asserted, not assumed) — its dist/index.d.ts opens import { SqlDriver, SqlDriverConfig } from '@objectstack/driver-sql' and re-declares none of the five members:

initObjects(                     0
registerObjectMetadata(          0
rotateShards(                    0
ensureShardTable(                0
registerManagedObjectMetadata(   0

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.ts pins 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-error directives ARE the assertion — tsc fails the file with TS2578 the moment a misspelling starts being accepted:

  • driver-sql: lifecycl, indexs, tenancyy, plus two keys nobody declares, across initObjects / rotateShards / registerObjectMetadata
  • driver-turso: tenancyy, indexs, and an undeclared key on the override
  • driver-sqlite-wasm: tenancyy and an undeclared key on the inherited door

pnpm typecheck is green for all three packages, which is what says every one of those directives still fired. And --listFiles proves the instrument reaches them: each pin test is in its package's tsc program (1 hit each, against a control pattern scoring 0). ⚠️ That control matters here — the worktree path contains 16711, so a naive grep -c 16711 over --listFiles returns the whole file count.

The two recorded workarounds

Both were read before being touched, as the standing instruction requires.

Changeset — minor × 3, and the text I rejected

.github/workflows/pr-automation.yml's WHICH LEVEL block governs:

A purely additive widening of a published package's public surface (a new exported symbol on an index, a new accepted key or value) takes at least minor. The commit type may raise a bump but never lower it below what the act requires.

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 against none, not a ceiling, and the block above says the act wins when the commit type disagrees. Rejected: the skip-changeset label (route 2) — this diff changes published .d.ts bytes in two packages, so it releases something. Rejected: major — refused during the launch window, and a widening is not breaking.

driver-sqlite-wasm gets 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 same fixed group, so the version is identical either way; the entry buys the changelog line.

⚠️ A green from check-changeset-no-major carries no information about the level here. Its PUBLISHED_SOURCE_PATH = /^packages\/([^/]+)\/src\// cannot match a nested package dir, so packages/drivers/* is invisible to its LEVEL axis. That regex is #16713, ruled in domain:devx and 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 merging origin/main.

  • pnpm --filter @objectstack/driver-sql --filter @objectstack/driver-turso --filter @objectstack/driver-sqlite-wasm typecheck — exit 0, all three Done. This is the instrument for the negative control: every @ts-expect-error above still fired, or tsc would have failed the file with TS2578.
  • Pin suites, exit 0 each: driver-sql 11 passed / 3 skipped over 4 files (the three skips are the named live-Postgres / live-MySQL cells of the D-A3 driver axis, absent OS_TEST_*_URL), driver-turso 2/2, driver-sqlite-wasm 2/2. sql-driver-16570-init-objects-indexes-param.test.ts re-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 three as any removals are verified by tsc only; 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 json output, not estimated.
  • Gate families: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 99, re-derived after adding the gate so its own family appears (pnpm check:object-def-param-keys is in the list). All 99 were run, each exit code captured before any pipe. Reconciliation, verbatim:
Run reconciliation — 99 derived, 99 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 99 derived famil(ies) accounted for — 99 run, 0 NOT-MEASURED.
  • Two of those first returned exit 3 — PREREQUISITE NOT MET (check:dual-build-cjs-loads, check:type-check-debt), which is NOT MEASURED and not a pass. Both were re-run after pnpm 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

  1. Gate covers the subclass-override shape, ⛔ not only 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 reported ambiguous rather than guessed.
  2. The gate has its own firing control; RED on TursoDriver before the fix. ✅ Reproduced above at commit b4a9788cc4 (exit 1, 5 problems), and made permanent as a --self-test fixture carrying the pre-repair signature verbatim.
  3. All three packages' .d.ts checked; the inheriting one asserted, ⛔ not assumed. ✅ Read off the built declarations, quoted above, and pinned in driver-sqlite-wasm's own tsc program.
  4. Negative control: a misspelling still raises TS2353; ⛔ no any, no index signature. ✅ Three @ts-expect-error batteries, plus arm C, which refuses both escape hatches mechanically.
  5. Isolation boundaries stand. ✅ Nothing here folds into driver-sql: initObjects / registerObjectMetadata still omit indexes from a parameter type they read it through — the shape #4311 fixed for tenancy, one key over #16570 / PR fix(driver-sql): declare the indexes key initObjects / registerObjectMetadata already 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

  • The rotation chain's ensureRotation still declares no lifecycle and registerObjectMetadata no lifecycle, 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.ts declares class 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.
  • ⚠️ The intermediate commit a15e006125 carries a stray ! in its conventional-commit type. This change is a WIDENING and is not breaking; the PR title and the three minor changesets are the authority, and a force-push to correct a message is not available here.

…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
…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
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-sql, @objectstack/driver-turso, touching 8 documentable anchor(s).

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx (via SqlDriver (symbol, a top-level class), TursoDriver (symbol, a top-level class))
  • content/docs/data-modeling/index.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/permissions/tenant-audit-census.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via SqlDriver (symbol, a top-level class), TursoDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/lifecycle.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/protocol/objectql/query-syntax.mdx (via SqlDriver (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via SqlDriver (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 12 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 68fd85a411036250755aec9807b1052c1955edc0packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8000b6688a29243f9f89f7241bbb26b09aa17c7d — the merge of head 56faa7b9c411411a00e6c2d0cad8c379bb1bcc2e into base 68fd85a411036250755aec9807b1052c1955edc0, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 68fd85a411036250755aec9807b1052c1955edc0 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Sep 8, 2026
@os-musk os-musk added needs:contract-review and removed documentation Improvements or additions to documentation ci/cd size/xl dependencies Pull requests that update a dependency file tests tooling labels Sep 8, 2026 — with Claude

Copy link
Copy Markdown
Contributor

Contract review (CONTRACT_REVIEW_TIER, isolated seat) — PR #16816 @ 56faa7b9c

Verdict: PASS WITH FINDINGS

Ruling implemented: yes — option B (class sweep + gate), all five 验收口径 items, verified independently on the fetched head (refs/review/16816, base 68fd85a41), not from the PR body.

The ruling, and whose it is

There is no comment titled ## Ruling recorded on #16711. The operative ruling is the triage comment 5578361301 (author os-zhuang, MEMBER), which self-identifies as the triage seat (「本席权限声明:分诊席只分类/定级/定车道,以及裁定卡内的范围形状(A/B)」) and is Claude-generated. ⇒ a seat's ruling, not a maintainer's. The seat asserts the A/B choice is within its remit and that the card does not enter the decision box (「加宽参数类型是放宽接受集」). The claim comment 5580636840 (exec PM seat) carried it into dispatch unchanged. Quoted verbatim:

裁定:选项 B(类扫 + 闸门),⛔ 不取 A

验收口径(承接 PR 请照抄进 ## 验收备注

  1. 闸门的作用域必须覆盖"子类覆写"这一形状,⛔ 不得只扫 sql-driver.ts。它至少要能回答:每个 extends SqlDriver 的类,其覆写方法的参数类型是否窄于被覆写者? —— 这条正是 [P2] framework: 66 个包用 tsup 构建、无人做类型检查 —— 实测 18 个包共 380 处 code-tier 错误(#4118 的 framework 侧对应) #4311 与本次都逃掉的那道题。
  2. 闸门自己要有发火对照:把 TursoDriver 现在的签名当作已知阳性样本,闸门在修复前必须对它报红。⛔ 只在修复后跑一次绿的闸门,与没有闸门无法区分 —— 本批 [finding] The changeset LEVEL axis is blind to every NESTED package: packages/*/src/** matches one segment, so 51 of 74 workspace packages (all drivers/services/adapters) can pair Clause-②: yes with patch and stay green #16713 正在处理的就是这种"没看"与"看过并批准"同色的绿。
  3. 三个包各自的 .d.ts 都要检查driver-sql(源)、driver-turso(覆写,必须一起加宽)、driver-sqlite-wasm(继承,应自动跟随 —— 断言它确实跟随了,⛔ 不要假定)。
  4. 阴性对照:加宽后,一个真的不该被接受的键(随便一个拼错的名字)在新鲜字面量上仍须报 TS2353。一个把参数类型放成 any 或加了索引签名的"修法"会让上面每条都绿,同时把整层类型保护删掉。
  5. 卡面的隔离边界原样遵守:⛔ 不并入 driver-sql: initObjects / registerObjectMetadata still omit indexes from a parameter type they read it through — the shape #4311 fixed for tenancy, one key over #16570 / PR fix(driver-sql): declare the indexes key initObjects / registerObjectMetadata already read #16710(该卡有意围在一个键上),⛔ 不并入 driver-sql: the hash-shadow NULL-safe arm still hand-rolls duplicate-group formatting, the drift formatDuplicateGroups exists to prevent #16289。本卡序列化在 PR fix(driver-sql): declare the indexes key initObjects / registerObjectMetadata already read #16710 之后(面重叠 :9600:9760)——⛔ 这不是催它的理由。

Numbered verification

  1. Files (13, diff vs merge-base 68fd85a41): .changeset/driver-sql-object-def-param-keys.md, .changeset/driver-sqlite-wasm-inherits-object-def-keys.md, .changeset/driver-turso-init-objects-declares-base-keys.md, .github/workflows/lint.yml (+23), package.json (+1), packages/drivers/driver-sql/src/sql-driver-11794-richtext-text-family.test.ts, …/sql-driver-15479-shadow-plain-unique-duplicates.test.ts, …/sql-driver-16711-object-def-param-keys.test.ts (new), …/sql-driver.ts, packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-16711-inherited-object-def-keys.test.ts (new), packages/drivers/driver-turso/src/turso-driver-16711-init-objects-param.test.ts (new), …/turso-driver.ts, scripts/check-object-def-param-keys.mjs (new, 783 lines). Governed paths: NO — none of docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, content/docs/releases/** is touched (Governed Surface Queue Guard: success). Gate wiring: package.json check:object-def-param-keys = --self-test && run (so check-self-test-wired's superset rule is satisfied by construction); the workflow step is at lint.yml:4410 inside job typecheck-source-gates (check-run "Type Check · source gates"), ⚠️ not inside lint ("Lint & Repo Gates") as the PR body's "wired into lint.yml" might be read; dispatch-gates derives families from any pnpm check:* run-step in .github/workflows/*.yml (extractCheckInvocations, dispatch-gates.mjs:2066), so the new family is auto-derived. CI log of that job on head, verbatim: check-object-def-param-keys self-test: OK (2200 corpus file(s), every control fired)check:object-def-param-keys: OK — 2200 source file(s), 431 class(es), 2 override parameter position(s) compared.

  2. Declarations. as any). counts, main → head: sql-driver.ts 8 → 4 (the four survivors read conn/this.config, none an object-definition parameter); turso-driver.ts 1 → 0; sqlite-wasm-driver.ts unchanged (1, unrelated). Widened, with the read site each key is declared for:

    • SqlDriver.rotateShards(objectDef) +tenancy?, +indexes?: any[] — forwards the same object to ensureRotationensureShardTable (chain, no direct read).
    • SqlDriver.ensureRotation(_, obj) +tenancy?, +indexes?: any[] — chain link.
    • SqlDriver.ensureShardTable(_, obj) +indexes?: any[] — reads at head :9575 (declaredIndexes: obj.indexes) and :9602 (const declared = obj.indexes), were (obj as any).indexes at main :9562/:9589.
    • SqlDriver.initObjects(objects) +lifecycle? — read at head :10031 (obj.lifecycle?.storage), was (obj as any).lifecycle at main :10007.
    • SqlDriver.detectManagedDrift — no widening (declared indexes?: any[] already); residual cast at main :11223 deleted.
    • TursoDriver.initObjects (override) +tenancy?, +indexes?: any[], +lifecycle? — now a superset of the base's literal.
    • TursoDriver.registerRemoteFieldMetadata(obj) +tenancy? — read at :1418, was (obj as any).tenancy.
    • SqliteWasmDriver overrides only connect/disconnect/flush/toKnexConfig (measured on sqlite-wasm-driver.ts) — inherits.
      No as any read of an object-definition key remains undeclared in either driver file.
  3. The gate. Arm A asserts, for every class X extends Y where Y resolves to exactly one class in the corpus and both declare a method of the same name: for each parameter position whose base annotation is an inline object literal (or T[] / Array<T> of one), the override's declared key set ⊇ the base's (findOverrideFindings). Arm B: (p as any).k / ?.k / ['k'] where p is a literal-typed parameter or a for…of binding over an array-literal-typed parameter, k undeclared. Arm C: override erases the base literal with an opaque/absent annotation, or either side carries an index signature. Corpus: git ls-files packages filtered to *.ts/src/ ∧ not .test|.spec — I re-derived it on the head tree: 2200 files (matches), exactly one class SqlDriver (sql-driver.ts:4344), three non-test subclasses (LegacyStorageDriver testkit, SqliteWasmDriver, TursoDriver). Would it have caught TursoDriver.initObjects at origin/main? Yes: base param 0 classifies array with keys {name, fields, tenancy, indexes} (Array<…> type-reference branch of classifyParamType), the override classifies array with {name, fields}missing = [tenancy, indexes], and the base name is unambiguous. The red-before-fix commit is real: b4a9788cc adds only lint.yml / package.json / the script (3 files, +802) on an otherwise untouched tree (0 lines of packages/drivers diff vs its parent). Self-test: present, with a firing positive control (TURSO_OVERRIDE_BEFORE_16711, asserting missing === 'tenancy,indexes' and derived === 'TursoDriver'), the wrapped-signature trap, arm B for parameter and for…of, arm C both shapes, and corpus positive controls (REQUIRED_CORPUS_FILES × 3, refuses with exit 3 when absent). The production run re-runs the self-test before sweeping. Gap noted as F1.

  4. Accept-set movement. All added keys are optional ?: any / ?: any[] on class methods → widening only, no narrowing of any published signature. Fresh literals with lifecycle (initObjects on all three drivers) and tenancy/indexes (rotateShards, Turso initObjects) now compile; a misspelling is still TS2353 (pinned, item 6). The one theoretical reverse movement — a variable-bound caller of rotateShards/ensureRotation/ensureShardTable whose indexes is not any[]-assignable — has 0 affected in-repo callers: non-test callers of the widened methods across packages/, apps/, examples/ are packages/objectql/src/lifecycle/lifecycle-service.ts:1050 (rotateShards, through the local RotationCapableDriver interface, not the class type) and examples/app-showcase/src/system/datasources/external-fixture.ts:103 (initObjects, through a local (objs: unknown[]) shape). Test callers carrying initObjects(...) as any (61) are unaffected by construction. Type Check · workspace / consumer gates: success.

  5. Changeset. Three entries, each minor: @objectstack/driver-sql, @objectstack/driver-turso, @objectstack/driver-sqlite-wasm — all in the same fixed group (.changeset/config.json), so one version moves. Level: batch [WIP] Add query enhancements and advanced validation features #35 "WHICH LEVEL" (pr-automation.yml:667: additive widening of a published surface takes at least minor; commit type never lowers it) → minor is correct for the two packages whose .d.ts bytes move (driver-sql, driver-turso) and defensible for driver-sqlite-wasm (inherited .d.ts, no source diff — a changelog line, not a version driver). No breaking carriers — nothing narrows. FROM initObjects: Array<{name; fields?; tenancy?; indexes?}> / rotateShards: {name; fields?; lifecycle?} / Turso initObjects: Array<{name; fields?}> TO the five-key / four-key literals quoted in item 2. LEVEL-axis blind spot: scripts/check-changeset-no-major.mjs:822 PUBLISHED_SOURCE_PATH = /^packages\/([^/]+)\/src\// cannot match packages/drivers/*/src ([finding] The changeset LEVEL axis is blind to every NESTED package: packages/*/src/** matches one segment, so 51 of 74 workspace packages (all drivers/services/adapters) can pair Clause-②: yes with patch and stay green #16713), so Check Changeset: success says nothing about the level here; graded by this seat as above: minor × 3 is right. Clause-②: yes is in the PR body and needs:contract-review is on both carriers.

  6. Tests. sql-driver-16711-object-def-param-keys.test.ts: §1 initObjects([{ ...bare, lifecycle }]) inline + runtime (view + shards exist), §2 rotateShards({ ...bare, tenancy, indexes, lifecycle }) inline via the public entry point + runtime UNIQUE on the shard, §3 detectManagedDrift inline; negative control: five @ts-expect-error on fresh literals (lifecycl, indexs, tenancyy, two undeclared) plus the narrowing axis (indexes as a record → TS2322). turso-driver-16711-init-objects-param.test.ts: inline four-key literal + tenancy.enabled:false read at runtime; three @ts-expect-error. sqlite-wasm-16711-inherited-object-def-keys.test.ts: IsAny guard on the inherited parameter, Pick<…, 'tenancy'|'indexes'|'lifecycle'|'fields'> presence (TS2344 if absent), inline literal + runtime, two @ts-expect-error. All pins are fresh literals in argument position (variable-bound pins would be vacuous for TS2353). Typecheck coverage: all three tsconfig.json include src/**/* (tests in the program) and typecheck = tsc --noEmit; a relaxation to any/index signature turns each directive into TS2578. .skip/.only/.todo: 0 across the five touched/new test files. Workaround files: 15479 — three as any removed (verified by tsc only; it is a live-MySQL cell, stated in the PR body); 11794 — hoist kept, expired clause rewritten as a dated record. driver-sql: initObjects / registerObjectMetadata still omit indexes from a parameter type they read it through — the shape #4311 fixed for tenancy, one key over #16570's pin file untouched; driver-sql: the hash-shadow NULL-safe arm still hand-rolls duplicate-group formatting, the drift formatDuplicateGroups exists to prevent #16289 untouched.

  7. CI on head 56faa7b9c: 37 check runs — 33 success, 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke, all opt-in/filtered), 1 in progress: Lint & Repo Gates (started 08:08Z, still running at 08:31Z; it hosts check-scripts-symbol-anchors, check-self-test-wired, check:pm-dispatch-gates and the clause-② carrier gate, so it is the remaining reading). The new gate does not run there — it ran green in Type Check · source gates (log quoted in item 1). mergeable_state: blocked (draft, contract review pending). Head is 1 commit behind origin/main (d4401f75b), no overlap with this diff.

Findings

  • F1 (medium, non-blocking; follow-up card, not this PR). Arm A's positive control is corpus-level only. If a second class SqlDriver ever appears in any packages/**/src/*.ts (a shim, a fixture that is not named .test/.spec), findOverrideFindings classifies the pair ambiguous, judges it by nothing, and the production run still exits 0 — the ambiguity is printed only under --list, and comparedPairs silently drops from 2. That is precisely "a green indistinguishable from an unfired gate", one level up from where the gate already guards it. Expectation: the production report treats an ambiguous entry as red (or at least prints it), and/or a REQUIRED_COMPARISONS control (TursoDriver.initObjects#0 → SqlDriver) is asserted inside sweep() the same way REQUIRED_CORPUS_FILES is. Today's tree measures exactly one SqlDriver, so the arm fires now.
  • F2 (low). Commit a15e00612 is titled fix(drivers)!: — a BREAKING marker on a widening. The PR title and the three minor changesets are the authority and no gate reads commit subjects for !, but a merge-commit (non-squash) landing puts that marker into main's history. Expectation: squash-merge, or the maintainer notes it; no code change.
  • F3 (info, accepted scope). Arm B reads only (ident as any) spellings; as Record<string, any>, as unknown as X, destructuring off a cast, and .tsx are outside the instrument, and test doubles (DriverOracle in packages/cli) are outside the corpus. The exec PM seat ruled the corpus and the keys-read criterion (comment 5581715405); the sibling asymmetry it names (registerObjectMetadata / registerManagedObjectMetadata / ensureRotation declare no lifecycle; detectManagedDrift declares neither tenancy nor lifecycle) is consistent with keys-read — none of them reads those keys (measured on head). Expectation: none for this PR.
  • F4 (info). The PR body's .d.ts readings are built-declaration claims this seat could not re-run (no build permitted here); they are consistent with the source signatures and with the sqlite-wasm type-level pin, which is the durable half. Expectation: none.

Merge authority: no governed path is hit, so PD#14's hand-merge rule is not triggered by path — but this PR is Clause-②: yes (accept-set widening on three published packages) and sits on CONTRACT_REVIEW_TIER; per the dispatch seat's own note it does not land on the dispatch tier. Lint & Repo Gates must complete green before any landing decision. This seat did not approve, request changes, label, edit, or merge.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 8, 2026 08:34
@os-zhuang
os-zhuang enabled auto-merge September 8, 2026 08:34
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 7862fb7 Sep 8, 2026
42 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-16711-subclass-shadowed-declarations branch September 8, 2026 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

3 participants