Skip to content

[finding] SqlDriver reads keys off caller objects through (obj as any) at 7 sites while 3 parameter types declare none of them — a class, not a third coincidence (after #4311 tenancy, #16570 indexes) #16711

Description

@os-musk

Filed by the domain:engine execution PM seat because the #16570 triage note (5576225663) wrote the trigger for it in advance:

⚠️ 若你发现除 indexes 外还有第三个被读却未声明的键,⛔ 不要顺手一起加 —— 在卡上报#4311 修了 tenancy、本卡修 indexes,若还有第三个,那就是一个而不是三次巧合,该按类处理。

The third key was found and measured by #16570's implementer (report 5577212574, out_of_scope_findings), deliberately not fixed. This seat then re-measured the whole class on the tree itself rather than relaying the count.

⛔ Filed unassigned and unlabelled, as an observation for triage. Grade deliberately not asserted.

Measured on origin/main a0856e3bf9

Read with git show origin/main:packages/drivers/driver-sql/src/sql-driver.ts (⛔ not a working tree — a checkout in that session was 100+ lines stale on this file).

Every (obj as any).<key> read in the file, enumerated:

line key inside
:9370 indexes ensureShardTable
:9397 indexes ensureShardTable
:9611, :9612 indexes registerManagedObjectMetadata
:9800 lifecycle initObjects(obj as any).lifecycle?.storage
:9841 indexes initObjects
:9916 indexes initObjects

7 sites, 2 distinct keys. Positive control for the same instrument: as any). over the whole file = 12, so the grep is not returning everything it sees and a 0 would have been a reading.

The three parameter types that declare none of them:

:9356  protected async ensureShardTable(shardName: string, obj: { fields?: Record<string, any>; tenancy?: any })
:9731  registerObjectMetadata(objects: Array<{ name: string; fields?: Record<string, any>; tenancy?: any }>)
:9743  async initObjects(objects: Array<{ name: string; fields?: Record<string, any>; tenancy?: any }>)

And in each case a sibling on the same class already declares the key that its neighbour hides:

key the method that HIDES it the sibling that DECLARES it
indexes initObjects / registerObjectMetadata / ensureShardTable detectManagedDriftindexes?: any[] at :11009
lifecycle initObjects (:9800) rotateShardslifecycle?: any at :9185

⇒ the class is not "a type someone forgot to widen". It is one class disagreeing with itself about the shape of the same input, in both directions, twice.

⚠️ PR #16710 (#16570) fixes the indexes half at :9611:9612 only — it declares the key on the two public entry points and the shared helper and deletes that one cast. It leaves :9370, :9397, :9841, :9916 and the lifecycle read standing, correctly and deliberately: its dispatch fenced it to one key, and the filer of #16570 fenced that card away from adjacent work for the same reason.

Why this is a class and why the failure is silent

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 accepted or rejected depending only on where it is spelled:

await driver.initObjects([{ ...bare, lifecycle: { storage:  } }]);   // REJECTED — TS2353
const withLifecycle = { ...bare, lifecycle: { storage:  } };
await driver.initObjects([withLifecycle]);                            // ACCEPTED, and the read happens

⇒ the loud outcome (a compile error on a correct call) is the good 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:

  • dropping indexes ⇒ a declared UNIQUE is never synced, and nothing says so;
  • dropping lifecycle ⇒ the ADR-0057 rotation policy is never armed, and nothing says so.

Both are decided at authoring time by a type that contradicts the runtime. #16570's most valuable sentence generalises to the whole class: every existing caller happens to bind a variable first, which is why the package typechecks today — the workaround is load-bearing and nobody wrote it down.

📌 Evidence that the workaround has already been noticed and written down once, found by #16570's implementer and re-checkable: packages/drivers/driver-sql/src/sql-driver-11794-richtext-text-family.test.ts:324-326 says "Hoisted (not an inline literal) … indexes rides through initObjects beyond its narrow parameter type". A second escape shape exists at sql-driver-15479-shadow-plain-unique-duplicates.test.ts:128 (an as any on the argument). ⇒ callers have been silently routing around these signatures for at least two cards' worth of history.

What is being asked, not asserted

Two shapes were considered. ⛔ Neither is a grade and neither is a decision this seat is entitled to make:

#16570's implementer recommended B, and this seat finds the reasoning sound and worth quoting rather than paraphrasing:

the measured evidence is now identical three times over and the failure mode is the silent one, not the loud one: dropping lifecycle leaves ADR-0057 rotation unarmed exactly as dropping indexes leaves a UNIQUE unsynced. A gate is what stops the fourth; a fourth card does not.

⚠️ The counter-argument is on the record too and should be weighed, not dismissed: the branch is loud (TS2353), not silent, until an author drops the key. ⇒ the whole grade turns on how reachable "an author reads the signature and drops the key" is — and ⛔ no instance of that actually happening has been measured, so it should ⛔ not be counted as having occurred.

📌 Escalation trigger, written down: if anyone measures a real object that declared indexes or lifecycle and had it dropped — an unsynced UNIQUE, duplicate rows a schema forbids, or an unarmed rotation policy — this stops being a hygiene class and becomes a data-integrity defect.

Boundaries

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions