Skip to content

fix(driver-sql): a MySQL NOW() datetime default carries its column's precision - #21252

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21241-mysql-now-default-precision
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21241-mysql-now-default-precision

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21241

Clause-②: no

What was wrong

On MySQL a declared Field.datetime is built as DATETIME(3). For its defaultValue: 'NOW()' default, SqlDriver.nowColumnDefault('datetime') fell through to a bare knex.fn.now(), which is CURRENT_TIMESTAMP at precision 0. MySQL 8.0 refuses a CURRENT_TIMESTAMP default whose precision differs from its column's, so the whole CREATE TABLE failed and so did ALTER TABLE … ADD. The builtin created_at / updated_at columns beside it carried their own literal now(3) and were accepted.

Measured at the base be5a83cf against a throwaway MySQL 8.0.46 (server zone +08:00):

boot schema sync refused DDL data door
pnpm dev:crm -- --fresh --database mysql://… synced 80, skipped 0, failed 1 sys_activity: `timestamp` datetime(3) default CURRENT_TIMESTAMP → Invalid default value for 'timestamp' GET /api/v1/data/sys_activity?limit=1 → 500 DATABASE_ERROR
pnpm dev:showcase -- --fresh --database mysql://… synced 102, skipped 2, failed 2 sys_activity as above, and sys_presence: `last_seen` datetime(3) default CURRENT_TIMESTAMP → Invalid default value for 'last_seen' GET /api/v1/data/sys_presence?limit=1 → 500, sys_activity → 500

The fix

  • One source for the precision. MYSQL_DATETIME_PRECISION is a module constant in sql-driver.ts. Five sites read it: the declared datetime column (createColumn), the builtin audit columns (createAuditTimestampColumn), the NOW() default (nowColumnDefault), the UPDATE stamp (updatedAtStamp) and the legacy TIMESTAMP widening (migrateMysqlDatetimeColumns). Before this PR, five literal 3s lived at those sites. Now there is one, and no new literal was added.
  • nowColumnDefault('datetime') on MySQL returns knex.fn.now(MYSQL_DATETIME_PRECISION), which renders CURRENT_TIMESTAMP(3).
  • The builtin audit column's MySQL default is routed through nowColumnDefault('datetime'), the way the SQLite branch already is. The declared column and the builtin column now share one definition. The emitted DDL for the audit columns is byte-identical: datetime(3) default CURRENT_TIMESTAMP(3).
  • PostgreSQL and SQLite emit unchanged SQL. On PostgreSQL the driver builds timestamptz, and CURRENT_TIMESTAMP on it was measured accepted on PostgreSQL 16.14. timestamp(3) default CURRENT_TIMESTAMP is also accepted there, rounding silently. So the mismatch does not exist on that dialect.

Bounded in-place fix in the same statement: the TIMESTAMP widening dropped a declared NOW() default

The widening's ALTER … MODIFY line now reads the constant, so this PR touches it. That same statement restated the default of created_at / updated_at and dropped the default of a declared NOW() column. The TIME twin (migrateMysqlTimeColumns) already restates it.

Measured on MySQL 8.0.46 with the driver built at this branch before the change: a legacy stamped_at timestamp null default current_timestamp column came out of schema sync as datetime(3) with COLUMN_DEFAULT null, and create without the field answered stamped_at = null. The widening now restates nowColumnDefault('datetime') for a declared NOW() column as well. All four conditions for an in-place fix hold:

  • same family (a MySQL NOW() datetime default that is not honoured);
  • mechanical, with the shape fixed by the TIME twin;
  • same file and claim;
  • same gate family.

The changed lines are sql-driver.ts migrateMysqlDatetimeColumns, and a MySQL-only pin, §3.

Pins: packages/drivers/driver-sql/src/sql-driver-21241-mysql-now-default-precision.test.ts

  • §1, run on every runner with no server: for mysql2, pg and better-sqlite3, the compiled DDL of a declared NOW() datetime column is byte-identical to the builtin audit column's. A second case reads the mysql2 DDL of the required sys_activity.timestamp shape and checks that its CURRENT_TIMESTAMP(n) names the column's own datetime(n). This compares against the column, not against a literal.
  • §2, one cell per dialect through declareDialectCell:
    • the table syncs, both on create and on add-column for a table that already exists;
    • create without the field answers the instant the column DEFAULT stored. The check uses a window and confirms that the answer equals findOne;
    • a raw insert that never names the column is filled by the DEFAULT alone;
    • the server's own catalogue (information_schema / pragma table_info) reports the declared column's type and default equal to created_at's.
  • §3, MySQL only: a legacy TIMESTAMP NOW() column keeps a default through the widening, equal to created_at's.

Verification (HEAD d334fe314b)

Every reading below was re-run at d334fe314b: the branch plus one merge of origin/main 4727fcb22a, which touches no driver-sql file and no lockfile. The readings match the earlier head e5cab5f20b, except the driver-turso count, which moved because #21226 landed on main. (Seat edit.)

The live servers were local: MySQL 8.0.46 at +08:00 and PostgreSQL 16.14 at Asia/Shanghai, with TZ=America/New_York.

  • Pins: vitest run --reporter=verbose src/sql-driver-21241-mysql-now-default-precision.test.ts with both URLs set → 17 passed (17).
  • Reverse verification. sql-driver.ts was written back to the base blob e65a0f08 while the HEAD blob 533a790b stayed committed. On-disk hash equal to the base blob, grep -c MYSQL_DATETIME_PRECISION → 0. Result: 7 failed | 10 passed (17). Every failure is MySQL: §1 mysql2 ×2, §2 live mysql ×4 (Invalid default value for 'stamped_at' at create and at add-column), and §3. The SQLite and PostgreSQL cells stayed green (the control). The restore was checked: hash equal to the HEAD blob and git diff HEAD empty.
  • Ablation of the widening half. Through scripts/ablation-replace.mjs: anchor 1 → 0, blob 533a790b → 16057202. Only §3 went red, dflt: null against CURRENT_TIMESTAMP(3); the other 16 stayed green. Restored: blob equal to HEAD and git diff HEAD empty.
  • @objectstack/driver-sql, the whole suite against both live servers with OS_EXPECT_LIVE_DIALECT_MATRIX=1: 224 files passed, 5386 passed | 1 skipped. The live-dialect reporter printed "all 3 dialects were exercised".
  • @objectstack/driver-sql, the SQLite tier (pnpm --filter @objectstack/driver-sql test, no URLs, at 04b23d2791 before the widening commit): 213 files passed | 11 skipped, 3570 passed | 200 skipped.
  • Typecheck of @objectstack/driver-sql, @objectstack/driver-sqlite-wasm and @objectstack/driver-turso (the inheritors): all three Done. tsc --listFiles includes the new test file once.
  • Tests of the inheritors: driver-sqlite-wasm 36 files / 675 passed; driver-turso 87 files / 2349 passed | 33 skipped.
  • pnpm check:driver-conformance gave the same reading before the first edit and after the last commit: OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt, with 0 in the DIALECT ledger.
  • Gates: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 63 families. All 63 ran with exit 0. --ran reconciliation: 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN, with every line recording its exit code. The derivation warned that the tree is one main commit behind (3dc33b2d13, file-disjoint from this diff, and it touches check-route-envelope.mjs / engine-double-contract.pinned.json). CI re-derives on the merge ref.
  • Lint, narrowed:
    • eslint --no-inline-config --format json over the two changed .ts files: 2 files, 0 errors, 0 warnings.
    • Population: both files are linted, not ignored.
    • Invariance: the config's parserOptions carry no project / projectService, so linting is not type-aware and this diff cannot move a verdict on an untouched file.
    • Repo-wide pnpm lint is left to CI.

After the fix, same boot

pnpm dev:showcase -- --fresh --database mysql://… at e5cab5f20b:

  • no Schema sync FAILED;
  • GET /api/v1/data/sys_presence → 200 and sys_activity → 200;
  • information_schema reports sys_activity.timestamp and sys_presence.last_seen as datetime(3) / CURRENT_TIMESTAMP(3), equal to their created_at;
  • a POST / PATCH / DELETE on showcase_account left three sys_activity rows (created, updated, deleted).

Raise-rule reading (triage)

At the base on MySQL, with sys_activity absent, a missing table does not break a record write. POST /api/v1/data/crm_account → 201, PATCH → 200 (a read-back showed the new name), DELETE → 200 (a read-back answered 404). sys_audit_log holds 3 rows for the record. Each mutation lost its activity row: the server logged Insert operation failed {object: sys_activity …} at warn and Audit write FAILED (ER_NO_SUCH_TABLE …) at error. The API answers carried nothing about it. Applying the rule is the seat's decision.

Acceptance notes

  • Existing tables / migration (H5). On MySQL no table could have been created with the refused default, so a fresh boot after this change creates the missing tables and no migration is owed. A column that an earlier release's TIMESTAMP widening already left without a default does not get one back from this change. The widening only touches columns that are still timestamp. This is a read-only inference beyond the measurement above.
  • MariaDB not measured. No MariaDB server here. client: 'mariadb' is not in the driver's MySQL family, but mysql2 pointed at a MariaDB server is. Whether MariaDB accepted the bare default, and so holds tables with a precision-0 default, is NOT MEASURED.
  • Operator text. At the base, the Audit write FAILED line for the sys_activity insert names sys_audit_log as the row that "never landed" and as the table to check. It printed 4 times despite "reported ONCE". Observation only, nothing filed.
  • sys_packages. Its raw DDL (created_at TEXT DEFAULT CURRENT_TIMESTAMP) is still refused on MySQL in both boots. This was already recorded in PR fix(driver-sql): create and bulkCreate answer the stored row on MySQL (#21227) #21239's acceptance notes; it is domain:services, and no door was measured.
  • Not filed from this PR, handed to the seat in the report. At the base on MySQL, GET /api/v1/auth/jwks and GET /api/v1/auth/token answered 500. An insert into sys_jwks is refused with Incorrect datetime value … for column 'updated_at', and the server logs JWT signing failed with alg "EdDSA".

Generated by Claude Code

claude added 3 commits October 1, 2026 21:53
…precision

A declared Field.datetime with defaultValue 'NOW()' is built DATETIME(3) on
MySQL, but nowColumnDefault's datetime arm fell through to a bare
CURRENT_TIMESTAMP (precision 0). MySQL refuses a CURRENT_TIMESTAMP default
whose precision differs from its column's, so the whole CREATE TABLE failed
and sys_activity / sys_presence were never created.

One source for the precision, MYSQL_DATETIME_PRECISION, now read by the
declared column, the builtin audit columns (whose default routes through
nowColumnDefault, as on SQLite), the NOW() default, the UPDATE stamp and the
legacy TIMESTAMP widening. Postgres and SQLite emit unchanged SQL.

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
…lumn on every dialect

Section 1 compiles the DDL with no server: a declared NOW() datetime column
is byte-identical to a builtin audit column on mysql2, pg and better-sqlite3,
and on mysql2 the default names the column's own precision. Section 2 runs
through declareDialectCell: the table syncs (create and add-column), an insert
omitting the field answers the instant the column DEFAULT stored, and the
server catalogue reports the declared column's type and default equal to
created_at's.

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

Widening a legacy TIMESTAMP column to DATETIME(3) with ALTER ... MODIFY
restated the audit columns' default and dropped a declared NOW() field's, so
an insert omitting the field stored NULL afterwards. The TIME twin already
restated it. Both now take nowColumnDefault's expression. Adds the changeset
and a MySQL-only pin for the widening.

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-sql, touching 7 documentable anchor(s).

9 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))
  • content/docs/data-modeling/index.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/data-modeling/queries.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))
  • 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))
  • content/docs/protocol/objectql/types.mdx (via SqlDriver (symbol, a top-level class))

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

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

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

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 11 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 4727fcb22a7e6a28da1ad20ef74469fbabb8dd0e → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 4727fcb22a7e6a28da1ad20ef74469fbabb8dd0e

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d334fe314b451d84ae64f59697391df55dd3aff2
Local-runs: none

Inputs, and nothing else: card #21241 (body; comments 5940340798, 5941097310, 5941379243, 5942461506, 5942490638), PR #21252 (body as the seat edited it, file list of 3, the net diff against merge base 4727fcb22a fetched as the three-dot diff and compared byte-equal to a local git diff of the fetched head), the head's check-runs read last. Reads were git show / git grep on the fetched ref and REST GETs; nothing built, run or re-run.

① Derived judgments

One precision source — right. At the base (e65a0f08) the code carried five MySQL DATETIME-precision literals: updatedAtStamp (now(3)), the TIMESTAMP widening (current_timestamp(3) and datetime(3) in one statement), createAuditTimestampColumn (precision: 3 and now(3)) and createColumn's datetime arm (precision: 3). At the head the module constant MYSQL_DATETIME_PRECISION = 3 (line 408) is read at exactly those five sites (8376, 13103, 17881, 17963, 19495) and no code literal of that family remains; the only residual DATETIME(3) is prose inside the widening's info log string, not DDL. The time(3) literals are a different family (TIME precision) and outside this card. Triage's premise that the source "already existed" was false at the base; creating it is the direction's intent carried out, as the claim amendment records.

nowColumnDefault('datetime') on MySQL — right. The new arm sits after the time and date arms inside the !isSqlite branch and is guarded by type === 'datetime' && this.isMysql, returning knex.fn.now(MYSQL_DATETIME_PRECISION); PostgreSQL still falls to the bare knex.fn.now() and the SQLite switch is untouched. The declared column (createColumn → applyDeclaredColumnDefault → nowColumnDefault(type)) therefore renders datetime(3) default CURRENT_TIMESTAMP(3).

Builtin audit columns' MySQL DDL bytes unchanged — right. createAuditTimestampColumn's MySQL arm now reads table.datetime(name, { precision: MYSQL_DATETIME_PRECISION }).defaultTo(this.nowColumnDefault('datetime')), and on MySQL that default is the same knex.fn.now(3) it spelled before. The reverse verification shows it from the other side: with sql-driver.ts reverted to the base blob, §1 mysql2 received the declared column as datetime(3) default CURRENT_TIMESTAMP against the audit column's CURRENT_TIMESTAMP(3), so the audit column rendered the same bytes at base and head. updatedAtStamp is byte-identical (now(3)).

PostgreSQL and SQLite DDL unchanged — right. No PostgreSQL or SQLite arm is touched in nowColumnDefault, createAuditTimestampColumn or createColumn; the only other nowColumnDefault('datetime') caller (the SQLite-only table rebuild at 14332) takes the untouched SQLite branch. §1's pg and better-sqlite3 cases and §2's two live cells are a real control: they were equal before the fix and stayed green under the reverse verification.

The widening restating a declared NOW() default — right, and bounded. legacyMysqlTimestampColumns enumerates only the audit columns plus declared non-multivalued datetime fields, and only those information_schema still types timestamp; the restatement condition is isAudit || isNowDefaultValue(fields[col.name]?.defaultValue), where isNowDefaultValue is spec's isNowDefaultToken (/^now\(\)$/i after trim). A declared datetime with a literal default keeps today's behaviour; a column already widened is never re-touched. The restated expression is nowColumnDefault('datetime').toString(), so fresh and widened columns share one spelling (the TIME twin keeps its own literal expression, outside this card). The case change from current_timestamp(3) to CURRENT_TIMESTAMP(3) is not consumer-visible: MySQL's catalogue reports CURRENT_TIMESTAMP(3) either way. previewDatetimeConvergence (the os migrate plan twin) lists columns from the same predicate and never prints the default clause, and the net diff has no hunk in it, so plan and apply still agree by construction.

Pins — they prove what they claim. §1 compiles DDL offline on mysql2, pg and better-sqlite3 and asserts declared-equals-audit plus a non-vacuous default; the required-column case reads the datetime(n) precision off the DDL and requires the CURRENT_TIMESTAMP(n) to match it, with a missing (n) read as 0, so it compares the column to itself, not to a literal. §2 goes through declareDialectCell (a total measure; a missing URL is reported un-run and is a failure under OS_EXPECT_LIVE_DIALECT_MATRIX=1) and measures create, add-column on an existing table, create without the field inside a window and equal to findOne, a raw insert naming no column (the DEFAULT alone), and the server catalogue equal to created_at's. §3 (MySQL only) seeds a pre-#3942 timestamp null default current_timestamp column, runs initObjects, and requires the widened column's catalogue row to equal created_at's; because created_at is always restated, that equality is not tautological, and the dev's ablation confirms it bites: removing the restatement turned only §3 red (dflt: null against CURRENT_TIMESTAMP(3)). The reverse verification's 7-failed/10-passed split, every failure MySQL, is the right shape for a MySQL-only fix with two controls. The CI Temporal Conformance (live PG + MySQL) job succeeded on this head, so the live MySQL cells were measured in CI, not only on the dev's box.

No migration for fresh MySQL tables — right. A CREATE TABLE MySQL refused left no table; the next boot creates it. The legacy stripped default — acceptable as a recorded Acceptance note. A column an earlier release's widening already moved to datetime(3) and left default-less is outside legacyMysqlTimestampColumns's data_type = 'timestamp' filter, so this PR does not repair it; the population is bounded to pre-#3942 MySQL databases holding a declared NOW() datetime field, and every engine write path fills NOW() app-side, so only a writer bypassing the engine sees NULL. That is a repair of its own (restate the declared default on datetime columns whose COLUMN_DEFAULT is null), not a reason to hold this fix; see ③.

Inheritors. createAuditTimestampColumn now couples to the protected nowColumnDefault; driver-sqlite-wasm and driver-turso are SQLite-family (isMysql false), their suites passed per the report, and Test Core and the type-check jobs are green on the head.

② Semver level

.changeset/21241-mysql-now-default-precision.md declares '@objectstack/driver-sql': patch with Clause-②: no at line start (line 7); the PR body carries Clause-②: no at line start (line 3). Right. The accept set is unchanged: Field.datetime({ defaultValue: 'NOW()' }) was already an accepted declaration on every surface, and the spec's token recogniser is untouched; the driver now emits DDL MySQL accepts for a declaration it already promised to honour. No export, key or option is added or removed, nothing accepted before is refused, and the widening's restatement moves a column toward its declaration rather than widening what an author may write. A fix that makes previously refused DDL succeed is a bug fix in a released package, which the rules bind to patch, never none. CI's Check Changeset succeeded on the head.

③ Boundary flags

Check-runs on the head, read at 2026-10-01T23:19:36Z: 41 runs, all completed; 36 success, 5 skipped (Build Docs, Console Pin Gate, the opt-in packed-tarball smoke, and the body-edit re-triggers of Auto Label and Check PR Size whose first runs succeeded), 0 failed, 0 in progress. Temporal Conformance (live PG + MySQL) completed success at 2026-10-01T22:49:14Z; Lint & Repo Gates at 2026-10-01T22:50:00Z; Governed Surface Queue Guard success (no governed path in the file list). Combined status success (Vercel).

Implemented-by: claude/issue-21241-mysql-now-default-precision
Reviewed-by: session_017xfMoEjKUuSh2xYB8sCozp

VERDICT: PASS

Rendered 2026-10-01T23:23Z by a subagent of the seat session; the seat adopts the verdict.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 23:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 23:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 7923c8e Oct 1, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21241-mysql-now-default-precision branch October 1, 2026 23:53
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…nd ADRs that decided them (stage 3 of objectstack-ai#20595) (objectstack-ai#21268)

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

## What changed

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

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
**279 sites on 275 lines in 67 files, covering 70 numbers**:

- **136 census sites** (136 lines, 17 files under `src/`): the whole
`allocated-but-absent` population of the gate's own census in this
package at the base, the slash-joined `plugin.ts` `objectstack-ai#10629` from the
post-landing census (`5923084795`, now at `:1543`) included;
- **1 site in `vitest.config.ts`** (`:61`, `objectstack-ai#17853`): outside the census
glob, inside the claimed surface;
- **142 test-comment sites** (138 lines, 49 test files), which the
census defers. They carry 51 numbers: the census itself reads 36 of them
as dead elsewhere in the repository, and never judges the other 15 (they
stand only in test files here), which the board and a single read each
settle.

**Anchors: 66 numbers by commit, 4 by ADR, 0 by words alone.** 45
numbers reuse the anchor another lane or stage already measured for
them, 23 were measured here, and 2 are split between a reused and a
measured commit (see the table). Two depart from another lane's anchor
for a stated reason (`objectstack-ai#10629`, `objectstack-ai#10243`, under Wordings to check).

Only comments changed. Every file keeps its line count (275 lines out,
275 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 14 citations left on
changed lines: 13 resolve and 1 is a declared cross-repo reference.

**A `patch` changeset**: 54 of the 137 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: `objectql`, 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/objectql/`.

| reading | tree | board | whole-repo `allocated-but-absent` | sites |
lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `4727fcb22`, run 22:38:04Z to 22:41:42Z | enumerated,
191 pages, frontier objectstack-ai#21252, 19,073 records (newest number read before
and after the run: objectstack-ai#21252) | 592 | **136** | 136 | 17 | 50 |
| after | `8d6465457`, run 23:19:32Z to 23:22:54Z | enumerated, 191
pages, frontier objectstack-ai#21261, 19,082 records (newest before and after: objectstack-ai#21261)
| 456 | **0** | 0 | 0 | 0 |

The whole-repo drop is 136, and the two finding sets differ by exactly
the 136 rows of this package, removed; none was added. `resolves`
(34,638), `resolves-as-pull-request` (2,095) and `cross-repo-unjudged`
(1,144) did not move. The card's 135 was taken at `f11b5f20a2` with the
older extractor; the base here reads 136, the difference being
`plugin.ts` `objectstack-ai#10629`. The same census at the first base `d150c3039`
(before a fast-forward to `4727fcb22`, which touched no `objectql` file)
read the identical 136 rows. The head's only later commit is a merge of
`main` that touches no file under `packages/objectql` (`git diff
8d64654 1e08958 -- packages/objectql` is empty).

**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) and `namesThisRepository` over every tracked
file in the package (430 `.ts`, 6 `.json`, 2 `.md`, `LICENSE`), and
classifies each citation with the gate's `classifyCitation` against one
board enumerated by the gate's `enumerateBoard` (191 pages, frontier
objectstack-ai#21252, 19,073 records, 22:43:50Z). Every one of the 70 numbers in the
population was then read on its own over the issues endpoint: **all 70
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, `4727fcb22` | 9,338 | **388** | 137 | 142 | 43 | 66 |
| after, head | 9,059 | **109** | 0 | 0 | 43 | 66 |

`src comment` includes `vitest.config.ts`. The drop of 279 citations is
exactly the rewritten sites, and the live counts did not move (non-test
comment: 2,971 resolve, 80 as pull requests; test comment: 2,700 and
122). A third, raw reading (every `#` followed by 2 to 6 digits,
whatever surrounds it) counts 9,504 before and 9,225 after: the same
drop of 279.

**Comment ids.** Six distinct comment-id citations stand on 11 lines in
this package (`5237739551`, `5434929046`, `5791803339`, `5805782503`,
`5865053231`, `5865693155`). Each was read over the issue-comments
endpoint and each answers 200 (control `5941871762`, 200), so none is in
the population.

## Per-number table

`src` counts census sites (plus `vitest.config.ts` for `objectstack-ai#17853`), `test`
counts test-comment sites. Every sha below matches exactly one commit
(`git rev-parse --disambiguate`, count 1) and is an ancestor of the base
(`git merge-base --is-ancestor`, exit 0 for all 67 shas; the clone is
not shallow, 15,432 commits at the base). The message or the diff of
each one names the number it replaces, with one exception, `objectstack-ai#10629`,
explained under Wordings to check. Where a sentence credits a ruling, a
measurement 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#6037` | 2 | 1 | `18189983d` | commit | reused | validate-only data
operation — DataProtocol.validateData |
| `objectstack-ai#6083` | 3 | 2 | `53068c130` | commit | reused | ADR-0122 phase 2 —
flip bare names to parsed semantics |
| `objectstack-ai#6241` | 0 | 1 | `83a3b1f2e` | commit | reused | normalize the
`:type` segment once per handler so the plural spelling cannot skip the
§6.7 audience gate |
| `objectstack-ai#6300` | 5 | 4 | `74155c735` | commit | reused |
IDataEngine.find/findOne accept the author state — engine fills
SortNode.order's declared default |
| `objectstack-ai#6311` | 0 | 1 | `59b794f71` | commit | measured | narrow
`HookContext.api` from `z.unknown()` to the minimal `IScopedContext` |
| `objectstack-ai#6478` | 0 | 1 | `474f131cf` | commit | reused | roll `flow`'s
`allowOrgOverride` back to `false` per ADR-0005's original call, the
write path refusing loudly |
| `objectstack-ai#6483` | 0 | 9 | `ee58392e1` | commit | reused | enforce the ADR-0005
whitelist: nine unratified `allowOrgOverride: true` flags rolled back to
`false` (its message records the 2026-08-08 ruling) |
| `objectstack-ai#6573` | 5 | 6 | `708431313` | commit | measured | `registerHook`
refuses an empty `object` target and a self-cancelling scope |
| `objectstack-ai#6723` | 0 | 4 | `8ad609c69` | commit | reused | declare what
IMetadataService.getObject answers with |
| `objectstack-ai#6725` | 3 | 7 | `1507ba356` | commit | reused | MetadataFacade
object writes now reach the map its reads use |
| `objectstack-ai#6745` | 0 | 4 | `7a5ef0008` | commit | reused | pin getObject(n) =
get('object', n) across all three IMetadataService implementations |
| `objectstack-ai#8454` | 1 | 0 | `427344c26` | commit | measured | the object catalog
loses to an explicitly-set scalar |
| `objectstack-ai#8460` | 7 | 0 | ADR-0029 D9.2a | ADR | reused | ADR-0029 D9.2a, the
2026-08-13 amendment: an extender's scalar yields to a diverged base (it
names the number; executed as `01a7337fc`) |
| `objectstack-ai#8648` | 1 | 4 | `e5eeb499c` | commit | reused | pin the SEARCH-axis
remedy agreement, and correct the three comments that claimed
word-identity |
| `objectstack-ai#8672` | 6 | 4 | `ff08691e6` | commit | measured | a system-context
insert resolves the install's organization, or is refused — the runtime
producer of the autonumber fork |
| `objectstack-ai#8818` | 0 | 1 | `fd6bdf89f` | commit | reused | saveMetaItem's
missing-item refusal declares 400 INVALID_REQUEST instead of answering
500 |
| `objectstack-ai#8823` | 10 | 4 | `4dfa369a9` | commit | measured | drop the caller
value MySQL inlines in its duplicate-entry diagnostic |
| `objectstack-ai#9030` | 2 | 1 | `27a567dd8` | commit | measured | teach the
internal-leak predicate MySQL's three error templates |
| `objectstack-ai#10062` | 2 | 0 | `fa5d137ab` | commit | reused | gate undeclared
workspace imports in published src |
| `objectstack-ai#10091` | 1 | 0 | `da891e0ef` | commit | reused | gate sys_attachment
beforeUpdate with the uploader-or-parent-editor rule |
| `objectstack-ai#10165` | 2 | 1 | `801296050` | commit | reused | lifecycle
ttl.onlyWhen row filter with the canonical null predicate |
| `objectstack-ai#10194` | 0 | 3 | `2306a765c` | commit | reused | validate theme /
analytics_cube at the /meta write door via UNREGISTERED_KIND_SCHEMAS |
| `objectstack-ai#10243` | 1 | 1 | ADR-0126 §7.2 | ADR | measured | ADR-0126 §7.2: the
durable ledger row replaces the process-local `flowEnabled` map,
retiring the env-wide toggle leak's mechanism (it names the number) |
| `objectstack-ai#10290` | 4 | 1 | `2570ab05c` | commit | measured | the primary key
is never a `__search` companion source |
| `objectstack-ai#10347` | 5 | 3 | `530c1df65` | commit | reused | the Archiver
honours a declared `ttl` instead of archiving by `created_at` age alone
|
| `objectstack-ai#10485` | 0 | 2 | `35ad101bc` | commit | reused | retire the `themes`
carrier key and ThemeSchema — `app.branding` is the one colour surface |
| `objectstack-ai#10527` | 1 | 0 | `5649efbf9` | commit | reused | refuse a diverging
retention + ttl + archive lifecycle triple at parse time |
| `objectstack-ai#10528` | 4 | 1 | `7d483e1e5` | commit | measured | the Archiver
resolves its window through P4 governance |
| `objectstack-ai#10629` | 1 | 2 | `199ec4712` | commit | measured | bind federated
objects whatever the boot order, and report what could not be bound |
| `objectstack-ai#10643` | 1 | 0 | `5649efbf9` | commit | measured | refuse a
diverging retention + ttl + archive lifecycle triple at parse time |
| `objectstack-ai#10729` | 1 | 1 | `10485009a` | commit | measured | log a contributed
kind by its declared `id` |
| `objectstack-ai#11065` | 1 | 0 | `20950404c` | commit | reused | count a boolean
aggregand as 1/0 in avg and sum |
| `objectstack-ai#11311` | 1 | 0 | `1272f0a6b` | commit | reused | promote
resolveRecordOrganizationField to the shared platform-row resolver:
approvals and automation runs stamp the subject record's organization |
| `objectstack-ai#11427` | 5 | 0 | `c3c72a4bc` | commit | reused | hydrate a
tombstoned sys_file that still has a live holder |
| `objectstack-ai#11674` | 0 | 4 | `9a884c6e4` + `1cba33f16` | commit | reused | seed
pass 2 writes back by the internal id captured at insert time, healing
keyless datasets / warn at load time when a seed defers a required
column, and document the ordering constraint at the four pointer-pair
sites |
| `objectstack-ai#12194` | 0 | 2 | `311433f6b` | commit | reused | Declare the
metadata item-name grammar in spec and refuse it loudly at the publish
door |
| `objectstack-ai#13178` | 2 | 1 | `f087c376f` + `e49d98896` | commit |
reused+measured | scope the sys_file / sys_upload_session update and
delete doors to the acting organization / cut the tenant-audit control's
scope by the object's tenancy, not the caller's flag |
| `objectstack-ai#13197` | 5 | 11 | `56c093c4d` | commit | reused | enforce
field-level `unique` so a colliding write is refused, not landed |
| `objectstack-ai#13273` | 1 | 4 | `3a86a65e7` | commit | reused | pick the `find`
failure log level from the cause — "the table is not provisioned yet" is
not "the read failed" |
| `objectstack-ai#13644` | 3 | 2 | `34ce8e7db` | commit | reused | declare
ctx.referentialFieldClear on HookContextSchema, populate every set_null
cleanup write, and carry it across the QuickJS sandbox boundary |
| `objectstack-ai#13657` | 4 | 4 | `b003cf2e8` | commit | reused | Refuse an
undeclared field a before-hook writes — the post-hook half of the
declared-field door, one envelope on every driver |
| `objectstack-ai#14163` | 0 | 1 | ADR-0130 D3 + `1dcb995f2` | ADR | measured |
ADR-0130 D3: the gate relaxation and the object-name uniqueness check
are one change; `1dcb995f2` landed it, and its changeset names the
number |
| `objectstack-ai#14345` | 0 | 1 | `e89fa9233` | commit | measured | declare
aggregate? on IDataDriver with the signature the engine calls |
| `objectstack-ai#14390` | 6 | 1 | `9d7f7259f` | commit | reused | `update` answers a
driver unique violation with the `DUPLICATE_RECORD` envelope, on every
driver |
| `objectstack-ai#14399` | 3 | 8 | `3c1bbd2a8` | commit | measured | derive a view
container's object through the shared helper, so the row's own `name` is
LAST at every SOURCE registrar |
| `objectstack-ai#14422` | 0 | 1 | `dc7c226b9` | commit | reused | give the
standalone-action owner-key ladder one spelling |
| `objectstack-ai#14423` | 11 | 2 | `a56baa2bd` | commit | reused | the action audit
reads the store key and asks the plane by name, and listNames gains
loadMany fault parity |
| `objectstack-ai#14472` | 1 | 1 | `00ff228fe` | commit | measured | decide the
insert-side runtime-owned strip by hook-write provenance |
| `objectstack-ai#14474` | 1 | 3 | `df657d9df` | commit | reused | carry an ADR-0112
envelope on the install-time namespace conflict refusal |
| `objectstack-ai#14484` | 1 | 0 | `3f64fe6c6` | commit | reused | stamp
organization_id on every sys_record_share write, backfill the stranded
rows, admit the object to the tenancy ledger |
| `objectstack-ai#14535` | 0 | 1 | `1aba3159a` | commit | measured | declare the
recorded-by fixture's lookup with the canonical `reference` key |
| `objectstack-ai#14666` | 1 | 6 | `d0ee598e6` | commit | measured | refuse a view
container whose `name` disagrees with its derived object key |
| `objectstack-ai#14667` | 0 | 1 | `dc7c226b9` | commit | reused | give the
standalone-action owner-key ladder one spelling |
| `objectstack-ai#14680` | 1 | 1 | `3bd9b3498` | commit | measured | a leaf
`/view-container` subpath keeps objectql's lean ADR-0076 closure free of
the manager, chokidar, glob and js-yaml |
| `objectstack-ai#14683` | 0 | 4 | `96326040f` | commit | reused | apply the
allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are
scoped per type |
| `objectstack-ai#14723` | 1 | 0 | `65846bc46` | commit | reused | a batch/import ROW
reports a unique-constraint refusal as `UNIQUE_VIOLATION`, the route's
one wire spelling |
| `objectstack-ai#14770` | 0 | 6 | `d5cbb44f3` | commit | reused | gate
`getMetaItem`'s overlay read on the metadata registry |
| `objectstack-ai#14878` | 0 | 2 | `29db3cd2a` | commit | reused | widen the
deleted-member absence pin from one file to the tree |
| `objectstack-ai#14957` | 1 | 0 | `26144c204` | commit | measured | Derive and gate
the platform-object tenancy census |
| `objectstack-ai#15041` | 2 | 1 | ADR-0104, 2026-09-05 addendum | ADR | reused |
ADR-0104's 2026-09-05 addendum: the media column holds the bare
`sys_file` id; its execution order puts the driver card at step 2 (it
names the number) |
| `objectstack-ai#15094` | 1 | 0 | `901773b21` | commit | measured |
check-react-page-adapter-contract names its class by shape, not by the
records spelling, and re-anchors its citation |
| `objectstack-ai#16608` | 4 | 0 | `a016f08b8` | commit | reused | evaluate the
insert-side RLS `check` on the row that will be stored, after
`beforeInsert` |
| `objectstack-ai#16711` | 1 | 0 | `7862fb711` | commit | measured | object-definition
parameters declare the keys they are read for, plus a gate that sees
subclass overrides |
| `objectstack-ai#16729` | 1 | 0 | `0f38ab084` | commit | measured | an explicit
tenancy opt-out survives a partial `syncSchema` re-registration |
| `objectstack-ai#16783` | 1 | 1 | `854639b31` | commit | reused | `findOne`, `update`
and `delete` declare what they answer, and their hook seams are guarded
|
| `objectstack-ai#16786` | 3 | 2 | `5c8f5af50` + `6059b29c0` | commit |
measured+reused | `ObjectRepository` declares the `findOne` / `update`
shapes it already published / declare
IScopedObjectRepository.updateById's answer — the record or null, not
any |
| `objectstack-ai#16805` | 2 | 0 | `a016f08b8` | commit | reused | evaluate the
insert-side RLS `check` on the row that will be stored, after
`beforeInsert` |
| `objectstack-ai#17195` | 1 | 0 | `d2c1d1980` | commit | reused | beforeUpdate
receives the persist image; the caller submission moves to ctx.submitted
|
| `objectstack-ai#17219` | 3 | 2 | `706ad0fcc` | commit | reused | name the withheld
read-only key when a hook faults reaching through it |
| `objectstack-ai#17853` | 1 | 0 | `08f5f0e5a` | commit | reused | make a vitest
filter that selects no test file say so |

## 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), the form the landed stages use. These say more than the tag:

- **`objectstack-ai#10629`, three sites** (`plugin.ts:1543`,
`skip-schema-sync-registers-object-metadata.test.ts:22`, `:153`): 「the
same ruling objectstack-ai#7737/objectstack-ai#10629 made for federated objects」 became 「the same
ruling objectstack-ai#7737 made for federated objects (commit 199ec47)」. `199ec4712`
is objectstack-ai#7737's fix, and its message states the ruling the sites paraphrase:
`OS_SKIP_SCHEMA_SYNC` is a DDL flag while the federated binding is
DDL-free, and the binding is reconciled on `kernel:ready`. Its message
and diff name objectstack-ai#7737, **not objectstack-ai#10629**: the only commit that names objectstack-ai#10629
as its own (`13a6cb4ad`, the runtime lane's anchor for it) is an
expected-log-noise capture, a different subject, and `a037f7cbd`, which
wrote the 「objectstack-ai#7737/objectstack-ai#10629 ruling」 phrase, records nothing objectstack-ai#10629 added. So
the dead number is dropped and the live objectstack-ai#7737 carries the citation,
beside the commit that decided it. The same pair stands in `driver-sql`
(`sql-driver.ts`), the next stage.
- **`objectstack-ai#10243`, two sites** (`action-activation.ts:185`,
`action-activation.test.ts:23`): 「the objectstack-ai#10243 mechanism ADR-0126 retires」
became 「the env-wide toggle leak's mechanism ADR-0126 §7.2 retires」.
ADR-0126 §7.2 names that mechanism (the process-local `flowEnabled` map)
and the number (「the objectstack-ai#10243 leak's mechanism」), so ruling C's ADR rung
applies; the dogfood lane anchored a similar sentence to `02b41232d`,
the measurement commit.
- **`objectstack-ai#8672`, ten sites**: objectstack-ai#8672 was an observation that no commit fixed.
`ff08691e6` is the first in-repo record of its reasoning: its diff
quotes 「an org-less row is defensible for `sys_permission_set`」 and
names objectstack-ai#8672 five times. So 「objectstack-ai#8672's reasoning」 became 「commit
ff08691's (recorded) reasoning」, and 「objectstack-ai#8672 measured this primitive」
(`system-write-organization.test.ts:347`) became 「The card commit
ff08691 cites measured this primitive」.
`system-write-organization.test.ts:117` quotes what the file used to
read, 「platform namespace ⇒ deliberately org-less (objectstack-ai#8672)」; the quoted
number is elided to 「(…)」 rather than re-spelled, so the quote stays
true.
- **`objectstack-ai#13178`'s census figure** (`engine.ts:5450`,
`tenancy-by-object-classification.test.ts:26`): 「objectstack-ai#13178 census measured
… 135 of 175」 became 「census cited in commit e49d988's message
measured … 135 of 175」. That message carries the figure; see Acceptance
notes for what `4ecafc78b` records about it.
`platform-object-tenancy.ts:144` (the `sys_upload_session` update
writer) takes `f087c376f`, the service-storage lane's anchor.
- **`objectstack-ai#15094`** (`find-hook-result-shape.ts:30`): 「the ~70 … normalizer
limbs the objectstack-ai#15094 census counted」 became 「… limbs a census counted
(commit 901773b records its band)」. `901773b21` wrote the header of
`scripts/check-react-page-adapter-contract.mjs`, which records that
census (104 blocks at `ca46f8f12`) and a re-measure band; it does not
restate 「~70」, so the sentence points at the band and claims nothing
more.
- **`objectstack-ai#16805`** (`engine.ts:13014`, `:13218`): 「the contract review of PR
objectstack-ai#16805 measured」 became 「the contract review commit a016f08 records
measured」. `a016f08b8` is that pull request's squash, and its message
records the review's finding.
- **`objectstack-ai#16786`**: the `objectql` half (`engine.ts:18230`, `:18264`, two
tests) takes `5c8f5af50`, the commit that declared `ObjectRepository`'s
`findOne` / `update` shapes; `engine.ts:18272`'s 「stays open on objectstack-ai#16786」
became 「(its spec half: commit 6059b29)」, the spec lane's anchor,
since that half has landed.
- **`objectstack-ai#14163`** (`registry-ownership-refusal-envelope.test.ts:11`):
`(ADR-0130 D3, commit 1dcb995)`. The cited thing is the install-time
object-name check; ADR-0130 D3 decides it, and `1dcb995f2` landed it
with a changeset naming objectstack-ai#14163.
- **`objectstack-ai#8460`** (seven `registry.ts` sites): ADR-0029 D9.2a, the
2026-08-13 amendment, which names objectstack-ai#8460; the dogfood and spec lanes used
the same. `:2508`'s 「every shape objectstack-ai#8460 measured」 became 「every shape the
ADR-0029 D9.2a amendment records」.
- **`objectstack-ai#15041`** (`engine.ts:7164`, `:9860`, one test): 「the ruling on
objectstack-ai#15041 step 2」 became 「sequencing step 2 of ADR-0104's 2026-09-05
addendum」, the cli lane's spelling of the same record.
- **`objectstack-ai#6241`** (`metadata-service-roundtrip-conformance.test.ts:41`):
「the gate's header carries objectstack-ai#3984/objectstack-ai#5881/objectstack-ai#6241」 became 「… carries objectstack-ai#3984,
objectstack-ai#5881 and the third bypass, fixed in commit 83a3b1f」, because that
header (`scripts/`, another lane's surface) still carries the number
itself.
- **Tense, where the anchor is past**: 「the phantom read objectstack-ai#14770 removes」
became 「… commit d5cbb44 removed」 (`protocol-meta.test.ts:100`) and
「the resurrection objectstack-ai#14683 closes」 became 「… commit 9632604 closed」
(`:169`), as stage 1 wrote it. `:181`'s backticked `` `objectstack-ai#14770` `` became
plain 「commit d5cbb44」; its message records the four raw-org callers
the line says it measured.
- **Card antecedents**: 「the whole subject of objectstack-ai#14423」 became 「the whole
subject of the card commit a56baa2 closed」
(`action-governance.ts:406`); 「the C4 cell objectstack-ai#14423's ruling left open」
became 「the C4 cell commit a56baa2 left open」
(`plugin-governance-scoped-metadata.test.ts:5`; that commit pins C4 as
「a BOUNDARY, not a defect」); 「route 3 of objectstack-ai#8648」 became 「route 3 of the
card commit e5eeb49 fixed」
(`query-expression-conformance.test.ts:1070`, a line that commit wrote);
「filed objectstack-ai#6573」 / 「objectstack-ai#6573's ruling」 became 「filed the card commit 7084313
closed」 / 「The decision commit 7084313 records」
(`hook-exclude-objects.test.ts:501`, `:502`); 「(objectstack-ai#11674, the card's
"Second, NOT measured" question)」 became 「(the "Second, NOT measured"
question on the card commit 9a884c6 fixed)」; 「measurement (objectstack-ai#13644)」
became 「measurement (on the card commit 34ce8e7 closed)」.
- **Other single rewrites**: 「and objectstack-ai#6573 is why」 became 「and commit
7084313 says why」 (`engine.ts:2433`; its message gives the reason);
「question at this line (objectstack-ai#10527), since decided」 became 「question at this
line, since decided by that commit」 (`lifecycle-service.ts:1294`, where
`:1293` now cites `5649efbf9`); 「objectstack-ai#14680 is what they cost」 became 「the
leak commit 3bd9b34 closed is what they cost」
(`core-boundary.ratchet.test.ts:52`); 「The objectstack-ai#10165 acceptance criterion」
became 「The acceptance criterion behind commit 8012960」.
- **Reference lists**
(`metadata-service-getobject-equivalence.test.ts:51`,
`metadata-service-roundtrip-conformance.test.ts:68`,
`metadata-facade.test.ts:101`): the dead numbers became their commits,
in the spec lane's 「commits SHA (what), …」 form; the live numbers in
those lists stay, `PR objectstack-ai#7211` beside its `1507ba356`.
- 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: 43 test-string sites, 25 numbers, 23 files**
(describe and `it` titles, assertion arguments): `objectstack-ai#14422` 4, `objectstack-ai#13273` 3,
`objectstack-ai#13657` 3, `objectstack-ai#17219` 3, `objectstack-ai#11674` 3, `objectstack-ai#6573` 3, `objectstack-ai#14423` 2, `objectstack-ai#10165` 2,
`objectstack-ai#14535` 2, `objectstack-ai#10290` 2, `objectstack-ai#8672` 2, and 14 more once each. Every one of
the 25 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 66 sites (44 numbers); left. `test-typecheck-debt.json`,
`tsconfig.test.json` and `tsconfig.scripts.json` cite only live numbers.

## Mechanical guard: no code token moves

The guard (stage 2's) compares, base `4727fcb22` against the working
tree, over all 67 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.
- **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: 298,707 base tokens, **0 files with a token
change** (exit 0).
- Comment control (「The defaulting」 to 「The DEFAULTING」 on
`engine.ts:5`): 0 files changed (exit 0).
- Positive control, an identifier (`ARCHIVE_BATCH_SIZE` to
`ARCHIVE_BATCH_SIZEX`, `lifecycle-service.ts`): DIFFER on both readings
(exit 1).
- Positive control, a string literal (`'[value redacted]'` to `'[value
redactedX]'`, `driver-fault-redaction.ts`): DIFFER on both readings
(exit 1).
- Positive control, a numeric literal (`ARCHIVE_MAX_BATCHES_PER_SWEEP =
20` to `21`): DIFFER on both readings (exit 1).

Each mutation went through `scripts/ablation-replace.mjs` (wrap mode)
under a shell trap that restores by absolute path from `HEAD`. Each
landed (anchor 1 to 0, blob changed), and each restore was proven equal
to its `HEAD` blob (`71f6c9268aeb`, `34b1dd7989b8`, `9594cdd593c8`),
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 dependency closure was built first (`turbo run build
--filter='@objectstack/objectql^...'`, 13 tasks). 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 `9d6a0a8d6`: 14 `dist` files hashed. Of the 137
rewritten non-test lines, 54 appear verbatim in `dist`: 25 from
`engine.ts`, 7 from `action-governance.ts`, 5 from
`platform-object-tenancy.ts`, and 17 from ten other files; in
`index.d.ts` / `index.d.mts`, the shared `util-*.d.ts` chunk, and
`index.js` / `index.mjs` / `core.js` / `core.mjs`.
- **Leg 2**, the base text put back in the 18 non-test files (18 of 18
proven equal to their base blob): 10 of the 14 files differ from leg 1
(`core` and `index` in `.d.ts`, `.d.mts`, `.js`, `.mjs`, and the
`util-*` chunk's two `.d` files).
- **Leg 3**, after the proven restore (18 of 18 equal to their `HEAD`
blob, `git diff HEAD` empty): all 14 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-objectql-provenance-anchors.md` declares a `patch` for
`@objectstack/objectql`, comment text only, with the claim's `Clause-②:
no` line.

## Gates (head `1e0895870d`)

- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `1e0895870d` (68 paths against
merge base `8dea55d31`; no stale-tree warning) derived 68 commands. All
68 ran, each exit code captured before any pipe: 68 exit 0. `--ran`
reports 「68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN」 (a derived zero)
and exits 0. A full `turbo run build` over `./packages/*` and
`./packages/*/*` ran first under the verify lock (71 of 71 tasks,
VERDICT command-exit 0), so no gate read an unbuilt workspace.
- **Named in the dispatch:** `node scripts/check-issue-citations.mjs`
exits 0 (「every citation this change adds resolves (or is a declared
cross-repo reference)」, 14 judged across 17 files); `pnpm
check:issue-citations` exits 0 (self-test); `pnpm check:doc-authoring`
exits 0 (the sibling-package prose-id baseline holds, no growth); `pnpm
check:nul-bytes` exits 0, and a raw scan of the 68 changed files for
control bytes finds none.
- **Tests and typecheck, under the verify lock, at `1e0895870d`:** `pnpm
--filter @objectstack/objectql test`: 360 test files and 7,082 tests
pass. `pnpm --filter @objectstack/objectql typecheck` exits 0; its
`check:test-typecheck` step compiles all 361 tracked test files under
`tsconfig.test.json` (`tsc --listFiles`), the ledger holding (40 files,
234 errors, 65 pinned signatures).
- **Lint, as a proven narrowing:** eslint with inline config disabled,
over the 67 touched `.ts` files plus `dist/index.js` as the control: 68
results, 0 errors and 1 warning, the control's ignore notice; none of
the 67 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 `d150c3039`, fast-forwarded to
`4727fcb22` before any edit (both census readings of this package
agree), and merged with `main` once at `8dea55d31` before the gates;
that merge touched no file under `packages/objectql` and neither
`check-issue-citations.mjs` nor `dispatch-gates.mjs`. Tests, typecheck
and gates ran on the merged head.
- **The `135 of 175` figure.** `engine.ts:5450` and
`tenancy-by-object-classification.test.ts:26` state that census figure
as measured. `4ecafc78b`, which re-derived the tenant-audit census as an
in-tree artifact after objectstack-ai#13178 became unreachable, records that 「the
135/77% "silenced by the isSystem guard" figure has no surviving
corroboration and is not reproduced」. This stage moves the citation to
the commit whose message carries the figure and leaves the claim as
written; whether those two sentences should say so is outside a citation
sweep.
- **A board enumeration came back short, at exit 0.** One run of the
gate's own `enumerateBoard`, between two full ones, returned 91 pages,
9,000 records and frontier objectstack-ai#9389, and raised nothing; the runs either
side of it read 191 pages and frontier objectstack-ai#21252. Its only guard is a
zero-record check. This stage discarded that reading and re-enumerated;
nothing above rests on it. Noted only.
- **Wording only:** no line without a number was changed, except the
lines whose antecedent was the number itself, listed above.

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

---------

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

2 participants