Skip to content

feat(driver-sql,driver-turso): update() publishes its honest type — the contract's Record[string, unknown] | null, not any (#14438) - #15280

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-14438-sql-driver-declared-null
Sep 4, 2026
Merged

os-warren merged 2 commits into
mainfrom
claude/issue-14438-sql-driver-declared-null

Conversation

@claude

@claude claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #14438

Generic type arguments are written in SQUARE brackets throughout this body (the body sanitizer eats the angle-bracket spelling): Promise[any] in prose means the angle-bracket form in code.

What

SqlDriver.update() was written out with an explicit Promise[any] while it has always answered a missing id with null. IDataDriver.update() declares Promise[Record[string, unknown] | null] (#13878, PR #14434) and an explicit any satisfies that structurally, so tsc said nothing, the published .d.ts of @objectstack/driver-sql read any, and no caller holding the class was asked to narrow. This PR:

  • narrows SqlDriver.update() — and its protected rotation-path producer rotatedUpdateById() — to the contract's type (packages/drivers/driver-sql/src/sql-driver.ts);
  • narrows TursoDriver.update()'s override the same way (packages/drivers/driver-turso/src/turso-driver.ts) — item 2 below is why this is not inherited;
  • adds a type-level pin inside each of the three packages' own tsc programs (IsAny = false, Equals the contract shape, plus a runtime miss/hit case): sql-driver-update-declared-null.test.ts, turso-driver-update-declared-null.test.ts, sqlite-wasm-update-declared-null.test.ts;
  • rewrites the six consumer sites the narrowing surfaced (all in driver-turso's own tests) with vitest's assert() — typed as an assertion signature, so it narrows; not a ! and not a cast;
  • changesets: @objectstack/driver-sql: minor and @objectstack/driver-turso: minor, both type-surface-only under ADR-0087 (gate-verified, see below).

No runtime behaviour changes.

Triage's three scope items

1. Census of Promise[any] doors in sql-driver.ts (measured on origin/main @ 97bcd99e)

Raw count: 9 return annotations. Classified against packages/spec/src/contracts/data-driver.ts (located by symbol):

method IDataDriver door? contract declares after this PR
update() yes Promise[Record[string, unknown] | null] narrowed
rotatedUpdateById() (protected producer of update()) no — internal — narrowed alongside
findOne() yes Promise[Record[string, unknown] | null] still any
create() yes Promise[Record[string, unknown]] still any
bulkCreate() yes Promise[Record[string, unknown][]] still any
execute() yes Promise[unknown] still any
explain() yes (optional) Promise[unknown] still any
analyzeQuery() no — helper behind explain() — any, not a door
aggregate() no — off-contract (#6212) — any, not a door

Driver doors: 6 were masked; 1 is un-masked here; 5 remain — filed as #15267 (unassigned) rather than widened into this PR, per triage's "report the count so the next card knows what is left". TursoDriver additionally overrides create() with its own Promise[any] (recorded in #15267). Frozen family (#5499): neither driver-memory nor driver-mongodb carries a Promise[any] door any more on this head — mongodb's update() / upsert() are the contract's since #14428, memory's since #14434; memory's remaining find / findOne / create are #14435. Reported, not touched.

Grep control: the annotation-shaped pattern (closing paren, colon, Promise[any]) counts 9 on the base file and 7 after the edit — the two removed annotations. A bare token count reads 8 because the doc comments added here mention the old spelling.

2. Do driver-turso / driver-sqlite-wasm re-declare the door in their emitted .d.ts?

Measured on each package's built dist/index.d.ts (post-change), counting update(object: string, id: string | number, …) declarations:

package declarations reading
@objectstack/driver-sql 1 — SqlDriver.update, now Promise[Record[string, unknown] | null] the door
@objectstack/driver-turso 2 — RemoteTransport.update (line 229) and TursoDriver.update (line 1464) re-declares — override async update(...) in turso-driver.ts carried its own Promise[any] ⇒ own fix, own pin, own changeset
@objectstack/driver-sqlite-wasm 0 inherits ⇒ pin only, no changeset

The card and the dispatch brief read both siblings as inheriting; turso does not. A driver-sql-only fix would have left @objectstack/driver-turso's published type masked.

3. Consumer-closure typecheck — the list, not a count

pnpm exec turbo run typecheck --filter='...@objectstack/driver-sql' — the prefix (downstream) direction: every workspace package that depends on driver-sql, with their builds — run on d54443031 (this branch's merged head): 111/111 tasks successful, 46/46 typecheck tasks exit 0 (turbo run summary .turbo/runs/3Ir5K17nOEV2v6ZafFW81pOx6XF.json; 40 re-measured, 6 cache hits). The same run on the pre-merge commit c039cee6e was 110/110 with 45/45 typechecks exit 0.

Sites the narrowing surfaced before the rewrite — all TS18047 "possibly 'null'", all in driver-turso's own tests, none in production code, none outside the three driver packages:

site what it did classification fix
turso-remote-autonumber-refusal.test.ts:370-371 — updated.case_number, updated.title read fields off update() on a seeded id with no null check the place the narrowing belongs (a test on the row arm) assert(updated !== null, …)
turso-update-missing-id.test.ts:245-248 — localHit.id / .title, remoteHit.id / .title expect(x).not.toBeNull() and then dereferenced — that assertion does not narrow the place the narrowing belongs (the parity positive control) assert(localHit !== null, …), assert(remoteHit !== null, …)

No real missing null check surfaced in production code: every production consumer reaches update() through IDataDriver, which has carried the null arm since #14434, and no package re-exports the concrete classes (grep for export * from / export { … } from the three driver packages: zero; positive control on @objectstack/core re-exports: fires).

Reverse verification — direction predicted before it ran

Prediction: with the pins present and the sources at the base annotation, each package's typecheck goes RED with exactly two TS2322 (IsAny = true, Equals = false); pnpm test is green either way (the type-level facts are consts vitest only compares).

Measured pre-change (pnpm --filter PKG typecheck, pins present, driver-sql built pre-change so the two dependents read the old .d.ts):

  • driver-sql: exit 2 — sql-driver-update-declared-null.test.ts(60,7) and (61,7) TS2322
  • driver-turso: exit 2 — turso-driver-update-declared-null.test.ts(52,7) and (53,7) TS2322
  • driver-sqlite-wasm: exit 2 — sqlite-wasm-update-declared-null.test.ts(47,7) and (48,7) TS2322

Post-change: all three typecheck exit 0; the three pin suites 3/3 each; the two rewritten turso suites 30/30 (vitest run).

Gates on d54443031

node scripts/pm/dispatch-gates.mjs with no path arguments (change set off the merge base) derived 43 runnable families; every one was run on the merged head with its exit captured before any pipe:

Thirteen further families are value-bearing ($MERGE_BASE, $RUNNER_TEMP, matrix shards) and are CI's. Deliberately not run locally: pnpm lint (repo-wide eslint is CI's) and driver-sql's full pnpm test (162 suites; the change is a type annotation, measured by tsc above — the targeted suites are the runtime half).


Generated by Claude Code

…ecord<string, unknown> | null`, not `any`

`SqlDriver.update()` was written out with an explicit `Promise<any>` while it
has always answered a missing id with `null`; `IDataDriver.update()` declares
`Promise<Record<string, unknown> | null>` and an explicit `any` satisfies that
structurally, so the published `.d.ts` read `any` and no caller holding the
class was asked to narrow. The annotation is now the contract's, on `update()`
and on its protected rotation-path producer `rotatedUpdateById()`.

`TursoDriver` overrides the door rather than inheriting it, with its own
explicit `Promise<any>`; its override is narrowed the same way and carries its
own changeset. `SqliteWasmDriver` inherits: its emitted `.d.ts` declares no
`update` member (measured), so it carries a pin and no changeset.

Type-level pins (`IsAny` = false, `Equals` the contract shape) live in each of
the three packages' own tsc programs; each was measured red (2 x TS2322)
against the pre-change annotation and green after.

The narrowing surfaced six sites in driver-turso's own tests that read fields
off an `update()` result after asserting `not.toBeNull()`; they now narrow
through vitest's `assert()` (an assertion signature), not a `!` or a cast.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-sql, @objectstack/driver-turso, touching 2 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
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 4dd5041bd0beba5a34458dd9b583121a2985409c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0b2ec1415f1eeb556ad3e3831aa82b591563da0e — the merge of head d54443031a5ba30d6a303b79e9aeb52cea2fb22e into base 4dd5041bd0beba5a34458dd9b583121a2985409c, 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 0b2ec1415f1eeb556ad3e3831aa82b591563da0e && git checkout 0b2ec1415f1eeb556ad3e3831aa82b591563da0e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4dd5041bd0beba5a34458dd9b583121a2985409c d54443031a5ba30d6a303b79e9aeb52cea2fb22e && git checkout -B drift-repro 4dd5041bd0beba5a34458dd9b583121a2985409c && git merge --no-ff d54443031a5ba30d6a303b79e9aeb52cea2fb22e

node scripts/docs-audit/affected-docs.mjs --json 4dd5041bd0beba5a34458dd9b583121a2985409c

⚠️ 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 4dd5041bd0beba5a34458dd9b583121a2985409c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator

⏸ Held in draft — the Clause-② gate is unmet because no review happened, ⛔ not because a review failed

domain:engine PM seat, session session_01ARYe3yQTQCUFm5qPYNgKaJ, R17, 2026-09-04T08:32Z. Writing which of the two states this is, because needs:contract-review cannot distinguish them and the difference decides what the next reader should do.

Measured. An isolated contract reviewer was dispatched for this PR at CONTRACT_REVIEW_TIER (model: fable, the tier constant claude-fable-5-1 at scripts/pm/dispatch-gates.mjs:8659). It died before producing a verdict:

HTTP 429 · rate_limit · "You've reached your Fable limit"
model sent to the API: claude-fable-5-1

A fable dev launched for a sibling card (#14666) died on the identical error minutes earlier. ⇒ The tier is exhausted, not merely slow.

⇒ No verdict exists on this PR. ⛔ Not a PASS, ⛔ not a FAIL, ⛔ and not something a subsequent green CI run converts into either.

Why it is not reviewed in-seat instead

This seat measured itself this fire: get_session returns session_context.model and last_served_model both claude-opus-5, against a required claude-fable-5-1. The rule is not negotiable for a Clause-② PR — ⛔ no in-seat review, and ⛔ 免复核不放行. So this PR stays draft, unenqueued, un-armed, until a reviewer at tier returns a verdict.

What is already established, so the eventual review starts from evidence rather than zero

These are the PM seat's own readings, taken before the reviewer was dispatched — ⛔ they are not a substitute for the tier review, and the reviewer should re-derive rather than adopt them:

  • Path face is clean: 9 files, all under .changeset/ and packages/drivers/** ⇒ not governed, so ordinary queue landing applies once the gate clears (Governed Surface Queue Guard green agrees).
  • The frozen family ([裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499) was respected: the diff touches neither driver-memory nor driver-mongodb; the census reported them and left them alone, which is what the ruling's standing rule requires.
  • The dev's own reverse verification is the strongest evidence on offer and is what the reviewer should attack first: three packages' typecheck measured RED with exactly two TS2322 each against the pre-change annotation, then green after. If that holds, the pins discriminate; if it is a restatement rather than a measurement, the whole delivery's evidentiary basis changes.
  • ⭐ The delivery falsified its own brief in a useful direction: the card and my dispatch both said TursoDriver inherits update(). It overrides it with its own Promise[any], and its emitted .d.ts re-declares the door — so a driver-sql-only fix would have left @objectstack/driver-turso's published type masked. That is worth confirming precisely because it expanded the PR beyond its claimed file surface.

For the next seat

⛔ Do not read a green CI as clearing this gate — CI does not run the contract review. ⛔ Do not strip needs:contract-review; it is correctly hung (it went on the card and the PR together, after the diff existed). The PR is complete and waiting on capacity, not on work.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Contract review PASS at CONTRACT_REVIEW_TIER — director seat (objectstack #12708, session_01LsEjuNMPitCHwEfYftZ1um), 2026-09-04. Verdict with the derived judgments and the semver reading is on the card: #14438 comment 5538229390. Tier fuse read before the review (get_session: claude-fable-5-1 served). Not governed (nine paths under .changeset/ and packages/drivers/**). needs:contract-review removed here and on the card (read-modify-write, other labels untouched), PR marked ready, squash auto-merge armed; this seat follows it to MERGED and #14438 closes on Fixes.


Generated by Claude Code

@os-warren
os-warren added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 2200f8e Sep 4, 2026
42 checks passed
@os-warren
os-warren deleted the claude/issue-14438-sql-driver-declared-null branch September 4, 2026 09:49
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…pes it already published (objectstack-ai#17255)

* fix(engine): ObjectRepository declares the findOne/update shapes it already publishes

`IScopedObjectRepository.findOne` / `.update` declare `Record<string, any> | null`
and `Record<string, any> | number | null`, and `IDataEngine` — the call each of
these forwards to — declares the same. `ObjectRepository` sat between two narrow
declarations and re-widened the value back to `Promise<any>` on the way out, which
`implements IScopedObjectRepository` accepts (a wider return always satisfies a
narrower one) while every call site reaching a repository through the CLASS kept
reading `any`, `ObjectQL.createContext(…).object(n).findOne(…)` included.

Census: one consumer, `engine-filter-alias.test.ts`, which read `.status` off a
value that can be null. Repaired with the file's own `not.toBeNull()` / `!` idiom.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU

* test(engine): pin that a class-typed `object(name)` hands back a declared repository

Compiler-driven probes (`ts.createProgram`, the idiom
`packages/spec/src/contracts/scoped-context.test.ts` uses) over the exported
class doors — `ScopedContext`, `ObjectQL.createContext`, `sudo()` — asserting the
diagnostic NAMES the declared shape, so neither a bare "it errored" nor an `any`
that erased the type can satisfy it. Anti-vacuity: the legal spelling must
compile clean and no probe may report TS2307.

Probes go through the CLASS, not `HookContext`: `HookContext.api` was narrowed to
`IScopedContext` by objectstack-ai#5945, so a `(ctx: HookContext)` probe is green on both sides
of this fix and pins nothing. Measured, and recorded in the file header.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU

* chore(engine): changeset for the declared repository return shapes

Graded `patch`: nothing is widened and no symbol is added. The contract already
published these shapes; the implementation is coming back to a declaration it had
already published. Checked against the recorded WHICH LEVEL ruling of 2026-09-04
(decision batch objectstack-ai#35, on objectstack-ai#15294), whose `minor` trigger is additive widening.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU

* chore(engine): re-grade the repository return declarations to minor + BREAKING

Landed precedent PR objectstack-ai#15280 measured: `SqlDriver.update()` and the `TursoDriver.update()`
override moved off an explicit `Promise<any>` onto the shape `IDataDriver` already
declared -- no new exported symbol, `packages/spec` untouched -- and both changesets
shipped `minor` with a **BREAKING** banner. That is this change's shape exactly, so the
earlier `patch` reasoning ("the contract already published it, so nothing moved") is the
very fact pattern that precedent grades `minor`: the emitted `.d.ts` read `any`, so no
caller holding the class was ever asked to narrow.

The ADR-0087 disposition is `no-migration-prescription`, as sibling PR objectstack-ai#16783 used for the
same family. `type-surface-only` is semantically the right category but its predicate 4
cannot address either narrowed symbol; the marker records that measurement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…h their declared types, not any (objectstack-ai#15267) (objectstack-ai#17258)

* feat(driver-sql,driver-turso): the remaining IDataDriver doors publish their declared types, not any (objectstack-ai#15267)

`SqlDriver` published an explicit `Promise<any>` over five doors the contract
had already declared narrower — `findOne`, `create`, `bulkCreate`, `execute`
and `explain` — so the emitted `.d.ts` erased every one of them. `TursoDriver`
overrides four of the same five with its own `Promise<any>`, which no
driver-sql fix reaches.

Each annotation is replaced with the type
`packages/spec/src/contracts/data-driver.ts` already declares for that door,
and each is pinned at the type level inside its own package's tsc program.
No runtime behaviour changes.

Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-authored-by: Claude <noreply@anthropic.com>

* fix(driver-sql,driver-turso): narrow the consumer sites the declared doors surfaced (objectstack-ai#15267)

The narrowing surfaced 68 un-narrowed dereferences, every one in the two
packages' own tests: 64 reads of a `findOne()` result with no `null` check
(`expect(...).not.toBeNull()` does not narrow), and four reads through the
`unknown` that `bulkCreate()`, `explain()` and `findOne()` now resolve to.

Each positive control asserts the row arm with vitest's `assert()` — a
narrowing assertion, not a `!` and not a cast — and each `unknown` read names
what it reads. The not-found controls keep their `toBeNull()` and gain nothing.

Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-authored-by: Claude <noreply@anthropic.com>

* fix(driver-sqlite-wasm): narrow the inherited-door consumer sites (objectstack-ai#15267)

`SqliteWasmDriver` overrides none of the five doors and inherits every one, so
the driver-sql narrowing reaches its callers through that package's `.d.ts`.
Eight positive controls assert the row arm; one `create()` read names the
string it collects. No source change in this package.

Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-authored-by: Claude <noreply@anthropic.com>

* fix(runtime): read the record number as a string in the autonumber parity probe (objectstack-ai#15267)

`SqlDriver.create()` declares the contract's `Record<string, unknown>` now, so
the cross-side parity probe's `rec_no` read is `unknown` where it was reached
through an `any`. It converts to the string the probe compares.

Caught by this package's `check:test-typecheck` gate, not by `tsc --noEmit` —
the file is in the checked test zone and the ledger does not cover it.

Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-authored-by: Claude <noreply@anthropic.com>

* chore(changeset): declare the driver-sql / driver-turso door narrowing (objectstack-ai#15267)

Both graded `minor` and marked `type-surface-only` under ADR-0087, matching
the landed precedent PR objectstack-ai#15280 for `update()` on the same classes.

Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-authored-by: Claude <noreply@anthropic.com>

* chore(changeset): name only predicate-4-verifiable symbols in the ADR-0087 markers (objectstack-ai#15267)

`isErasedType` counts `unknown` as erased by design (pinned TSO-U6), so the
`execute` / `explain` doors — which move onto the contract's own `unknown` —
cannot serve as predicate-4 evidence. The markers name the three doors that
move onto concrete shapes and state the rest in prose; the disposition is
identical for every door.

Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
… record or null, not any (objectstack-ai#17836)

Fixes objectstack-ai#16786

Generic arguments are spelled with SQUARE brackets throughout this body
— `Promise[T]`, `Record[string, any]` — so that no fragment of it is
shaped like an HTML tag. The source carries the real spelling.

## What changed

One member of one interface, plus its docblock, a pin and a changeset:

    packages/spec/src/contracts/scoped-context.ts
    - updateById(id: string | number, data: any): Promise[any];
+ updateById(id: string | number, data: any): Promise[Record[string,
any] | null];

PR objectstack-ai#17255 landed this card's `objectql` half and left this member named
as the remainder; triage re-routed the card to `domain:spec` on
2026-09-10. This is that remainder and nothing else.

## Why — the measurement, ⛔ not an enumeration

⚠️ Stated plainly so the authority is not over-read: **ruling A on
objectstack-ai#16231 did not enumerate this line.** What the ruling settled is the
RULE — objectstack-ai#15823's `find()` narrowing extends to the sibling doors — and
its enumeration named `scoped-context.ts:148` / `:164`. `updateById` is
a door of that family the enumeration missed. It is narrowed here
because the **measurement** says the declaration was wider than every
implementation and wider than the door it forwards to.

Located by SYMBOL, re-derived at this branch's base `b59b74909c` — ⛔
every `path:line` on the card is its writer's reading at their own head
and two of them are days old.

### The declaration, and its siblings after PR objectstack-ai#16783

| member | declared on `origin/main` before this PR |
|---|---|
| `find` | `Promise[any[]]` |
| `findOne` | `Promise[Record[string, any] \| null]` — narrowed under
ruling A |
| `count` | `Promise[number]` |
| `insert` | `Promise[any]` |
| `update` | `Promise[Record[string, any] \| number \| null]` — narrowed
under ruling A |
| **`updateById`** | **`Promise[any]`** ← this PR |

⚠️ Note for a reader expecting a fourth sibling: this interface declares
**no `delete`** member at all. `scoped-context.test.ts` pins its ABSENCE
(`rejects-delete`). Ruling A's `delete` anchor is on `IDataEngine`, a
different interface.

### What the implementations actually return

⭐ The whole defect class is that TypeScript's `implements` accepts a
**wider** declared return, so a class satisfies a narrowed interface
while publishing `any` to every caller. Shown at this member rather than
asserted:

- `packages/objectql/src/engine.ts`, `class ObjectRepository implements
IScopedObjectRepository` — `async updateById(id, data): Promise[any]`.
Its own docblock, written by PR objectstack-ai#17255, says why: *"Its `Promise[any]`
is what `IScopedObjectRepository.updateById` itself declares, so the
class matches its contract and there is no drift to repair on this side;
that member is `packages/spec`'s to narrow and stays open on objectstack-ai#16786."* ⇒
the class declared `any` to MATCH this member, not independently of it.
Narrowing here is the half that was left open, not a second one.
- Its body forwards to `this.engine.update(name, { ...data, id }, {
where: { id }, context })` — the `IDataEngine.update` door, which
declares `Promise[Record[string, any] | number | null]`.
- That door's by-id branch calls `driver.update(object, id, data,
options)`, and `IDataDriver.update` declares exactly
`Promise[Record[string, unknown] | null]`.
- The other implementation of the surrounding contract,
`UnscopedHookApi` (`packages/objectql/src/hook-run-as.ts`), never
returns a repository at all — `object()` throws.
- The two in-repo object literals typed at this contract
(`scoped-context.test.ts`, `hook.test.ts`) both answer a record.

⇒ `Promise[any]` was wider than every implementation AND wider than the
door it forwards to. The bar the dispatch set is met.

### Why the `number` limb is NOT declared here

`update` carries a count limb because it has two dispatch exits;
`updateById` has one. The implementation binds a truthy scalar payload
id AND a pure-id `where`, and never declares `multi`, so
`resolveEngineUpdateDispatch` (`@objectstack/metadata-core`) answers
`by-id` for every call this signature admits:
`unhonouredByIdPredicateKeys({ id })` filters out the key `id` and
returns `[]`, and the payload id equals `where.id` by construction, so
neither refusal branch is reachable. A falsy id (`0`, `''`) is a REFUSAL
rather than a narrower answer — it identifies no row, the dispatch
rejects and the call throws.

⇒ the honest answer is the by-id exit: the record, or `null`.

## Consumer census

Who reaches `updateById` **through the interface-typed door**, repo-wide
(`grep` over all `.ts` / `.tsx` / `.md` / `.mdx`, excluding
`node_modules` and `dist`):

- `packages/spec/src/contracts/scoped-context.test.ts` —
`legal-updateById` compile probe (discards the result) and a literal
`IScopedObjectRepository` returning `({ id, ...data })`. Both satisfy
the narrowed shape unchanged.
- `packages/spec/src/data/hook.test.ts` — a fake returning `({ id,
...data })`. Same.
- `packages/lint/**` — twelve sites, all of which parse the SPELLING
`ctx.api.object(…).updateById(…)` out of authored hook/action bodies as
text. None reads a return type.
- `content/docs/api/error-handling-server.mdx`,
`content/docs/automation/hook-bodies.mdx` — documented call sites; both
discard the result.
- `packages/objectql/src/engine-update-duplicate-record.test.ts` —
reaches it on the CLASS, whose own declaration this PR does not touch.

⇒ **no consumer relies on the `any`**, and nothing in the repository
needed repair. `pnpm --filter @objectstack/spec test` and `typecheck`
are green with no consumer edits in the diff. ⚠️ Out-of-repo TypeScript
consumers DO break — that is the changeset's BREAKING banner, not an
absence of breakage.

## The published surface — measured before and after, with controls

The emitted `.d.ts` is what consumers resolve, ⛔ not the source. Built
FIRST in both readings (`npm pack --dry-run --json` before a build reads
`dist` as empty).

    BEFORE  packages/spec/dist/analytics.zod-BuSXQiW9.d.ts:2202
              updateById(id: string | number, data: any): Promise[any];
    AFTER   packages/spec/dist/analytics.zod-CR3DXUJM.d.ts:2230
            packages/spec/dist/analytics.zod-BBKc62s9.d.mts:2230
updateById(id: string | number, data: any): Promise[Record[string, any]
| null];

The wide spelling now reads **0** anywhere under `dist/`. Controls on
both readings: `IScopedObjectRepository` reads 1 file (lit), a
fabricated symbol reads 0. The chunk is re-exported by the published
`./contracts` entry (`dist/contracts/index.d.ts` re-exports `i as
IScopedObjectRepository` from it).

⭐ A second, unplanned control fell out of the ordering: the green pin
leg ran while `dist/` still held the OLD declaration and still asserted
the NEW shape — direct proof the probe harness resolves `src/`, not
`dist/`, so the ablation below needs no rebuild leg.

## The pin, and its ablation

`packages/spec/src/contracts/scoped-context.test.ts` gains a probe-based
pin driven through `ts.createProgram` — the harness that file already
owns. ⛔ Not `@ts-expect-error`: a directive is satisfied by ANY error on
the next line, and the sibling pin's TS2578 mechanism is only read by
`check:test-typecheck`, a gate OUTSIDE this suite. These fail in vitest
itself.

- `rejects-unnarrowed-updateById` — assigning the answer into a
`Record[string, any]` slot must report **TS2322**, and the message must
NAME `Record[string, any] | null`. Naming the shape is deliberate: the
file's own header records a phantom check that survived a revert because
a code-only assertion was satisfied by a different type. It also asserts
the message does NOT contain `number | null`, so a copy-paste of the
`update` sibling's wider shape is caught.
- `legal-updateById-null-checked` — the correct null-checked spelling
must compile CLEAN. Anti-vacuity: a harness that resolved nothing would
report an empty string for both probes.
- `harness-self-test` — the existing "can this harness report at all"
control.
- a runtime leg proving the contract is implementable at the declared
shape in both directions (record, and the `null` a by-id miss resolves).

**GREEN:** `pnpm --filter @objectstack/spec exec vitest run
--maxWorkers=2 src/contracts/scoped-context.test.ts` → `Test Files 1
passed (1) · Tests 8 passed (8)`.

**ABLATION**, run from the committed state (`d3344d52a7`), mutating the
declaration back to `Promise[any]`:

    HEAD blob                67f2697
on-disk, before mutation 67f2697 (equal
— tree was at HEAD)
occurrence counts BEFORE narrow=1 wide=0 -> AFTER narrow=0 wide=1
on-disk, mutated e81dba2c0d84ecd78542dfe10b5dc7dd89328ec9 (differs — the
mutation landed)

    RED  Tests 1 failed | 7 passed (8)
AssertionError: an unnarrowed record slot must be refused: expected ''
to contain 'TS2322'

⭐ The failure signature is the point: the probe reports **NOTHING**, and
an absent diagnostic IS the `any` reading. The other seven legs stay
green — only the leg that measures the narrowing moves, which is the
anti-vacuity shape.

RESTORE git checkout HEAD -- ABSOLUTE_PATH -> on-disk
67f2697 (= HEAD blob)
             git diff HEAD: 0 lines · git status --porcelain: clean

The script carried `trap RESTORE_FN EXIT INT TERM` with an absolute path
seeded from `git rev-parse --show-toplevel`, treated an empty `git
hash-object` as FAILURE, and proved restore by blob comparison — ⛔ never
by an exit code.

## Changeset — `minor` + BREAKING, and the grade was measured, ⛔ not
inherited

`.changeset/16786-scoped-updatebyid-answer.md`, `"@objectstack/spec":
minor`, body opening **BREAKING**.

Does the changed text reach a published `dist`? Measured after a build,
with controls:

- POSITIVE — `dist/analytics.zod-*.d.ts` and `dist/contracts/index.d.ts`
are both in `npm pack --dry-run --json` (2012 files).
- NEGATIVE — `src/contracts/scoped-context.ts` is NOT: `files[]` ships
`src/**/*.zod.ts` only, and this file is not a `.zod.ts`. Zero
`src/contracts/*` paths ship.

⇒ the declaration reaches consumers through `dist` and only through
`dist`. Not a `skip-changeset` case.

Weighed against the precedent the dispatch named — PR objectstack-ai#15280
(`SqlDriver.update()` / the `TursoDriver.update()` override), PR objectstack-ai#14434
(`@objectstack/driver-memory`), and this card's own PR objectstack-ai#17255,
**re-graded from `patch` to `minor` mid-round**:

⇒ **`minor` + BREAKING, and this case is STRICTLY stronger than the
precedent.** objectstack-ai#17255's `patch` argument was *"the interface already
declared the narrow shape; the class merely re-widened on the way out,
so repairing it violates nothing new"* — and even that was refuted. Here
that argument is not available at all: the interface IS what moves, on a
published exported type. There is no reading on which this is the
`patch` rung.

ADR-0087 disposition: `not-required (no-migration-prescription)`. ⚠️
`type-surface-only` — the category built for exactly this class — was
measured and is **unavailable**, ⛔ not skipped: its predicate 2
(`no-spec-diff`) is false by construction, because the narrowed symbol
LIVES in `packages/spec`; its predicate 3 (`no-metadata-surface-diff`)
is false for the same file, `packages/spec/src/contracts/**` being an
ADR-0087 shape surface by the gate's own classifier. Two of four
predicates cannot hold for ANY edit to this symbol. The **BREAKING**
token is carried rather than dropped — that erosion is what objectstack-ai#13080 was
filed about.

⚠️ This is a second, different reason the category is unreachable from
the one PR objectstack-ai#17255 recorded (there: predicate 4 cannot address a class
member whose name repeats in its file; filed as objectstack-ai#17279). Recorded so the
two are not conflated.

## Clause ②

`Clause-②: no`, as the claiming seat declared it. This round's own
measurement agrees and does not re-declare: the change adds no schema
key, no closed-set member, no published export and no registry entry —
`pnpm --filter @objectstack/spec run check:api-surface` is green with no
snapshot movement, because `api-surface/` records that an export EXISTS,
never what it resolves to. It narrows one existing member's declared
return.

## Verification

- **Pin**: green 8/8; ablation red 1/7 with the predicted signature;
restore proven by blob hash. Above.
- **Package**: `pnpm --filter @objectstack/spec test` and `pnpm --filter
@objectstack/spec typecheck` — see the report on the card for the run
figures.
- **Dependency closure** (`pnpm --filter '@objectstack/spec^...'
build`): EMPTY — `packages/spec` has no workspace dependencies (`No
projects matched the filters`). Nothing to build; declared rather than
silently skipped.
- **Derived gate families** — `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack`, 81 commands, every exit
code captured BEFORE any pipe. **77 exit 0.** Four exit **3 =
PREREQUISITE NOT MET = NOT MEASURED**, ⛔ never counted green:
- `check:dual-build-cjs-loads` and `check:type-check-debt` need a
FULL-REPO build closure (87 packages / 31 dependencies unbuilt). CI owns
that build; PR objectstack-ai#17255 reported the same two the same way.
- `check:lean-entry-closure` and `@objectstack/lint
check:doc-formula-expressions` need smaller closures, built and re-run
in this round — see the report.
- ⛔ No `#`+digits and no closing stem appears in any commit message or
trailer; swept per stem with a lit control that hit
`fixes`/`closes`/`part of`/`refs` and four `#`-digit tokens.

## Out of scope, ⛔ not folded in

- `IScopedObjectRepository.find` still declares `Promise[any[]]` and
`insert` still declares `Promise[any]`. ⛔ Deliberately untouched:
neither is implicated by this measurement. `find`'s width is ARGUED in
the file's own module header (the corpus reads rows off it), and
`insert`'s engine door `IDataEngine.insert` itself declares
`Promise[any]`, so there is nothing narrower to come back to. Narrowing
either would be a NEW declaration with an unmeasured census — the class
of change ruling A paid for.
- ⚠️ A correction to this round's dispatch order, recorded rather than
smoothed over: it stated the `.changeset/issue-N-slug` spelling has
**zero** precedents in this tree. Measured at `b59b74909c`: there are
**four** (`issue-17400-…`, `issue-17461-…`, `issue-17574-…`,
`issue-17595-…`). The numeric-prefix form the order prescribes is
nonetheless the prevailing one (50 of 227) and is what this PR uses, so
the instruction's conclusion stands and only its count was wrong.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… and ADR that decided them (stage 4 of objectstack-ai#20595) (objectstack-ai#21357)

Part of objectstack-ai#20595
Clause-②: no

## What changed

Stage 4 of the `domain:engine` lane of the dead-citation sweep:
`packages/drivers/driver-sql/**`, comment and docblock prose only, per
the claim (`5946343111`). Stages 1 to 3 landed as `a7d9768ec`,
`d150c3039` and `4bf4e7e70`; objectstack-ai#20595 stays open for the next stage.

Every comment or docblock site in the package that cited a tracker
number answering 404 is rewritten in ruling C+D's form C (record
`5749154545` on objectstack-ai#19123): the ADR when one records the decision,
otherwise the commit in this repository's history that made it. That is
**291 sites on 287 lines in 54 files, covering 39 numbers**, plus one
dead comment-id citation on two lines:

- **126 census sites** (122 lines, 6 files under `src/`): the whole
`allocated-but-absent` population of the gate's own census in this
package at the base, the slash-joined `objectstack-ai#7737/objectstack-ai#10629` and
`objectstack-ai#14079/objectstack-ai#15683/objectstack-ai#17343` from the post-landing census (`5923084795`, now
at `sql-driver.ts:12236` and `:16167`) included. That census's third
driver-sql site, the URL-spelled `objectstack-ai#17590` at `:3933`, left the file in
`58a77dbde2` before this base;
- **165 test-comment sites** (165 lines, 48 test files), which the
census defers. They carry 37 numbers: 24 the census itself reads as dead
in this package's `src`, 5 more it reads as dead elsewhere in the
repository, and 8 it never judges (they stand only in test files), which
the board and a single read each settle;
- **the dead comment id `5448627494`** (on objectstack-ai#11152, which is live), on
two test-comment lines.

**Anchors: 38 numbers by commit, 1 by ADR (ADR-0104's 2026-09-05
addendum), 0 by words alone; the comment id by commit.** 16 numbers
reuse the anchor another lane or stage already measured for them, 23
were measured here (one of them split across two commits), and the
comment id was measured here. 39 distinct shas.

Only comments changed. Every file keeps its line count (291 lines out,
291 in, plus the changeset), so no line citation into any of them moves.
No code token moves (the guard below). **No citation number is added**:
on every changed line, the numbers on the new text are a subset of those
on the old, and the diff-scoped gate judged the 10 citations left on
changed lines: all 10 resolve.

**A `patch` changeset**: 57 of the 122 rewritten non-test lines are in
the published `dist` (the `.d.ts` keeps JSDoc on exported members, and
esbuild keeps some comments in the JS), and `dist` is not byte-identical
with the base text (see Changeset).

## Census: `driver-sql`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count is its `allocated-but-absent` findings under
`packages/drivers/driver-sql/`.

| reading | tree | board | whole-repo `allocated-but-absent` | sites |
lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `222ecc27f`, run 05:52:18Z to 05:55:43Z | enumerated,
192 pages, frontier objectstack-ai#21345, 19,166 records (newest number read before
and after the run: objectstack-ai#21345) | 456 | **126** | 122 | 6 | 26 |
| after | `286316ebd`, run 06:17:50Z to 06:21:10Z | enumerated, 192
pages, frontier objectstack-ai#21352, 19,173 records (newest before: objectstack-ai#21351, after:
objectstack-ai#21352) | 330 | **0** | 0 | 0 | 0 |

The whole-repo drop is 126, and the two finding sets differ by exactly
the 126 rows of this package, removed; none was added. `resolves`
(34,789), `resolves-as-pull-request` (2,378) and `cross-repo-unjudged`
(1,155) did not move. The card's 128 was taken at `f11b5f20a2` with the
older extractor; the base here reads 126. The head's later commits are a
merge of `main` that touches no file under `packages/drivers/driver-sql`
and the changeset (outside the census surface).

**Supplementary instrument, the whole package.** The census reads
neither test files nor strings nor files outside `src`. A second reading
runs the gate's own exported `extractCitations` (whole-file and
comment-prose projections) over every tracked file in the package (238
`.ts`, `package.json`, `tsconfig.json`, `README.md`, `CHANGELOG.md`,
`LICENSE`), and classifies each citation with the gate's
`classifyCitation` against one board enumerated by the gate's
`enumerateBoard` (192 pages, frontier objectstack-ai#21346, 19,167 records, 05:56:41Z
to 05:59:55Z). Every one of the 39 numbers in the population was then
read on its own over the issues endpoint: **all 39 answer 404**, and the
lit controls `objectstack-ai#5286` and `objectstack-ai#12624` answer 200.

| reading | citations | dead | src comment | test comment | test string
| changelog |
|---|---|---|---|---|---|---|
| before, `222ecc27f` | 4,920 | **411** | 126 | 165 | 51 | 69 |
| after, working tree at `286316ebd` | 4,629 | **120** | 0 | 0 | 51 | 69
|

The drop of 291 citations is exactly the rewritten sites, and the live
counts did not move (src comment: 1,352 resolve, 24 as pull requests;
test comment: 1,545 and 69). A third, raw reading (every `#` followed by
2 to 6 digits, whatever surrounds it) counts 4,939 before and 4,654
after: a drop of 285, which is the 291 less the 6 URL-spelled sites that
carry no `#`.

**Comment ids.** Nine distinct comment-id citations stand on 19 sites in
this package (`CHANGELOG.md` aside). Each was read over the
issue-comments endpoint: `5448627494` answers 404 (it was a comment on
objectstack-ai#11152, which itself answers 200), and the other eight answer 200
(`5302931807`, `5404884704`, `5556979386`, `5618311630`, `5861435168`,
`5865693155`, `5881556735`, `5934879010`; control `5946343111`, 200). So
the one dead id is in the population. `5642107998`, the ruling record
`e04a0aff2`'s message names for objectstack-ai#17590, also answers 404; it is not
cited in this package.

## Per-number table

`src` counts census sites, `test` counts test-comment sites. Every sha
below matches exactly one commit (`git rev-parse --disambiguate`, count
1) and is an ancestor of the merge base `04f0cc499` (`git merge-base
--is-ancestor`, exit 0 for all 39 shas; the clone is not shallow). The
`+` lines introduce exactly these 39 nine-hex spans and no other. The
message or the diff of each one names the number it replaces: 23 in the
message and the diff, 11 in the diff alone (`objectstack-ai#11065`, `objectstack-ai#11374` for
`d0e3a885b`, `objectstack-ai#12978`, `objectstack-ai#12999`, `objectstack-ai#13015`, `objectstack-ai#13324`, `objectstack-ai#14438`, `objectstack-ai#14628`,
`objectstack-ai#16649`, `objectstack-ai#16711`, `objectstack-ai#17586`), 3 only in the subject's squash suffix
(`objectstack-ai#6076`, `objectstack-ai#14434`, `objectstack-ai#17876`, where the dead number was that pull
request's own and the commit is its squash), and one exception,
`objectstack-ai#10629`, explained under Wordings to check. `f6fa22ce1`'s diff names
the comment id three times. Where a sentence credits a ruling, a
measurement, a review or a note to the number, the anchor's own message
or diff carries it (checked per site; the ones that needed a reworded
sentence are listed below). `source` says whether another lane or stage
already used this anchor for this number (`reused`) or it was measured
here (`measured`).

| number | src | test | anchor | kind | source | what it decided |
|---|---|---|---|---|---|---|
| `objectstack-ai#6075` | 1 | 3 | `d367f03d6` | commit | measured | five drivers'
query parameters follow `DriverQuery` |
| `objectstack-ai#6076` | 0 | 1 | `6513c1749` | commit | measured | `IDataDriver`'s
query parameter becomes `DriverQuery` (that pull request's squash) |
| `objectstack-ai#8778` | 0 | 2 | `7901b2dd2` | commit | reused | stamp-only
`tenancy.organizationField` |
| `objectstack-ai#8823` | 0 | 3 | `4dfa369a9` | commit | reused | drop the caller
value MySQL inlines in its duplicate-entry diagnostic |
| `objectstack-ai#9542` | 5 | 6 | `8bbf45947` | commit | measured | bound the
metadata-lock wait on boot schema-sync's MySQL widening ALTER too,
keeping boot's swallow |
| `objectstack-ai#10165` | 0 | 1 | `801296050` | commit | reused | lifecycle
`ttl.onlyWhen` row filter with the canonical null predicate |
| `objectstack-ai#10629` | 1 | 0 | `199ec4712` | commit | reused | bind federated
objects whatever the boot order (stage 3's anchor; the live objectstack-ai#7737
carries the citation) |
| `objectstack-ai#10836` | 0 | 2 | `7ab286e44` | commit | measured | live pg + mysql
legs for the `ttl.onlyWhen` `$null` suite |
| `objectstack-ai#11065` | 0 | 1 | `20950404c` | commit | reused | count a boolean
aggregand as 1/0 in avg and sum |
| `objectstack-ai#11067` | 10 | 10 | `479fba50d` | commit | measured | stamp
`updated_at` when the driver never ran DDL |
| `objectstack-ai#11374` | 16 | 7 | `d0e3a885b` + `107bb4ba4` | commit | measured |
emit `varchar(maxLength)` for a text field a declared index keys on /
carry an over-long UNIQUE index on a hash-shadow column, the route the
2026-08-24 ruling chose |
| `objectstack-ai#12380` | 9 | 13 | `4045b954d` | commit | reused | make the SQLite
`Field.json` codec injective, one encoding across all three dialects |
| `objectstack-ai#12978` | 0 | 2 | `e4902d2b9` | commit | reused | declare sourced
`maxLength` on the keyed text columns of the `sys_notification_*`
objects |
| `objectstack-ai#12998` | 19 | 7 | `df1c75c4b` | commit | measured | hash-shadow
UNIQUE indexes carry the NULL-safe organization key part (ADR-0120 D3) |
| `objectstack-ai#12999` | 3 | 1 | `ebcc34e89` | commit | measured | name the real
remedy when a bounded field sits over a stale TEXT column |
| `objectstack-ai#13015` | 17 | 6 | `cd1348802` | commit | measured | a shadow-carried
UNIQUE is not index drift, and its remedy dropped the constraint |
| `objectstack-ai#13279` | 1 | 1 | `6a180e42d` | commit | reused | fail loud when a
permission-store read fails; it moved the classifier pins into
`driver-error-classification.test.ts` |
| `objectstack-ai#13324` | 1 | 4 | `4cda78c9b` | commit | reused | require a
missing-table error to name the table that was read |
| `objectstack-ai#14434` | 0 | 1 | `93940d492` | commit | measured | declare the
not-found arm on `IDataDriver.update()` (that pull request's squash) |
| `objectstack-ai#14438` | 1 | 3 | `2200f8ec8` | commit | measured | `update()`
publishes the contract's record-or-null, not `any` (the squash of PR
objectstack-ai#15280) |
| `objectstack-ai#14628` | 1 | 1 | `6392b9c2b` | commit | measured | budget the 8th
live-cell hook, reached through `rawDriver()` |
| `objectstack-ai#14902` | 7 | 9 | `61821e54c` | commit | reused | a plain unique
index over duplicate rows is loud and non-fatal (the squash of PR
objectstack-ai#15477) |
| `objectstack-ai#15041` | 2 | 5 | ADR-0104, 2026-09-05 addendum | ADR | reused | the
media family's column holds the bare `sys_file` id; the step's abort
requirement, its SQL sketch, and the generator as the side that does not
move |
| `objectstack-ai#16570` | 0 | 5 | `b72226f48` | commit | measured | declare the
`indexes` key `initObjects` / `registerObjectMetadata` already read |
| `objectstack-ai#16609` | 1 | 5 | `78bc4ad58` | commit | measured |
`findWithWindowFunctions` presents its rows like every other read door,
and the alias-collision ruling |
| `objectstack-ai#16619` | 2 | 1 | `45cfa1b88` | commit | measured | canonical ISO-Z
read presentation (that pull request's squash; its message records the
contract review) |
| `objectstack-ai#16649` | 1 | 0 | `613bfbd3d` | commit | reused | register the
remaining `door: 'none'` codes, re-registering
`MONGODB_MULTI_TENANT_UNSUPPORTED` |
| `objectstack-ai#16657` | 0 | 1 | `5a95b0e93` | commit | reused | read the dialect
text out of `cause` for operator-facing records |
| `objectstack-ai#16711` | 2 | 5 | `7862fb711` | commit | reused | object-definition
parameters declare the keys they are read for |
| `objectstack-ai#16729` | 1 | 1 | `0f38ab084` | commit | reused | an explicit tenancy
opt-out survives a partial `syncSchema` re-registration |
| `objectstack-ai#17343` | 3 | 8 | `82cb69fed` | commit | measured | a `multiple:
true` boolean column keeps its `$contains` membership filter |
| `objectstack-ai#17586` | 4 | 2 | `d46deba19` | commit | measured | keep multi-valued
boolean/toggle columns out of the read-coercion registry |
| `objectstack-ai#17590` | 5 | 20 | `e04a0aff2` | commit | reused | compile
`$contains` on a JSON column as a per-dialect MEMBERSHIP test; its
message records the director's 2026-09-12 Ruling A |
| `objectstack-ai#17639` | 5 | 10 | `7c2c5aedd` | commit | measured | envelope the
`distinct()` backend fault |
| `objectstack-ai#17690` | 4 | 8 | `be5c60291` | commit | measured | eight more
`IDataDriver` doors publish their declared return type |
| `objectstack-ai#17857` | 4 | 2 | `9ccc4179e` | commit | measured | attribute an
unresolvable `distinct()` column to the clause the caller named |
| `objectstack-ai#17876` | 0 | 2 | `be5c60291` | commit | measured | that pull
request's squash, which installed the `ContainsAny` detector |
| `objectstack-ai#17879` | 0 | 4 | `eb9334915` | commit | measured | measure the
`ContainsAny` phantom-leg sweep across eight door pins |
| `objectstack-ai#17970` | 0 | 2 | `47e6601c5` | commit | measured | collapse
`ContainsAny`'s distributivity so union-shaped doors assert |
| comment `5448627494` | 0 | 2 | `f6fa22ce1` | commit | measured | the
2026-08-28 maintainer ruling: min/max over booleans answer numbers on
every face, superseding objectstack-ai#11249's false/true |

## Wordings to check

Most rewrites swap a tag in place (`[#N]` to `[commit SHA]`, `(#N)` to
`(commit SHA)`, `#N's X` to `commit SHA's X`, `PR #N` to its squash
commit, a URL `@see` to `@see commit SHA`), the form the landed stages
use. These say more than the tag:

- **`objectstack-ai#10629`, one site** (`sql-driver.ts:12236`): 「the same ruling
objectstack-ai#7737/objectstack-ai#10629 already made for FEDERATED objects」 became 「the same ruling
objectstack-ai#7737 already made for FEDERATED objects (commit 199ec47)」, stage 3's
judged form (contract review `5943182373`): `199ec4712` is objectstack-ai#7737's fix
and its message states the ruling; it names objectstack-ai#7737, not objectstack-ai#10629.
- **`objectstack-ai#11374`, split by subject.** 20 sites describe the keyed-text rule
(`varchar(maxLength)` for a field a declared index keys on, the shard
path, `boundedObject()`, the named refusal): `d0e3a885b`, the driver
commit whose diff names objectstack-ai#11374 fourteen times and introduced
`explainUnkeyableTextColumn` and `boundedObject()`. 3 sites credit 「the
maintainer's 2026-08-24 ruling」 that chose the hash route: `107bb4ba4`,
whose message moves 「objectstack-ai#11374's refusal pins」 and records 「the ruling
deliberately replaced」 and 「the prefix constraint the ruling rejected」.
Its message does not carry the date, so those sites keep their date and
read 「landed as commit 107bb4b」 / 「recorded in commit 107bb4b」
(stage 1's `objectstack-ai#9741` precedent). Other lanes anchored objectstack-ai#11374 to their own
packages' bound declarations (`e4902d2b9`, `f64668d3c`, `3954fb7df`), a
different subject; the driver's own rule is `d0e3a885b`.
- **`objectstack-ai#15041`, seven sites**: 「the objectstack-ai#15041 addendum」 / 「the ruling on
objectstack-ai#15041」 became 「the ADR-0104 2026-09-05 addendum」 / 「the ruling in
ADR-0104's 2026-09-05 addendum」, the spelling stage 3 and the cli lane
used. The addendum names objectstack-ai#15041 as its provenance and carries each claim
the sites make: the abort-on-the-first-non-JSON-string requirement, the
`USING (col #>> '{}')` sketch, and 「the driver is the side that moves」
for the generator. `sql-driver-15989-file-family-bare-id.test.ts:7`
keeps the maintainer's verbatim quote untouched (it carries no `#`).
- **The comment id**
(`sql-driver-11635-boolean-aggregand-answers.test.ts:9`,
`sql-driver-11782-boolean-row-read-presentation.test.ts:36`-`:37`):
「applied in its comment 5448627494」 became 「landed as commit f6fa22c」.
`f6fa22ce1` applies 「the 2026-08-28 maintainer ruling (option A,
superseding objectstack-ai#11249's false/true)」 to driver-sql's result presentation,
and its diff names the comment id. objectstack-ai#11152 stays.
- **`objectstack-ai#17590` where it was written while the card was open** (the `17639`
and `17857` suites, `sql-driver.ts:11060`,
`sql-driver-17586-…test.ts:101`): those sentences park the json-column
`distinct()` question 「with objectstack-ai#17590」. `e04a0aff2` closed that card with
Ruling A on `$contains` only, so a bare swap would credit it with a
`distinct()` verdict it never made. They now read 「the card commit
e04a0af closed, which owned the sibling divergence」, 「if a card after
commit e04a0af rules that a json column should ANSWER a distinct read」
(both retirement clauses), 「the ruling commit e04a0af records cannot
move it」, 「that question belonged to the card commit e04a0af closed」,
「Nothing here touches the card commit e04a0af closed」 and 「the
divergence commit e04a0af ruled on the filter side」.
`sql-driver-17639-distinct-fault-envelope.test.ts:206` is the **one
changed line that carries no number**: its verb (「owns」 to 「owned」) had
the number on `:205` as its subject.
- **Where the number named the defect, not the change**: 「## Not objectstack-ai#11067」
became 「## Not the defect commit 479fba5 fixed」; 「objectstack-ai#13015 was the price
of the split」 became 「The defect commit cd13488 fixed was the price of
the split」; 「objectstack-ai#17343: a `multiple: true` BOOLEAN column lost …」 became
「The defect commit 82cb69f fixed: …」; 「The defect that produced the
table (objectstack-ai#12380)」 became 「(fixed in commit 4045b95)」 and 「let objectstack-ai#12380
survive」 became 「let the defect commit 4045b95 fixed survive」; 「family
as objectstack-ai#11067 / objectstack-ai#11176 / objectstack-ai#11223」 became 「family as objectstack-ai#11176 / objectstack-ai#11223 / the one
commit 479fba5 fixed」.
- **`pre-` / `post-` spellings**: 「a pre-objectstack-ai#12998 shadow」 became 「a shadow
from before commit df1c75c」 (and the same shape for `objectstack-ai#12380` and
`objectstack-ai#17590`); 「post-objectstack-ai#12998」 became 「since commit df1c75c」; 「The
pre-objectstack-ai#12998 shadow over RAW columns」 became 「The older (before commit
df1c75c) shadow over RAW columns」.
- **Reviews and reports**: 「the contract review of PR objectstack-ai#16619」 became
「the contract review recorded in commit 45cfa1b」 (and 「measured in the
contract review commit 45cfa1b records」, 「commit 45cfa1b's contract
review's finding」); `45cfa1b88` is that pull request's squash, and its
message records FINDING-1 to FINDING-3 and the hand-made TEXT-affinity
measurement. 「the objectstack-ai#17879 report」 became 「commit eb93349's message」
(two sites); that message records that both repair candidates were
measured.
- **`objectstack-ai#16649`** (`dialect-emission-refusal.ts:66`): 「a removal objectstack-ai#16649
reversed」 became 「a removal that commit 613bfbd reversed」. `613bfbd3d`
re-registered `MONGODB_MULTI_TENANT_UNSUPPORTED` 「under the ruling」; the
runtime lane's `44c917a47` is objectstack-ai#16649's vocabulary-gate half, a different
subject.
- **`objectstack-ai#14628`** (`live-dialect-matrix.testkit.ts:451`,
`sql-driver-datetime-mysql-storage.test.ts:57`): `6392b9c2b` budgeted
the eighth live-cell hook and wrote the `(objectstack-ai#14628)` line itself; the
seven before it were `13b520069` (PR objectstack-ai#14629), which cites objectstack-ai#14213, the
number kept beside it.
- **Squash rewrites**: 「after objectstack-ai#6076 merged」 became 「after commit
6513c17 landed」; 「objectstack-ai#13878 / PR objectstack-ai#14434」 became 「objectstack-ai#13878 / commit
93940d4」; 「`ContainsAny` (objectstack-ai#17876)」 became 「(commit be5c602)」, the
squash that installed it; 「objectstack-ai#14438 (PR objectstack-ai#15280)」 became 「Commit 2200f8e
(PR objectstack-ai#15280)」, the live pull-request number kept beside its squash as a
convenience.
- **Slash pairs**: 「[objectstack-ai#9542/objectstack-ai#9609]」 became 「[commit 8bbf459, objectstack-ai#9609]」;
「objectstack-ai#5181/objectstack-ai#6075」 became 「objectstack-ai#5181 and commit d367f03」; 「objectstack-ai#11374/objectstack-ai#11627 exist
to end」 became 「objectstack-ai#11627 and commit d0e3a88 exist to end」; 「the
objectstack-ai#11627/objectstack-ai#12998 suites」 became 「the objectstack-ai#11627 and commit df1c75c suites」;
「[objectstack-ai#14079/objectstack-ai#15683/objectstack-ai#17343]」 became 「[objectstack-ai#14079/objectstack-ai#15683, commit 82cb69f]」;
「(objectstack-ai#11374, objectstack-ai#12999)」 became 「(commits d0e3a88 and ebcc34e)」.
- **Other single rewrites**: 「(objectstack-ai#16711 验收口径 item 4)」 became 「(验收口径 item 4
of the card commit 7862fb7 closed)」, whose message records the TS2353
negative control; 「the defect objectstack-ai#8287 removed and objectstack-ai#8778 must not
reintroduce」 became 「… and commit 7901b2d's stamp-only key must not
reintroduce」; 「the gap objectstack-ai#17639 left FILED」 became 「the gap commit
7c2c5ae left FILED」 (`9ccc4179e`'s message: 「the attribution arm
objectstack-ai#17639 deliberately left filed」); 「See the ruling recorded on objectstack-ai#12380.」
became 「See the decision recorded in commit 4045b95's message.」 (that
message records the posture, 「refuses to guess at the two that the
pre-fix encoding made ambiguous」, and calls it no ruling).
- **Capitalisation**: where the number opened a sentence, the new text
opens with 「Commit」.
- No line was reflowed, so some are longer than their block's wrap
(`eslint.config.mjs` declares no line-length rule, and a reflow would
move neighbouring lines and every line citation into the file).

## Sites left

- **In comments (src, test, `vitest.config.ts`): none.**
- **String literals: 51 test-string sites, 24 numbers, 26 files**
(`describe` and `it` titles, assertion arguments): `objectstack-ai#11374` 5, `objectstack-ai#12380`
4, `objectstack-ai#17590` 4, `objectstack-ai#17639` 4, `objectstack-ai#12998` 3, `objectstack-ai#13015` 3, `objectstack-ai#16609` 3, `objectstack-ai#17343`
3, `objectstack-ai#17586` 3, `objectstack-ai#17857` 3, `objectstack-ai#11067` 2, `objectstack-ai#14902` 2, and 12 more once
each. Every one of the 24 is in this stage's population, so the table
above holds an anchor for each. Non-test strings carry none. Strings are
outside this stage's surface.
- **Outside `src`:** the release-owned `CHANGELOG.md` names dead numbers
on 69 sites (31 numbers); left. `package.json`, `tsconfig.json`,
`vitest.config.ts`, `README.md` and `LICENSE` cite no dead number.

## Mechanical guard: no code token moves

The guard (stages 2 and 3's) compares base `222ecc27f` against the
working tree over all 55 touched files, with TypeScript 6.0.3:

- **Reading 1**: the parser's leaf nodes, from a `forEachChild` walk.
Comments are trivia there, and JSDoc is never visited. A leaf that is
not itself a token (an empty block) is re-scanned with trivia skipped.
- **Reading 2**: the full token stream in parser context, from a
`getChildren` walk, JSDoc nodes skipped. String, template and numeric
literals are compared in full on both readings.

Results:

- Real run at the head: 165,798 base tokens, **0 files with a token
change** (exit 0).
- Comment control (「which IS its value」 to 「which IS ITS value」,
`sql-driver.ts`): 0 files changed (exit 0).
- Positive control, an identifier (`hashShadowColumnFor` to
`hashShadowColumnForX`, `schema-drift.ts`): DIFFER on both readings
(exit 1).
- Positive control, a string literal (`'storage'` to `'storageX'` in
`FieldKeyClass`, `builtin-column-collision.ts`): DIFFER on both readings
(exit 1).
- Positive control, a numeric literal (`MEDIA_ID_MOVE_WIDTH = 2048` to
`2049`, `media-column-move.ts`): DIFFER on both readings (exit 1).

Each mutation went through `scripts/ablation-replace.mjs` (wrap mode,
anchor hit 1 to 0, replacement 0 to 1) under a shell trap that restores
by absolute path from `HEAD`. Each restore was proven equal to its
`HEAD` blob (`cd55a4ca44f3`, `72e45a83968f`, `86ebb324c1b6`,
`fe0ce2c4d5bd`), with `git diff HEAD` empty and a clean tree afterwards.

## Changeset: `patch` (`dist` measured)

`files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the package is
not private. The whole workspace was built first (`turbo run build` over
`./packages/*` and `./packages/*/*`, 71 of 71 tasks, VERDICT
command-exit 0). Then the package's own `build` (tsup plus
`check-dts-emitted`) ran three times in one script under the shared
verify lock (VERDICT command-exit 0):

- **Leg 1**, at `02d2a034a`: 6 `dist` files hashed. Of the 122 rewritten
non-test lines, 57 appear verbatim in `dist`: 52 from `sql-driver.ts`, 3
from `schema-drift.ts`, 1 each from `media-column-move.ts` and
`dialect-emission-refusal.ts`; in `index.d.ts` / `index.d.mts` (51) and
`index.js` / `index.mjs` (52).
- **Leg 2**, the base text put back in the 6 non-test files (6 of 6
proven equal to their base blob): 4 of the 6 files differ from leg 1
(`index.d.ts`, `index.d.mts`, `index.js`, `index.mjs`).
- **Leg 3**, after the proven restore (6 of 6 equal to their `HEAD`
blob, `git diff HEAD` empty): all 6 files are byte-identical to leg 1,
so the build is deterministic and the difference is the rewrite.

So the rewrite ships, and
`.changeset/20595-driver-sql-provenance-anchors.md` declares a `patch`
for `@objectstack/driver-sql`, comment text only, with the claim's
`Clause-②: no` line.

## Gates (head `fad95aff7`)

- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `fad95aff7` (56 paths against
merge base `04f0cc499`) derived 63 commands. All 63 ran, each exit code
captured before any pipe: 63 exit 0. `--ran` reports 「63 derived, 63
run, 0 NOT-MEASURED, 0 UNRUN」 (a derived zero) and exits 0. The roster
families the derivation flags under a touched directory also ran, each
exit 0: `node scripts/check-changeset-fixed.mjs`, `pnpm
check:authz-resolver`, `pnpm check:error-code-casing`, `pnpm
check:filter-alias-parity`.
- **Against the newer `main`:** `main` moved six commits after the
merge, none touching a file here. A probe tree at `origin/main`
`f9bcd08be` with this diff applied derived 64 commands, the one addition
being `node scripts/check-dts-emitted.mjs --self-test` (that script is
unchanged across the range). It ran, exit 0, and `--ran` on the probe
reports 「64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN」.
- **Named in the dispatch:** `node scripts/check-issue-citations.mjs`
exits 0 (「every citation this change adds resolves」, 10 judged across 6
files); `pnpm check:issue-citations` exits 0 (self-test, 173 cases, 9
batteries); `pnpm check:doc-authoring` exits 0 (the sibling-package
prose-id baseline holds, no growth); `pnpm check:nul-bytes` exits 0
(9,641 files, no raw control bytes).
- **Tests and typecheck, under the verify lock, at `fad95aff7`:** `pnpm
--filter @objectstack/driver-sql test`: 215 test files pass and 11 skip
(226); 3,594 tests pass and 202 skip. The 11 skipped files are the
live-dialect suites (PostgreSQL and MySQL cells), declared un-run
locally; CI's `Temporal Conformance (live PG + MySQL)` job runs them.
`pnpm --filter @objectstack/driver-sql typecheck` exits 0; `tsc
--listFiles` puts all 226 tracked test files and all 55 changed files in
its program.
- **Lint, as a proven narrowing:** eslint with inline config disabled,
over the 55 touched `.ts` files plus `dist/index.js` as the control: 56
results, 0 errors and 1 warning, the control's ignore notice; none of
the 55 is reported ignored. `eslint.config.mjs` never enables type-aware
linting (its lines 327-328 say so), so a comment edit cannot move the
verdict on an untouched file. The repo-wide `pnpm lint` is CI's run.

## Acceptance notes

- **Base.** The branch was cut at `222ecc27f` and merged with `main`
once, at `04f0cc499`, before the gates (merge `02d2a034a`, no conflict,
no file under `packages/drivers/driver-sql`, and neither
`check-issue-citations.mjs` nor `dispatch-gates.mjs`). Tests, typecheck
and gates ran on the head after it. The net diff against `main` is the
55 rewritten files (+291/−291) and the changeset.
- **Two sentences now state something that is no longer true, and this
stage changes only their citation.**
`sql-driver-12998-shadow-null-safe-key.test.ts:266` says `initObjects`'
parameter type does not declare `indexes`; `b72226f48` declared it, and
the citation now reads 「(the gap commit b72226f closed)」.
`sql-driver.ts:18142` says 「objectstack-ai#11374's remaining half may still reshape
the text side」; it was written on 2026-08-24 (`c49afd0886`), a day
before `107bb4ba4` settled that half, and now reads 「the half commit
d0e3a88 left open」. Comment accuracy, outside a citation sweep.
- **The token guard's first reading-1 run was wrong, and was replaced.**
It reported `sql-driver.ts` as DIFFER on reading 1 alone: an empty
`catch { }` block has no child nodes, so its leaf text carried the
comment inside it (`// Pre-objectstack-ai#12380 row: …`). Reading 2 agreed with no
change throughout. Reading 1 now re-scans such a leaf with trivia
skipped; the real run and all four controls above are from the corrected
guard.
- **Wording only:** no line without a number was changed, except
`sql-driver-17639-distinct-fault-envelope.test.ts:206`, listed above.

---
_Generated by [Claude
Code](https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ts that decided them (stage 10 of objectstack-ai#20595) (objectstack-ai#21546)

Part of objectstack-ai#20595
Clause-②: no

## What changed

Stage 10 of the `domain:engine` lane of the dead-citation sweep:
`packages/drivers/driver-turso/**`, comment and docblock prose only, per
the claim (`5965451347`). Stages 1 to 9 landed as `a7d9768ec`,
`d150c3039`, `4bf4e7e70`, `13a24ece2`, `db0cf2231`, `85986144c`,
`48fa7a381`, `c205b6c35` and `c98a72d69`. objectstack-ai#20595 stays open: the other
half of this lane is the packages this stage does not touch
(`drivers/driver-mongodb` 9, `formula` 4, `metadata-fs` 2 on the census
after this stage, 15 in all), plus the test-string sites the card
carries for a widened stage.

Every comment or docblock site in the package that cited a tracker
number answering 404 is rewritten in ruling C+D's form C (record
`5749154545` on objectstack-ai#19123): the ADR when one records the decision,
otherwise the commit in this repository's history that made it. That is
**50 sites on 49 lines in 12 files, covering 13 numbers**:

- **14 census sites** (13 lines, 2 files under `src/`): the whole
`allocated-but-absent` population of the gate's own census in this
package at the base. `turso-driver.ts:2131` carries two of them (objectstack-ai#6076
and objectstack-ai#6075);
- **36 test-comment sites** (36 lines, 10 test files), which the census
defers. They carry 12 numbers: 7 the census itself reads as dead in this
package's `src` (objectstack-ai#6075, objectstack-ai#12380, objectstack-ai#14428, objectstack-ai#14438, objectstack-ai#16570, objectstack-ai#16711, objectstack-ai#17690),
and 5 that stand only in test files here (objectstack-ai#14434, objectstack-ai#17590, objectstack-ai#17876,
objectstack-ai#17879, objectstack-ai#17970), which the board and a single read each settle;
- **no site outside the census glob**: `vitest.config.ts`,
`tsconfig.json`, `package.json`, `README.md` and `LICENSE` cite no dead
number.

No comment-id citation is dead here: the package's two comment ids,
ruling records `5865693155` and `5861435168` (`remote-transport.ts:38`,
`:4152` to `:4153`, `turso-20444-empty-operator.test.ts:8`), both answer
200 (see Census).

**Anchors: 13 numbers by commit, 0 by ADR, 0 by repository qualifier; 12
distinct shas** (objectstack-ai#17690 and objectstack-ai#17876 share `be5c60291`: the squash of PR
objectstack-ai#17876, whose message opens 「Part of」 objectstack-ai#17690). 12 numbers reuse the
anchor stage 4 (`driver-sql`) measured for them; `ca3fd4b1a` (objectstack-ai#14428) is
measured here.

Only comments changed. Every file keeps its line count (49 lines out, 49
in, plus the changeset), so no line citation into any of them moves. No
code token moves (the guard below). All 98 changed lines open with a
comment marker. **No citation number is added**: on every changed line
the numbers on the new text are a subset of those on the old (the only
numbers on `+` lines are objectstack-ai#5181, objectstack-ai#6210 twice, objectstack-ai#6212, objectstack-ai#13878 and objectstack-ai#20987,
each already on its line and each answering 200).

**A `patch` changeset**: all 13 rewritten non-test lines are in the
published `dist` (the `.d.ts` keeps JSDoc on exported members, and
esbuild keeps these comments in the JavaScript), and `dist` is not
byte-identical with the base text (see Changeset).

## H0: the package and its size

The gate's own `node scripts/check-issue-citations.mjs --census --json`
at base `31d2255f5` (the before run below), `allocated-but-absent` per
remaining `domain:engine` package:

| package | before | after this stage |
|---|---|---|
| `drivers/driver-turso` | **14** | **0** |
| `drivers/driver-mongodb` | 9 | 9 |
| `formula` | 4 | 4 |
| `metadata-fs` | 2 | 2 |
| `metadata-core`, `core`, `metadata-protocol`, `objectql`, `metadata`,
`drivers/driver-sql`, `drivers/driver-memory`,
`drivers/driver-sqlite-wasm`, `plugins/plugin-pinyin-search`,
`platform-objects` | 0 each | 0 each |

The lane total goes 29 to 15. `driver-turso` is the largest remaining
package and reads 14, as at stage 9's head census (`99f2cfdf0`), so the
stage went ahead.

## Census: `driver-turso`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count is its `allocated-but-absent` findings under
`packages/drivers/driver-turso/`.

| reading | tree | board | whole-repo `allocated-but-absent` | sites |
lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `31d2255f5`, run 04:20:17Z to 04:23:32Z | enumerated,
194 pages, frontier objectstack-ai#21537, 19,358 records | 144 | **14** | 13 | 2 | 8 |
| after | `841405134`, run 04:32:36Z to 04:35:47Z | enumerated, 194
pages, frontier objectstack-ai#21539, 19,360 records (newest number read before and
after the run: objectstack-ai#21539) | 130 | **0** | 0 | 0 | 0 |

The whole-repo drop is 14, and the two finding sets differ by exactly
the 14 rows of this package, removed; none was added. `resolves`
(35,495), `resolves-as-pull-request` (2,388) and `cross-repo-unjudged`
(1,247) did not move.

The head's later commits are the changeset and one merge of `main`. The
census was run a third time at the head `e89bd10cd` (04:53:10Z to
04:56:16Z, 194 pages, frontier objectstack-ai#21544, 19,365 records, newest objectstack-ai#21543
before and objectstack-ai#21544 after): whole-repo 130, `driver-turso` 0. Against the
after run, 5 rows moved line (all in `packages/spec`, shifted by the
merged `main` commit `48eb9c193`), and the finding multiset with line
numbers set aside is identical. Its `resolves` reads 35,509, 14 more
than above, from the merged `main` commits outside this package.

**Supplementary instrument, the whole package.** The census reads
neither test files nor strings nor files outside `src`. A second reading
runs the gate's own exported `extractCitations` (whole-file and
comment-prose projections) over every tracked file in the package (102)
and classifies each citation with the gate's `classifyCitation` against
one board enumerated by the gate's `enumerateBoard` (194 pages, frontier
objectstack-ai#21537, 19,358 records, read 04:24:21Z to 04:27:29Z), the same board for
both readings. Every one of the 13 numbers was then read on its own over
the issues endpoint (04:29:27Z): **all 13 answer 404**; the numbers that
stay on changed lines (objectstack-ai#5181, objectstack-ai#6210, objectstack-ai#6212, objectstack-ai#13878, objectstack-ai#20987) and the lit
controls objectstack-ai#15280, objectstack-ai#12585 and objectstack-ai#6402 answer 200.

| reading | citations | dead | src comment | test comment | other files
| test string | changelog |
|---|---|---|---|---|---|---|---|
| before, `31d2255f5` | 1,921 | **83** | 14 | 36 | 0 | 6 | 27 |
| after, `841405134` | 1,871 | **33** | 0 | 0 | 0 | 6 | 27 |

The citation count drops by 50, the 50 rewritten sites; no respelling
stays a citation. The live counts did not move (src comment: 522
resolve, 80 as pull requests, 8 cross-repo; test comment: 609, 75 and
6). A third, raw reading (every `#` followed by 2 to 6 digits, whatever
surrounds it, `CHANGELOG.md` aside) counts 1,558 before and 1,509 after,
and the URL-spelled issue links go 7 to 6: also a drop of 50.

**Comment ids.** Every ten-digit run under
`packages/drivers/driver-turso` (its `CHANGELOG.md` aside) was read:
five lines. `5861435168` (the record of ruling B on objectstack-ai#20311) and
`5865693155` (the record of ruling A on objectstack-ai#20399) answer 200 (the control
`5965451347`, the claim, answers 200 too).
`remote-canonical-backfill.ts:357` is an epoch-seconds example, not a
citation.

## Per-number table

`src` counts census sites, `test` the test-comment sites. Every sha
matches exactly one commit (`git rev-parse --disambiguate`, count 1), is
an ancestor of the base `31d2255f5` and of `origin/main` (`git
merge-base --is-ancestor`, exit 0 for all 12 on both; the clone is not
shallow), and resolves over the commits endpoint. The `+` lines carry
exactly these 12 nine-hex spans as new ones. Each commit names the
number it replaces, in its message, its diff or both: 7 in the message
and the diff (objectstack-ai#6075, objectstack-ai#12380, objectstack-ai#16570, objectstack-ai#17590, objectstack-ai#17690, objectstack-ai#17879, objectstack-ai#17970), 3
in the diff alone (objectstack-ai#14428 for `ca3fd4b1a`, objectstack-ai#14438 for `2200f8ec8`,
objectstack-ai#16711 for `7862fb711`), and 3 only in the subject's squash suffix
(objectstack-ai#6076 for `6513c1749`, objectstack-ai#14434 for `93940d492`, objectstack-ai#17876 for `be5c60291`),
where the dead number was that pull request's own and the commit is its
squash. `git blame` at the base puts 24 of the 49 changed lines on their
anchor; the other 25 were written by a commit that cites the number as
an earlier decision (`242eb0ac1`, `9f4a6d55f`, `9bfbacbf8` and
`6bd3231f6` citing objectstack-ai#12380; `3cbcedb62` and `e35c40a52` citing objectstack-ai#14438;
`fa2d3b737` citing objectstack-ai#6075 and objectstack-ai#6076; `5ba2ec3ca` citing objectstack-ai#17690;
`862f12c0b` citing objectstack-ai#17590; `2200f8ec8` citing objectstack-ai#14428; `ca3fd4b1a` citing
objectstack-ai#14434; `7862fb711` citing objectstack-ai#16570; `eb9334915` citing objectstack-ai#17876), and in
each case the anchor is the commit that made the change the sentence
credits to the number. `source` says whether stage 4 already used this
anchor for this number (`reused`) or it was measured here (`measured`).

| number | src | test | anchor | kind | source | what it decided |
|---|---|---|---|---|---|---|
| `objectstack-ai#6075` | 1 | 1 | `d367f03d6` | commit | reused (stage 4, stage 5) |
five drivers' `query` parameters follow `DriverQuery`; its turso diff
narrows `count`'s `query` and leaves `options?: any`, the half-narrowed
state both sites describe. It is the squash of PR objectstack-ai#6210, the live number
kept beside it |
| `objectstack-ai#6076` | 1 | 0 | `6513c1749` | commit | reused (stage 4) |
`IDataDriver`'s `query` parameter becomes `DriverQuery` (that pull
request's squash) |
| `objectstack-ai#12380` | 2 | 6 | `4045b954d` | commit | reused (stage 4) | make the
SQLite `Field.json` codec injective, one encoding across all three
dialects: the local half's codec every site credits |
| `objectstack-ai#14428` | 2 | 3 | `ca3fd4b1a` | commit | measured | `update()` on a
missing id answers `null` on Turso's remote face; it wrote the
`RemoteTransport.update()` docblock and the
`turso-update-missing-id.test.ts` header the sites carry, and declared
that door's record-or-null return |
| `objectstack-ai#14434` | 0 | 1 | `93940d492` | commit | reused (stage 4, stage 5) |
declare the not-found arm on `IDataDriver.update()` (that pull request's
squash) |
| `objectstack-ai#14438` | 2 | 5 | `2200f8ec8` | commit | reused (stage 4) |
`update()` publishes the contract's record-or-null, not `any`, on
`SqlDriver` and on `TursoDriver`'s override (the squash of PR objectstack-ai#15280) |
| `objectstack-ai#16570` | 1 | 1 | `b72226f48` | commit | reused (stage 4) | declare
the `indexes` key `initObjects` / `registerObjectMetadata` already read
|
| `objectstack-ai#16711` | 1 | 3 | `7862fb711` | commit | reused (stage 4, stage 3) |
object-definition parameters declare the keys they are read for, plus
the gate that sees subclass overrides; it wrote
`TursoDriver.initObjects`'s docblock and
`turso-driver-16711-init-objects-param.test.ts` |
| `objectstack-ai#17590` | 0 | 1 | `e04a0aff2` | commit | reused (stage 4, stage 5,
stage 8) | compile `$contains` on a JSON column as a per-dialect
MEMBERSHIP test |
| `objectstack-ai#17690` | 4 | 4 | `be5c60291` | commit | reused (stage 4) | eight
more `IDataDriver` doors publish their declared return type,
`TursoDriver`'s overrides among them; its message records the
transaction door left un-narrowed because each of the two repairs is
「neither of which is an annotation swap」, with the 「+14 further consumer
sites」 measurement, the sentence `turso-driver.ts:3549` cites |
| `objectstack-ai#17876` | 0 | 3 | `be5c60291` | commit | reused (stage 4) | that pull
request's squash, which installed the `ContainsAny` detector |
| `objectstack-ai#17879` | 0 | 6 | `eb9334915` | commit | reused (stage 4) | measure
the `ContainsAny` phantom-leg sweep across eight door pins; its message
records that both repair candidates were measured |
| `objectstack-ai#17970` | 0 | 2 | `47e6601c5` | commit | reused (stage 4) | collapse
`ContainsAny`'s distributivity so union-shaped doors assert |

No ADR or ruling record names any of the 13 numbers as the place their
decision is recorded; ADR-0104 names objectstack-ai#12380 in passing, and `4045b954d`
is the change these sites describe.

## Wordings to check

Most rewrites swap a tag in place (`[#N]` to `[commit SHA]`, `(#N)` to
`(commit SHA)`, `with #N` to `with commit SHA`, stage 1's form). These
say more than the tag:

- **Squash rewrites, stage 4's spellings**: 「objectstack-ai#5181 (PR objectstack-ai#6076), objectstack-ai#6075 (PR
objectstack-ai#6210)」 became 「objectstack-ai#5181 (commit 6513c17), commit d367f03 (PR objectstack-ai#6210)」
(`turso-driver.ts:2131`): the dead pull-request number gives way to its
squash, and the dead card number to the commit that landed its fix with
the live pull-request number kept beside it as a convenience link, stage
4's 「Commit 2200f8e (PR objectstack-ai#15280)」 form. 「objectstack-ai#6075 (PR objectstack-ai#6210) narrowed」
became 「commit d367f03 (PR objectstack-ai#6210) narrowed」
(`turso-driver-options-door.test.ts:21`). 「Since objectstack-ai#13878 (PR objectstack-ai#14434)」
became 「Since objectstack-ai#13878 (commit 93940d4)」
(`turso-update-missing-id.test.ts:20`). 「`ContainsAny` (objectstack-ai#17876)」 became
「`ContainsAny` (commit be5c602)」 on three test lines.
- **Reports**: 「the two measured repairs are in the objectstack-ai#17879 report」
became 「the two measured repairs are in commit eb93349's message」
(three test files), stage 4's spelling; that message records that both
candidates were measured.
- **`pre-` spellings**: 「restoring the pre-objectstack-ai#12380 SQLite `json` branch」
became 「restoring the SQLite `json` branch from before commit 4045b95」
(`remote-transport.ts:2612`), and 「The pre-objectstack-ai#12380 form of the string
'bare'」 became 「The form of the string 'bare' from before commit
4045b95」 (`turso-local-json-backfill-depth-limit.test.ts:72`), stage
4's 「from before commit」 form.
- **A URL-spelled citation**: 「@see
objectstack-ai#12380 (the
injective local codec)」 became 「@see commit 4045b95 (the injective
local codec)」 (`turso-json-column-type-asymmetry.test.ts:84`), stage 4's
「@see commit e04a0af (the membership construct)」 form. The three
`@see` links beside it (objectstack-ai#12738, objectstack-ai#12586, objectstack-ai#11535) resolve and stay.
- **The acceptance-criteria reference**: 「(objectstack-ai#16711 验收口径 item 4)」 became
「(验收口径 item 4 of the card commit 7862fb7 closed)」
(`turso-driver-16711-init-objects-param.test.ts:105`), stage 4's wording
for the same sentence in `driver-sql`'s twin test, verbatim. Neither the
card nor PR objectstack-ai#16816 resolves any more, so 「closed」 rests on stage 4's
reading and on `7862fb711` itself: its diff names objectstack-ai#16711 32 times and
creates this test file, and its message records the TS2353 negative
control this paragraph introduces.
- **Sentence and bullet starts**: where the number opened a sentence, a
bullet or a header, the new text opens with 「Commit」: 「Commit
4045b95's codec still runs」
(`turso-json-column-type-asymmetry.test.ts:259`), 「- Commit b72226f's
`indexes` fix」 (`turso-driver-16711-init-objects-param.test.ts:17`), 「//
Commit 2200f8e — 」 (`turso-driver-update-declared-null.test.ts:3`) and
「⛔ Commit 7862fb7 — this parameter type must declare every key」
(`turso-driver.ts:3689`). 「* commit 4045b95 had to defeat」
(`turso-json-column-type-asymmetry.test.ts:46`) stays lower-case: it
continues 「the measured … exposure that」 from the line above.
- **A tag over a paragraph that says 「this card」**:
`turso-driver-doors-declared-types.test.ts:117` now opens 「[commit
be5c602]」, and its line 119 still says 「every door on this card had
regressed to」. That line carries no number, so it was not touched; it
now reads as the card behind the commit in the tag (`be5c60291`'s
message opens 「Part of」 that card).
- **The commit that wrote the file**:
`turso-driver-update-declared-null.test.ts:3` and `:11` cite
`2200f8ec8`, the commit that created that test file. Its subject is
「feat(driver-sql,driver-turso): `update()` publishes the contract's …
not `any`」, so it is both the override fix the header names and the
`SqlDriver.update()` narrowing line 11 credits.
- No line was reflowed, so some are longer than their block's wrap
(`eslint.config.mjs` declares no line-length rule, and a reflow would
move neighbouring lines and every line citation into the file).

## Sites left

- **In comments (src, test, outside the glob): none.**
- **String literals: 6 test-string sites, 4 numbers, 4 files**: the
`describe` titles at `turso-driver-16711-init-objects-param.test.ts:67`
(objectstack-ai#16711), `turso-driver-update-declared-null.test.ts:85` (objectstack-ai#14438) and
`turso-update-missing-id.test.ts:141` and `:223` (objectstack-ai#14428), and two
assertion-message strings at
`turso-json-column-type-asymmetry.test.ts:266` and `:291` (objectstack-ai#12380). All
four numbers are in this stage's table. Strings are outside this stage's
surface; non-test strings cite none.
- **Outside `src`:** the release-owned `CHANGELOG.md` names dead numbers
on 27 sites (11 numbers); left.

## Mechanical guard: no code token moves

The guard (stages 2 to 9's, copied verbatim from stage 9) compares base
`31d2255f5` against the tree over all 12 touched files, with TypeScript
6.0.3:

- **Reading 1**: the parser's leaf nodes, from a `forEachChild` walk.
Comments are trivia there, and JSDoc is never visited. A leaf that is
not itself a token is re-scanned with trivia skipped.
- **Reading 2**: the full token stream in parser context, from a
`getChildren` walk, JSDoc nodes skipped. String, template and numeric
literals are compared in full on both readings.

Results, at `841405134` (the later commits touch none of the 12 files):

- Real run: 38,590 base tokens, **0 files with a token change** (exit
0).
- Comment control (「Keep it that way」 to 「KEEP it that way」,
`turso-driver.ts`): 0 files changed (exit 0).
- Positive control, an identifier (`export class RemoteTransport` to
`XRemoteTransport`, `remote-transport.ts`): DIFFER on both readings
(exit 1).
- Positive control, a string literal (the assertion message 「… for an
existing id」 to 「… for an existing iD」,
`turso-update-missing-id.test.ts`): DIFFER on both readings (exit 1).
- Positive control, a template literal (the index name 「uniq_ … _v」 to
「uniq_ … _w」, `turso-driver-16711-init-objects-param.test.ts`): DIFFER
on both readings (exit 1).
- Positive control, a numeric literal (`deepArray(1001)` to `1002`,
`turso-local-json-backfill-depth-limit.test.ts`): DIFFER on both
readings (exit 1).

Each mutation went through `scripts/ablation-replace.mjs` (wrap mode,
anchor hit 1 to 0, blob changed) under a shell trap that restores by
absolute path from `HEAD`. Each restore was proven equal to its `HEAD`
blob (`cc15138aba39`, `8deefdb0598b`, `e36d370cb3ed`, `f9142478172e`,
`7343e92b5c7d`), with `git diff HEAD` empty and a clean tree afterwards.

## Changeset: `patch` (`dist` measured)

`files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the package is
not private. In one script under the shared verify lock (VERDICT
command-exit 0, held 104s), at `841405134`: the dependency closure was
built first (`pnpm --filter '@objectstack/driver-turso^...' build`),
then the package's own `build` (tsup and `check-dts-emitted`) ran three
times:

- **Leg 1**, the head text: 6 `dist` files hashed (`index.js`,
`index.mjs`, their sourcemaps, `index.d.ts`, `index.d.mts`). All 13
rewritten non-test lines appear verbatim in `dist`. The 5 docblock lines
on exported members (`remote-transport.ts:1645`, `:2601`, `:2612`;
`turso-driver.ts:3689`, `:3694`) are in all four of `index.js`,
`index.mjs`, `index.d.ts` and `index.d.mts`; the 8 `//` lines in
`turso-driver.ts` are in `index.js` and `index.mjs`.
- **Leg 2**, the base text put back in the 2 non-test touched files (2
of 2 proven equal to their base blob): 4 of the 6 files differ from leg
1 (`index.d.ts`, `index.d.mts`, `index.js`, `index.mjs`); the two
sourcemaps do not. `scripts/ablation-dist-preflight.mjs` finds the base
marker 「[objectstack-ai#14428] A miss answers」 in those 4 built files (exit 0).
- **Leg 3**, after the proven restore (2 of 2 equal to their `HEAD`
blob, `git diff HEAD` empty, porcelain empty): all 6 files are
byte-identical to leg 1, and the preflight's `--absent` reading exits 0
with a clean tree, so the build is deterministic and the difference is
the rewrite.

So the rewrite ships, and
`.changeset/20595-driver-turso-provenance-anchors.md` declares a `patch`
for `@objectstack/driver-turso`, comment text only, with the claim's
`Clause-②: no` line. Every anchor is a commit, so it names no ADR,
repository qualifier or bracketed substitution; it says which published
files carry the reworded text, as measured above. The changeset commit
touches no file under `packages/drivers/driver-turso`.

## Gates (head `e89bd10cd`)

- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `e89bd10cd` (13 paths against
merge base `49161683f`, 113 changed lines) derived 65 commands. All 65
ran (04:52:21Z to 05:03:07Z, after the workspace build), each exit code
captured before any pipe: 65 exit 0. `--ran` reports 「65 derived, 65
run, 0 NOT-MEASURED, 0 UNRUN」 (a derived zero) and exits 0. The PM's
lead derivation (55 commands, tree `550f4cc2f`) is a subset: the extra
10 are the eight families the `.changeset/` path adds (the ADR-0087
registration and empty-changeset pairs, `check:objectui-changeset`,
`check:pm-changeset-deadline-census` and two release self-tests) and two
gates whose source names
`packages/drivers/driver-turso/src/turso-driver.ts` itself
(`check:object-def-param-keys`, `check:tenant-chokepoint`).
- **Named readings:** `node scripts/check-issue-citations.mjs` exits 0
(「every citation this change adds resolves (or is a declared cross-repo
reference)」: 3 judged across 2 files, the three numbers kept on
`turso-driver.ts:2131`, two of them issues and one a pull request);
`pnpm check:issue-citations` exits 0 (self-test, 173 cases, 9
batteries); `pnpm check:doc-authoring` exits 0 (the sibling-package
prose-id baseline holds, no growth); `pnpm check:nul-bytes` exits 0
(9,888 files, no raw control bytes), and a control-byte grep over the 13
changed files finds none (exit 1). The changeset gates
(`check-changeset-no-major`, `check-adr-0087-registration`,
`check-empty-changeset` with `--base origin/main`, and
`check:changeset-gate-self-tests`) exit 0; `check:object-def-param-keys`
exits 0 (5 override parameter positions compared).

- **Build, tests and typecheck, under the verify lock:** at `e89bd10cd`,
the dependency closure and the package were rebuilt, then `pnpm --filter
@objectstack/driver-turso test`: 88 test files pass (88), 2,365 tests
pass and 33 are skipped (2,398); `pnpm --filter
@objectstack/driver-turso typecheck` (`tsc --noEmit`) exits 0 (VERDICT
command-exit 0, held 151s). `tsc --listFilesOnly` puts all 88 tracked
test files and all 12 changed files in `tsconfig.json`'s program. The
workspace build after the merge (`turbo run build
--filter='./packages/*' --filter='./packages/*/*' --concurrency=2`, 71
of 71 tasks, 1 cached; VERDICT command-exit 0, held 319s) ran before the
gates. No importing package owes a run: the declaration files change
only in comment text.
- **Lint, as a proven narrowing, at `e89bd10cd`:** eslint with inline
config disabled, over the 12 touched `.ts` files plus `dist/index.js` as
the control: 13 results, 0 errors and 1 warning, the control's ignore
notice; none of the 12 is reported ignored. `eslint.config.mjs` never
enables type-aware linting (its lines 327 and 328 say so), so a comment
edit cannot move the verdict on an untouched file. The repo-wide `pnpm
lint` is CI's run.

## Acceptance notes

- **Base and merge.** The dispatch read `origin/main` at `550f4cc2f`; by
the time the worktree was cut, `main` had moved one commit (`31d2255f5`,
`packages/mcp/server.json` only), and the branch was cut there. It
merges `main` once, pinned to `49161683f` (merge `e89bd10cd`, no
conflict). The two commits it brought (`48eb9c193`, `spec`; `49161683f`,
`plugin-sharing` and `plugin-audit`) touch neither
`packages/drivers/driver-turso`, `check-issue-citations.mjs` nor
`dispatch-gates.mjs`; the closure and the workspace were rebuilt after
the merge, before the tests and gates. The net diff against `main` is
the 12 rewritten files (+49/−49) and the changeset (+15).
- **The same dead numbers outside this package**, each left to its own
carrier: `driver-mongodb` (this lane's next stage) names objectstack-ai#14428 in
`src/mongodb-driver.ts` and its tests and objectstack-ai#14434 in
`mongodb-update-missing-id.test.ts`, where `ca3fd4b1a` and `93940d492`
are the anchors; `driver-sqlite-wasm`, which the census reads 0, still
carries objectstack-ai#12380, objectstack-ai#14438, objectstack-ai#16711, objectstack-ai#17690, objectstack-ai#17876 and objectstack-ai#17879 in five test
files (comments and two `describe` titles; the census defers test files,
every anchor is in this table, and the comment sites are the same
sentences this stage rewrote here);
`scripts/check-object-def-param-keys.mjs` and
`.github/workflows/lint.yml` name objectstack-ai#16570 and objectstack-ai#16711 (outside the census
surface); `service-analytics`'s `contains-membership.test.ts` carries
objectstack-ai#17590 in a comment, and `driver-sql`'s test strings carry objectstack-ai#12380 and
objectstack-ai#17590; ADR-0104 names objectstack-ai#12380 (governed); the release pages name objectstack-ai#6075
and objectstack-ai#12380 (release-owned).
- **Wording only:** no line without a number was changed.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…mits that decided them (stage 11 of objectstack-ai#20595) (objectstack-ai#21617)

Part of objectstack-ai#20595
Clause-②: no

## What changed

Stage 11 of the `domain:engine` lane of the dead-citation sweep:
`packages/drivers/driver-mongodb/**`, comment and docblock prose only,
per the claim (`5971485908`). Stages 1 to 10 landed as `a7d9768ec`,
`d150c3039`, `4bf4e7e70`, `13a24ece2`, `db0cf2231`, `85986144c`,
`48fa7a381`, `c205b6c35`, `c98a72d69` and `fd5a1cd59`. objectstack-ai#20595 stays
open: the other half of this lane is the packages this stage does not
touch (`formula` 4 and `metadata-fs` 2 on the census after this stage, 6
in all), plus the test-string sites the card carries for a widened
stage.

Every comment or docblock site in the package that cited a tracker
number answering 404 is rewritten in ruling C+D's form C (record
`5749154545` on objectstack-ai#19123): the ADR when one records the decision,
otherwise the commit in this repository's history that made it. That is
**22 sites on 22 lines in 10 files, covering 7 numbers, plus one dead
comment id on one more line**:

- **9 census sites** (9 lines, 3 files under `src/`): the whole
`allocated-but-absent` population of the gate's own census in this
package at the base;
- **10 test-comment sites** (10 lines, 6 test files), which the census
defers. They carry 5 numbers: 3 the census itself reads as dead in this
package's `src` (objectstack-ai#11065, objectstack-ai#13195, objectstack-ai#14428) and 2 the census never judges
in this package (objectstack-ai#14434, and objectstack-ai#14917, which also stands on
`tsconfig.test.json`), which the board and a single read each settle;
- **3 sites outside the census glob, inside the claimed surface**: the
`//` comment lines of `tsconfig.test.json` (`:1` objectstack-ai#14917, `:3` objectstack-ai#14613,
`:25` objectstack-ai#14914), stage 8's precedent for the same file shape in
`packages/core`. `vitest.config.ts`, `tsconfig.json`, `package.json`,
`README.md` and `LICENSE` cite no dead number;
- **1 dead comment-id citation**:
`mongodb-11151-boolean-aggregand-answers.test.ts:13` named comment
`5448627494` on objectstack-ai#11152, which answers 404 although objectstack-ai#11152 itself
resolves (the card reports 24 comments and serves 16). Stage 4 rewrote
the same id in `driver-sql` as 「landed as commit f6fa22c」, and this
stage takes that wording.

**Anchors: 7 numbers and the comment id, all by commit; 0 by ADR, 0 by
repository qualifier; 7 distinct shas** (objectstack-ai#14428 and objectstack-ai#14914 share
`ca3fd4b1a`: the card's fix and that pull request's own squash). 5
numbers and the comment id reuse the anchor an earlier stage measured
for them; `a06faebbe` (objectstack-ai#14917) and the objectstack-ai#14914 reading of `ca3fd4b1a` are
measured here.

Only comments changed. Every file keeps its line count (23 lines out, 23
in, plus the changeset), so no line citation into any of them moves. No
code token moves (the guard below). All 46 changed lines open with a
comment marker. **No citation number is added**: on every changed line
the numbers on the new text are a subset of those on the old (the only
numbers on `+` lines are objectstack-ai#5286, objectstack-ai#13676 and objectstack-ai#14504 on
`tsconfig.test.json`, each already on its line and each answering 200).

**A `patch` changeset**: 2 of the 9 rewritten non-test lines are in the
published `dist` (the `MongoDBDriver.update()` docblock in the `.d.ts`
and the JavaScript, and one `//` line esbuild keeps), and `dist` is not
byte-identical with the base text (see Changeset).

## H0: the package and its size

The gate's own `node scripts/check-issue-citations.mjs --census --json`
at base `37442d475` (the before run below), `allocated-but-absent` per
remaining `domain:engine` package:

| package | before | after this stage |
|---|---|---|
| `drivers/driver-mongodb` | **9** | **0** |
| `formula` | 4 | 4 |
| `metadata-fs` | 2 | 2 |
| `drivers/driver-turso`, `metadata-core`, `core`, `metadata-protocol`,
`objectql`, `metadata`, `drivers/driver-sql`, `drivers/driver-memory`,
`drivers/driver-sqlite-wasm`, `plugins/plugin-pinyin-search`,
`platform-objects` | 0 each | 0 each |

The lane total goes 15 to 6. `driver-mongodb` reads 9, as at stage 10's
head census (`e89bd10cd`), so the stage went ahead.

## Census: `driver-mongodb`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count is its `allocated-but-absent` findings under
`packages/drivers/driver-mongodb/`.

| reading | tree | board | whole-repo `allocated-but-absent` | sites |
lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `37442d475`, run 17:13:20Z to 17:16:55Z | enumerated,
195 pages, frontier objectstack-ai#21613, 19,434 records | 128 | **9** | 9 | 3 | 3 |
| after | `ee337fc39`, run 17:27:02Z to 17:30:28Z | enumerated, 195
pages, frontier objectstack-ai#21614, 19,435 records (newest number read before the
run objectstack-ai#21613, after it objectstack-ai#21614) | 119 | **0** | 0 | 0 | 0 |

The whole-repo drop is 9, and the two finding sets differ by exactly the
9 rows of this package, removed; none was added. `resolves` (35,697),
`resolves-as-pull-request` (2,383) and `cross-repo-unjudged` (1,250) did
not move.

The head's later commits are the changeset and one merge of `main`. The
census was run a third time at the head `b69c176e9` (17:44:02Z to
17:47:23Z, 195 pages, frontier objectstack-ai#21615, 19,436 records, newest objectstack-ai#21615
before and after): whole-repo 119, `driver-mongodb` 0, and the finding
set is identical to the after run, line numbers included.

**Supplementary instrument, the whole package.** The census reads
neither test files nor strings nor files outside `src`. A second reading
runs the gate's own exported `extractCitations` (whole-file and
comment-prose projections) over every tracked file in the package (53)
and classifies each citation with the gate's `classifyCitation` against
one board enumerated by the gate's `enumerateBoard` (195 pages, frontier
objectstack-ai#21613, 19,434 records, read 17:17:48Z to 17:21:11Z), the same board for
both readings. Every one of the 7 numbers was then read on its own over
the issues endpoint (17:21:47Z): **all 7 answer 404**; the numbers that
stay on changed lines (objectstack-ai#5286, objectstack-ai#13676, objectstack-ai#14504) and the controls objectstack-ai#11152,
objectstack-ai#11249, objectstack-ai#11635, objectstack-ai#5346, objectstack-ai#13878, objectstack-ai#20399, objectstack-ai#15280 and objectstack-ai#12745 answer 200.

| reading | citations | dead | src comment | test comment |
`tsconfig.test.json` | test string | changelog |
|---|---|---|---|---|---|---|---|
| before, `37442d475` | 1,002 | **36** | 9 | 10 | 3 | 3 | 11 |
| after, `ee337fc39` | 980 | **14** | 0 | 0 | 0 | 3 | 11 |

The citation count drops by 22, the 22 rewritten tracker-number sites;
no respelling stays a citation. The live counts did not move (src
comment: 279 resolve, 12 as pull requests, 3 cross-repo; test comment:
264, 16 and 5; files outside `src`: 9 resolve). A third, raw reading
(every `#` followed by 2 to 6 digits, whatever surrounds it,
`CHANGELOG.md` aside) counts 700 before and 678 after: also a drop of
22.

**Comment ids.** Every ten-digit run under
`packages/drivers/driver-mongodb` (its `CHANGELOG.md` aside) was read:
five lines, four ids. `5448627494` answers 404 and is rewritten;
`5861435168` and `5865693155` (ruling records on objectstack-ai#20311 and objectstack-ai#20399,
`mongodb-filter.ts:696` to `:697`,
`mongodb-20444-empty-operator.test.ts:6`) and `5186668033`
(`mongodb-filter.ts:807`) answer 200; the control `5971485908`, the
claim, answers 200. After the rewrite the package carries four ten-digit
lines, all live. No `issuecomment` or `discussion_r` link stands in the
package (grep exit 1; the same grep finds them in `packages/runtime` and
`packages/spec`).

## Per-number table

`src` counts census sites, `test` the test-comment sites, `cfg` the
`tsconfig.test.json` comment lines. Every sha matches exactly one commit
(`git rev-parse --disambiguate`, count 1), is an ancestor of the base
`37442d475` and of `origin/main` `54521f08c` (`git merge-base
--is-ancestor`, exit 0 for all 7 on both; exit 0 is self-proving, and
the clone was unshallowed first), and names the number it replaces in
its message, its diff or both: 3 in the message and the diff (objectstack-ai#13195,
objectstack-ai#14613, objectstack-ai#14917), 3 in the diff alone (objectstack-ai#11065 for `20950404c`, objectstack-ai#14428 for
`ca3fd4b1a`, the comment id for `f6fa22ce1`), and 2 in the message alone
(objectstack-ai#14434 for `93940d492`, in the subject's squash suffix; objectstack-ai#14914 for
`ca3fd4b1a`, in the subject's squash suffix and a body line naming that
pull request's contract-review round), where the dead number was that
pull request's own and the commit is its squash. The `+` lines carry
exactly these 7 nine-hex spans as new ones. `git blame` at the base puts
10 of the 23 changed lines on their anchor; the other 13 were written by
a commit that cites the number as an earlier decision (`c4ecf0c49`
citing objectstack-ai#11065 three times; `df1812050` citing objectstack-ai#13195 five times;
`9268aec56`, which created
`mongodb-exists-has-value-translation.test.ts` as a measurement before
the ruling, citing objectstack-ai#13195 once; `ca3fd4b1a` citing objectstack-ai#14434 and objectstack-ai#14917;
`a06faebbe` citing objectstack-ai#14613 and objectstack-ai#14914 on the file it created), and in
each case the anchor is the commit that made the change the sentence
credits to the number. `source` says whether an earlier stage already
used this anchor for this number (`reused`) or it was measured here
(`measured`).

| number | src | test | cfg | anchor | kind | source | what it decided |
|---|---|---|---|---|---|---|---|
| `objectstack-ai#11065` | 2 | 1 | 0 | `20950404c` | commit | reused (stage 4, stage
5) | `driver-memory` counts a boolean aggregand as 1/0 in `avg` and
`sum` on both its faces; it wrote `numericAggregandExpr` in
`memory-analytics.ts`, the expression `mongodb-aggregation.ts:657` says
it reproduces (the squash of PR objectstack-ai#11153; its diff names objectstack-ai#11065 7 times) |
| `objectstack-ai#13195` | 6 | 5 | 0 | `9dac1ae01` | commit | reused (stage 5) |
`$exists` means HAS A VALUE on the live mingo path, the analytics face
and `translateFilter`; it wrote the `_presenceAnd` guard for `$exists`
alone, the `[objectstack-ai#13195] Value-independent` comment and the note that the
same clobber was reachable through `$null` and `$between`, the three
things the `mongodb-filter.ts` sites credit to the number (the squash of
PR objectstack-ai#13529) |
| `objectstack-ai#14428` | 1 | 2 | 0 | `ca3fd4b1a` | commit | reused (stage 10) |
`update()` on a missing id answers `null` on MongoDB and Turso's remote
face; it wrote the `MongoDBDriver.update()` docblock and created
`mongodb-update-missing-id.test.ts` |
| `objectstack-ai#14434` | 0 | 1 | 0 | `93940d492` | commit | reused (stage 4, stage
5, stage 10) | declare the not-found arm on `IDataDriver.update()` (that
pull request's squash); stage 10's 「Since objectstack-ai#13878 (commit 93940d4)」 for
the twin sentence |
| `objectstack-ai#14917` | 0 | 1 | 1 | `a06faebbe` | commit | measured | put the test
layer in front of tsc via the `objectstack-ai#5286` sibling route; it created this
package's `tsconfig.test.json` and graduated the `TEST_DEBT` entry (the
squash of PR objectstack-ai#15465) |
| `objectstack-ai#14613` | 0 | 0 | 1 | `81208086a` | commit | reused (stage 8) |
`@objectstack/core` declares a typecheck script and its test layer
enters the ratchet through a `tsconfig.test.json` sibling; stage 8 used
it for the identical sentence in `packages/core` |
| `objectstack-ai#14914` | 0 | 0 | 1 | `ca3fd4b1a` | commit | measured | that pull
request's own squash; its message records the three TS18047 errors the
widened `update()` declaration introduced and the type-check debt
ratchet catching them (10 to 13), the event `tsconfig.test.json:25`
describes |
| comment `5448627494` | 0 | 1 | 0 | `f6fa22ce1` | commit | reused
(stage 4) | the boolean aggregand column in the aggregation conformance
fixture with ruled numeric `min` / `max` on every face; its message
records the 2026-08-28 maintainer ruling (option A, superseding objectstack-ai#11249's
`false` / `true`), its diff names the id 3 times, and it wrote the line
|

No ADR or ruling record names any of the 7 numbers as the place their
decision is recorded.

## Wordings to check

Most rewrites swap a tag in place (`[#N]` to `[commit SHA]`, `(#N)` to
`(commit SHA)`, `#N's` to `commit SHA's`, stage 1's form). These say
more than the tag:

- **The dead comment id**: 「applied on that card's comment 5448627494,
ruling verbatim and」 became 「landed as commit f6fa22c, ruling verbatim
and」 (`mongodb-11151-boolean-aggregand-answers.test.ts:13`), stage 4's
wording for the same id and the same sentence in `driver-sql`, verbatim.
The quoted ruling 「12745 A回,其他同意。」 on the next line is untouched.
- **A dead pull-request number, in a possessive**: 「That is how CI
caught PR objectstack-ai#14914's / three TS18047 errors」 became 「That is how CI caught
commit ca3fd4b's / three TS18047 errors」 (`tsconfig.test.json:25`).
The errors arose on that pull request's branch and were narrowed before
it landed, so the squash as landed carries none of them; its message
records both the errors and the catch. This is stage 10's squash form
(「objectstack-ai#5181 (PR objectstack-ai#6076)」 to 「objectstack-ai#5181 (commit 6513c17)」). The alternative, if
the possessive reads wrong: 「caught, in the change commit ca3fd4b
landed, its」, one line, no reflow.
- **A filed defect**: 「That exclusion is itself a filed defect (objectstack-ai#14917),
not a design.」 became 「… a filed defect (closed by commit a06faeb),
not a design.」 (`mongodb-update-missing-id.test.ts:72`). The sentence
was written the day before the fix and describes the card; stage 5's
「CLOSED by commit 9dac1ae」 is the form for a card named as an open
defect.
- **A file header written before the ruling**:
`mongodb-exists-has-value-translation.test.ts:4` was written by
`9268aec56`, the measurement that pinned the divergence while 「the
direction stays undecided」; `9dac1ae01` inverted the file in place onto
the ruled answer. The header now opens 「[commit 9dac1ae]」, stage 5's
anchor for the twin header in `driver-memory`'s
`memory-exists-has-value-faces.test.ts`.
- **Sentence starts**: where the number opened a sentence, the new text
opens with 「Commit」: 「// Commit 9dac1ae landed this rule for `$exists`
alone」 (`mongodb-filter.ts:1476`). 「The commit 2095040 family shape」
(`mongodb-11151-boolean-aggregand-answers.test.ts:10`) is stage 4's 「the
settled commit 2095040 family shape」.
- **Ruling dates kept**: 「[objectstack-ai#13195, ruled 2026-08-30]」 became 「[commit
9dac1ae, ruled 2026-08-30]」 on two test lines, stage 5's form.
- No line was reflowed, so some are longer than their block's wrap
(`eslint.config.mjs` declares no line-length rule, and a reflow would
move neighbouring lines and every line citation into the file).

## Sites left

- **In comments (src, test, outside the glob): none.**
- **String literals: 3 test-string sites, 2 numbers, 3 files**: the
`describe` titles at `mongodb-exists-has-value-translation.test.ts:138`
(objectstack-ai#13195) and `mongodb-update-missing-id.test.ts:150` (objectstack-ai#14428), and the
`it` title at `mongodb-operator-key-clobber.test.ts:227` (objectstack-ai#13195). Both
numbers are in this stage's table. Strings are outside this stage's
surface; non-test strings cite none.
- **Outside `src`:** the release-owned `CHANGELOG.md` names dead numbers
on 11 sites (7 numbers); left.

## Mechanical guard: no code token moves

The guard compares base `37442d475` against the tree over all 10 touched
files, with TypeScript 6.0.3, to stages 2 to 10's two-reading
specification (their script was a scratch file and is gone, so it was
rewritten here to that specification and proven with the controls
below):

- **Reading 1**: the parser's leaf nodes, from a `forEachChild` walk.
Comments are trivia there, and JSDoc is never visited. A leaf that is
not itself a token is re-scanned with trivia skipped.
- **Reading 2**: the full token stream in parser context, from a
`getChildren` walk, JSDoc nodes skipped. String, template and numeric
literals are compared in full on both readings.
- **`tsconfig.test.json`**: reading 2 over `ts.parseJsonText`, and
reading 1 replaced by the parsed config object
(`ts.parseConfigFileTextToJson`) compared structurally.

Results, at `ee337fc39` (the later commits touch none of the 10 files):

- Real run: 20,837 base tokens, **0 files with a token change** (exit
0).
- Comment controls: 「Value-independent」 to 「VALUE-independent」
(`mongodb-filter.ts`) and 「it is the BUILD」 to 「it is the BUILd」
(`tsconfig.test.json`): 0 files changed (exit 0 each).
- Positive control, an identifier (`export class MongoDBDriver` to
`XMongoDBDriver`, `mongodb-driver.ts`): DIFFER on both readings (exit
1).
- Positive control, a string literal (the `describe` title 「… on a
missing id」 to 「… on a missing iD」,
`mongodb-update-missing-id.test.ts`): DIFFER on both readings (exit 1).
- Positive control, a template literal (`input: ` followed by the
`$${field}` template, to `$${field}x`, `mongodb-aggregation.ts`): DIFFER
on both readings (exit 1).
- Positive control, a numeric literal (`$lte: 100` to `101`,
`mongodb-filter.test.ts`): DIFFER on both readings (exit 1).
- Positive control, a config value (`"lib": ["ES2022"]` to `ES2023`,
`tsconfig.test.json`): DIFFER on both readings (exit 1).

Each mutation went through `scripts/ablation-replace.mjs` (wrap mode,
anchor hit 1 to 0, blob changed) under a shell trap that restores by
absolute path from `HEAD`. Each restore was proven equal to its `HEAD`
blob (`2c4ba4f8babb`, `aebae4859583`, `48ffb45e011f`, `4f12e29dbbce`,
`581fa7ca2401`, `162a1f72bae6`), with `git diff HEAD` empty and a clean
tree afterwards.

## Changeset: `patch` (`dist` measured)

`files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the package is
not private. In one script under the shared verify lock (VERDICT
command-exit 0, held 164s, shared-box seconds), at `ee337fc39`: the
dependency closure was built first (`pnpm --filter
'@objectstack/driver-mongodb^...' build`, exit 0), then the package's
own `build` (tsup and `check-dts-emitted`) ran three times, exit 0 each:

- **Leg 1**, the head text: 6 `dist` files hashed (`index.js`,
`index.mjs`, their sourcemaps, `index.d.ts`, `index.d.mts`). 2 of the 9
rewritten non-test lines appear verbatim in `dist`: the
`MongoDBDriver.update()` docblock line (`mongodb-driver.ts:432`) in all
four of `index.js`, `index.mjs`, `index.d.ts` and `index.d.mts`, and the
`// [commit 9dac1ae] Value-independent` line
(`mongodb-filter.ts:1265`) in `index.js` and `index.mjs`. The other 7
sit in comments the build drops.
- **Leg 2**, the base text put back in the 3 non-test touched files (3
of 3 proven equal to their base blob, written to the tree only): 4 of
the 6 files differ from leg 1 (`index.d.ts`, `index.d.mts`, `index.js`,
`index.mjs`); the two sourcemaps do not.
`scripts/ablation-dist-preflight.mjs` finds the base marker 「[objectstack-ai#14428] A
miss answers」 in those 4 built files (exit 0).
- **Leg 3**, after the proven restore (3 of 3 equal to their `HEAD`
blob, `git diff HEAD` empty, porcelain empty): all 6 files are
byte-identical to leg 1, and the preflight's `--absent` reading exits 0
with a clean tree, so the build is deterministic and the difference is
the rewrite.

So the rewrite ships, and
`.changeset/20595-driver-mongodb-provenance-anchors.md` declares a
`patch` for `@objectstack/driver-mongodb`, comment text only, with the
claim's `Clause-②: no` line. Every anchor is a commit, so it names no
ADR, repository qualifier or bracketed substitution; it says which
published files carry the reworded text, as measured above. The
changeset commit touches no file under
`packages/drivers/driver-mongodb`.

## Gates (head `b69c176e9`)

- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `b69c176e9` (11 paths against
merge base `54521f08c`) derived 62 commands. All 62 ran (17:43:06Z to
17:55:21Z, after the workspace build), each exit code captured before
any pipe: 62 exit 0. `--ran` reports 「62 derived, 62 run, 0
NOT-MEASURED, 0 UNRUN」 (a derived zero) and exits 0. The PM's lead
derivation (48 commands, tree `ec390ec00`, one path) is a subset: the
extra 14 are the eight families the `.changeset/` path adds (the
ADR-0087 registration and empty-changeset pairs,
`check:objectui-changeset`, `check:pm-changeset-deadline-census` and two
release self-tests), `check:type-check-coverage` and
`check:type-check-debt` (the `tsconfig.test.json` path), and four gates
whose sources name the touched driver files
(`check:engine-double-contract`, `check:objectql-double-limit`,
`check:query-options-erasure`, `check:where-matcher`).
- **Named readings:** `node scripts/check-issue-citations.mjs` exits 0
(「no issue citations added against 54521f0」: the `+` lines in the 3
non-test source files carry no number); `pnpm check:issue-citations`
exits 0 (its self-test); `pnpm check:doc-authoring` exits 0 (the
sibling-package prose-id baseline holds, no growth); `pnpm
check:nul-bytes` exits 0 (9,980 files, no raw control bytes), and a
control-byte grep over the 11 changed files finds none (exit 1). The
changeset gates exit 0: `check-adr-0087-registration` (「1 non-breaking
changeset(s) seen」), `check-empty-changeset` (「1 declaring changeset(s)
added」), `check-changeset-no-major` (「no `major` bump」; its Clause-②
level axis reads the pull request body, so it is not applicable to a
local run and is CI's reading), and `check:changeset-gate-self-tests`.
`check:type-check-coverage` and `check:type-check-debt` exit 0 (the debt
re-measure: every entry at its measurement).

- **Build, tests and typecheck, under the verify lock** (VERDICT
command-exit 0, held 225s, shared-box seconds), at `b69c176e9`: the
workspace build (`turbo run build --filter='./packages/*'
--filter='./packages/*/*' --concurrency=2`: 71 of 71 tasks, 17 cached),
then `pnpm --filter @objectstack/driver-mongodb test`: 31 test files
pass and 5 are skipped (36), 690 tests pass and 182 are skipped (872);
the skips are the suites that need a `mongod` binary, which run only on
opt-in. `pnpm --filter @objectstack/driver-mongodb typecheck` (`tsc
--noEmit` and `check:test-typecheck` over `tsconfig.test.json`) exits 0.
`tsc --listFilesOnly` puts all 36 tracked test files in
`tsconfig.test.json`'s program, and each of the 9 changed `.ts` files in
a program (the 3 non-test ones in both). No importing package owes a
run: the declaration files change only in comment text.
- **Lint, as a proven narrowing, at `b69c176e9`:** eslint with inline
config disabled, over the 9 touched `.ts` files plus `dist/index.js` as
the control and `tsconfig.test.json`: 11 results, 0 errors and 2
warnings, the control's ignore notice and 「no matching configuration」
for `tsconfig.test.json` (eslint's files patterns never name `.json`, so
that file is outside its population); none of the 9 is reported ignored.
`eslint.config.mjs` never enables type-aware linting (its lines 327 and
328 say so), so a comment edit cannot move the verdict on an untouched
file. The repo-wide `pnpm lint` is CI's run.

## Acceptance notes

- **Base and merge.** The dispatch read `origin/main` at `ec390ec00`; by
the time the worktree was cut, `main` had moved one commit (`37442d475`,
a release-workflow fix touching none of this package,
`check-issue-citations.mjs` or `dispatch-gates.mjs`), and the branch was
cut there. The clone was shallow (two shallow roots, 1,215 commits
reachable) and held none of the anchors, so it was unshallowed (`git
fetch --unshallow origin main`, 15,634 commits) before any blame,
ancestry or history reading. The branch merges `main` once, pinned to
`54521f08c` (merge `b69c176e9`, no conflict, no deferred regeneration).
The two commits it brought (`0721848b8`, `spec`; `54521f08c`, a QA
checklist item) touch neither `packages/drivers/driver-mongodb`,
`check-issue-citations.mjs` nor `dispatch-gates.mjs`; the workspace was
rebuilt after the merge, before the tests and gates. The net diff
against `main` is the 10 rewritten files (+23/−23) and the changeset
(+15).
- **The same dead numbers outside this package**, each left to its own
carrier: `driver-mongodb`'s 3 test-string sites (above); `CHANGELOG.md`
(release-owned).
- **Wording only:** no line without a number was changed.

---
_Generated by [Claude
Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants