Skip to content

fix(objectql, driver-sql, spec): a stored expression that reads an autonumber stores the value computed with the number - #25218

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-m25d8w
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-m25d8w

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

Fixes #25073

Clause-②: yes (widening)
Metadata-protocol: none

Ruling-ref: 6335312761 (letter A). Contract review: CONTRACT_REVIEW_TIER, run by the engine seat. Cross-lane declaration to domain:spec on #6017.

What changes

A stored field whose expression reads an autonumber (record.case_number + " - " + record.subject, or has(record.product_code) ? … : record.name) was computed before the number existed, so an insert stored null or the has() fallback until a later update by id. Now the insert computes it again right after the number is issued, and the row is written once, carrying both.

  • IDataDriver.issueAutonumbers?(object, rows, options?, afterCollision?) (packages/spec/src/contracts/data-driver.ts), the one optional member. Issue mode fills a row's empty autonumber slots in place from the driver's own counter and answers the filled names per row. After-collision mode judges whether a create() / bulkCreate() error is a unique violation on a number it issued to those row objects; if so it re-seeds each stale counter once, empties the slots drawn from it and answers their names. Otherwise (a value the caller typed, a non-unique error, inside a caller transaction) it answers empty lists and changes nothing.
  • SqlDriver.issueAutonumbers is fillAutoNumberFields on request (organization stamped first, on a view, as create() does) plus create()'s own Autonumber counter neither syncs to MAX(existing) per tenant nor re-checks on collision — warm-DB creates 409 in bursts, each failure burning a number (25 retries observed) #5495 / bulkCreate / upsert 仍会逐次烧号:#5495 的重播种只落在 create(),批次语义是独立决策 #6943 re-seed split where the caller must go on (collidingAutoNumberReservations, resyncSequenceToDataMax, autoNumberCounterKey). driver-sqlite-wasm and driver-turso inherit it; Turso's REMOTE_FACE_ANSWERS names it inherited (the driver-turso remote: inherited SqlDriver methods the remote face does not override answer from the placeholder :memory: Knex database — introspectSchema() returns no tables, distinct() a 500, findWithWindowFunctions() a raw SQLite error #20055 compile-time table requires a row for every public SqlDriver member).
  • The engine (packages/objectql/src/engine.ts). At the point applyAutonumbers already runs (after validation, so a rejected row burns no number):
    • it plans the stored fields that read an autonumber, directly or through another stored field (storedExpressionsReading, using formula's analyzeRelationshipTraversals, the one member reader);
    • where the engine issues (supports.autonumber absent: driver-memory, driver-mongodb), it computes them again after applyAutonumbers;
    • where the driver owns the counter and the object has such a field, it calls driver.issueAutonumbers per live row, computes them again, and hands create() / bulkCreate() the filled row: one INSERT, no transaction. Objects with no such field keep the driver's issue-at-write path unchanged;
    • createWithAutonumberResync is the one single-row retry loop for both issuers: on a driver-issued number the member judges the error and re-seeds, the loop re-issues through the member and computes the readers again, under the same AUTONUMBER_COLLISION_ATTEMPTS;
    • a batch collision on driver-issued numbers asks the member to re-seed and is not re-issued (the engine's existing batch policy), raising DUPLICATE_RECORD;
    • a driver that owns its counter without the member: the number stays inside create(), and the engine says so once per object at error level with the remedy;
    • no instanceof SqlDriver or driver-name branch anywhere: the member's presence and supports.autonumber decide.
  • ObjectQL.registerInsertRecomputeListener(id, fn): called per row after that second computation (and after each re-issue), on the row the driver is handed. plugin-pinyin-search registers its existing stampCompanion on it, narrowed to the recomputed fields, so the __search companion is stamped from the value carrying the number. ⚠️ Approved file-surface extension: packages/plugins/plugin-pinyin-search/src/companion-projection.ts and this engine method were outside the dispatched surface. The seat decided (A) on the card thread on 2026-10-22, recorded on objectql: a stored field's expression is computed on insert before the autonumber is issued, so an expression that reads the record number stores null (or its has() fallback) until a later update by id #25073: ruling item 4 binds the same-step recompute, no new HookEvent name, an additive plugin-facing registration in the registerWriteGateProbe family.

Measurements

shape before (base, #25073's probe) after (this branch)
SQLite, concat title display_title: null "CASE-00001 - one"
SQLite, has() title "Probe product" "PRD-0001 Probe product"
driver-memory, concat title null "CASE-00001 - one"
__search, concat title as nameField (SQLite and memory) null; $search "zhangwei" 0 hits, "case00001" 0 hits "case00001zhangwei case00001zw"; 1 hit each
__search, has() title "chabei cb" "prd0001chabei prd0001cb"

The __search "before" column was measured on this branch before the listener existed (the number in the title, the companion still stamped in beforeInsert from the pre-number value).

Turso remote face, with the inherited member (turso-remote-autonumber-issue-ahead.test.ts, a real SQLite behind the @libsql/client interface): the member is not redeclared on TursoDriver; it issues from the counter the remote create() draws from (last_value 1 → 2, create() of the issued row leaves it at 2); after rows 2..30 land by a bypass path, the collision call re-seeds the remote counter to 30 and empties the slot, and the next issue writes CASE-00031; a duplicate on a typed value answers nothing. No fork.

Pins and ablations

file what result
driver-sql/src/sql-driver-autonumber-issue-ahead.test.ts issue (same counter as create(), per organization from options, typed value untouched, batch alignment); after collision (re-seed + empty, typed duplicate, non-unique error, caller transaction, batch per counter) 9/9
objectql/src/engine-autonumber-stored-expression.test.ts both issuers over hand-rolled drivers: single, batch (one bulkCreate), partial mode, has(), forced collision, listener per row and per re-issue; controls (expression reading no number, update by id); driver-only: member not asked for an object with no reader, batch collision re-seeds and does not re-issue, exhaustion ERR_AUTONUMBER_COLLISION, no-member error once 20/20
runtime/src/autonumber-stored-expression.integration.test.ts real SqlDriver on better-sqlite3: single (one create, one number drawn), batch (one bulkCreate), partial (dead row burns none), has(), forced single collision, batch collision (nothing lands, re-seed, next past the store), afterInsert sees the value, inside a caller transaction; controls (object with no reader keeps issue-at-write, update by id) 10/10
plugin-pinyin-search/src/companion-autonumber-recompute.test.ts real engine, both issuers: concat and has() companions carry the number, $search finds by pinyin and by number, batch; controls (companion source not recomputed, object with no companion column) 10/10
driver-turso/src/turso-remote-autonumber-issue-ahead.test.ts (+ turso-remote-inherited-members.test.ts) the remote face measurement above 97/97

Ablations, each through scripts/ablation-replace.mjs (mutation proved on disk, restored to HEAD):

  • Remove the recompute (recomputeAutonumberReaders returns at entry), objectql suite: 13 red (every autonumber pin on both issuers, plus the batch-collision pin's next title), 7 green (the 4 controls, member-not-asked, exhaustion, no-member).
  • The same, through dist for the runtime suite (ablation-dist-preflight.mjs saw the marker in 4 built files): 8 red, 2 green (the two controls).
  • Unregister the listener in companion-projection.ts, plugin suite: 6 red (the companion pins, both issuers), 4 green (the controls).

Acceptance notes

  • "One retry loop for every driver", read as the single-row loop. A single-row collision on a driver-issued number goes through the engine's createWithAutonumberResync, re-issuing through the member, for every driver. The batch path keeps the engine's existing "re-seed, never re-issue" policy for driver-issued numbers too, because the engine cannot know whether a driver's bulkCreate is all-or-nothing (Turso's remote bulkCreate writes row by row), and re-issuing a partly landed batch duplicates rows. And the engine issues through the member only for objects with a reader: other objects keep SqlDriver's own issue-at-write and its in-driver bulkCreate / upsert 仍会逐次烧号:#5495 的重播种只落在 create(),批次语义是独立决策 #6943 batch heal, which the engine could not take over without re-issuing batches. Reviewer's call if the ruling meant more.
  • What a refused insert reports moved on objects with a reader: a SQL collision that survives every retry now leaves as ERR_AUTONUMBER_COLLISION after the engine's 3 attempts, where the driver's own loop used to retry and the engine then raised DUPLICATE_RECORD. Named in the changeset.
  • Validation and the RLS write seam judge the pre-number value of a reader field: they run before the number is issued, by the ruling's order (a rejected row burns no number). A rule over such a field sees null / the fallback.
  • Upsert is not covered: engine.upsert → driver.upsert still issues inside the driver.
  • Turso remote bulkCreate partial application is pre-existing (row by row) and unchanged.
  • Inside a caller transaction the member answers no collision, as create() retried nothing there (P4 unchanged): the caller sees DUPLICATE_RECORD.
  • A driver that owns its counter but issues nothing ahead (a third-party driver without the member) keeps today's stored value, now reported once per object at error.
  • Changeset level is patch, not minor: AGENTS.md Post-Task step 3 sets the level from the Metadata-protocol kind alone (none ⇒ patch), the precedent being IEmailService declares no "does this transport deliver mail" fact, so a consumer cannot tell plugin-email's default log transport from a live one (blocks #25060, ruling 1B's precondition) #25065's optional IEmailService member. The dispatch said minor where the contract widens.

Gates

Local, at 800779bd (the last commit before merging main):

  • dispatch-gates --commands derived 98 families; all 98 run, exit codes recorded before any pipe; --ran: 98 run, 0 NOT-MEASURED, 0 UNRUN.
  • Four were non-zero on the first pass and are 0 on rerun: check-engine-split-ratio --days 90 and check-plugin-teardown-shape --self-test refused on the shallow clone (history deepened, then 0); check:dual-build-cjs-loads refused without a whole-workspace build (0 after one); check:query-options-erasure caught one as any on a find query in the new plugin test (typed in 800779bd, then 0).
  • pnpm --filter @objectstack/spec check:generated: 0, all 14 artifacts current.
  • check:driver-conformance: 0 at the base and at the head, 55 covered cells and 0 in the DEBT ledger both times.
  • typecheck: 0 for spec, objectql, driver-sql, driver-turso, plugin-pinyin-search, runtime.

The rerun on the merged head and the dogfood battery are reported in the os-dev-report on #25073.


Generated by Claude Code

…rite for stored expressions that read it

Claude-Session: https://claude.ai/code/session_01LMugqQAMxZFLH5rBUFsFHC
Co-authored-by: Claude <noreply@anthropic.com>
… the search companion; issue numbers per organization; pins

Claude-Session: https://claude.ai/code/session_01LMugqQAMxZFLH5rBUFsFHC
Co-authored-by: Claude <noreply@anthropic.com>
… record number carries it

Claude-Session: https://claude.ai/code/session_01LMugqQAMxZFLH5rBUFsFHC
Co-authored-by: Claude <noreply@anthropic.com>
…ver SQLite; the inherited member on the Turso remote face

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/driver-sql, @objectstack/driver-turso, @objectstack/objectql, @objectstack/plugin-pinyin-search, @objectstack/spec, touching 21 documentable anchor(s).

12 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 IDataDriver (symbol, a top-level interface), SqlDriver (symbol, a top-level class))
  • content/docs/data-modeling/queries.mdx (via IDataDriver (symbol, a top-level interface), SqlDriver (symbol, a top-level class))
  • content/docs/kernel/contracts/data-engine.mdx (via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/index.mdx (via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/services.mdx (via IDataDriver (symbol, a top-level interface))
  • content/docs/permissions/tenant-audit-census.mdx (via SqlDriver (symbol, a top-level class), execCtx (symbol, a field of interface AutonumberReaders))
  • 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 IDataDriver (symbol, a top-level interface), SqlDriver (symbol, a top-level class))
  • content/docs/protocol/objectql/types.mdx (via SqlDriver (symbol, a top-level class))

⛔ 4 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-3.mdx (via IDataDriver (symbol, a top-level interface))
  • content/docs/releases/v17/17-5.mdx (via IDataDriver (symbol, a top-level interface), SqlDriver (symbol, a top-level class))
  • content/docs/releases/v17/17-6.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 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 72 pages)
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 149 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 15764cd723e750c01416cfe91b44bbd0a4c28f37 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 15764cd723e750c01416cfe91b44bbd0a4c28f37

⚠️ 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 15764cd723e750c01416cfe91b44bbd0a4c28f37 → 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: 54971471cc12809500c7f7c5c3afa8e2e021892f
Local-runs: none
Scope: packages/spec/src/contracts/data-driver.ts + packages/objectql/src/engine.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-turso/src/turso-driver.ts, packages/plugins/plugin-pinyin-search/src/companion-projection.ts, packages/drivers/driver-sql/src/sql-driver-autonumber-issue-ahead.test.ts, packages/drivers/driver-turso/src/turso-remote-autonumber-issue-ahead.test.ts, packages/objectql/src/engine-autonumber-stored-expression.test.ts, packages/plugins/plugin-pinyin-search/src/companion-autonumber-recompute.test.ts, packages/runtime/src/autonumber-stored-expression.integration.test.ts, .changeset/25073-autonumber-stored-expression.md; cited code read at the head: packages/spec/src/data/driver.zod.ts (DriverOptions, DriverInterfaceSchema), packages/spec/src/data/hook.zod.ts (unchanged), packages/spec/src/api/error-code-ledger.zod.ts, packages/drivers/driver-turso/src/turso-remote-inherited-members.test.ts, packages/drivers/driver-turso/src/index.ts, packages/objectql/package.json.

Reviewed 2026-10-22T14:25Z on card #25073, ruling 6335312761 (letter A), the seat's surface call 6340248712, the cross-lane declaration on #6017 (6339724909), the dev report 6342739367, the PR body with its Acceptance notes, the net diff against the merge base 15764cd723e750c01416cfe91b44bbd0a4c28f37, and the 42 check-runs on this head (every one success or path-skipped; Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard all success). Read-only: nothing built, run or re-run.

① Derived judgments

(a) The ruling's premise and the insert ordering — right, on every path. At this head the insert body's second stored-expression pass (computeStoredExpressions, engine.ts near 17136) still runs before validation; the new issue loop sits exactly where applyAutonumbers already ran, after validation (near 17264), so a rejected row burns no number, in partial mode too (dead rows are skipped at issue and at recompute). recomputeAutonumberReaders (near 17284) then recomputes over every live row that received a number from either issuer and tells the listeners; createWithAutonumberResync (near 8794) recomputes after each re-issue under the same AUTONUMBER_COLLISION_ATTEMPTS (3); the batch door re-seeds (the engine's counters, or the member on driver-issued numbers, near 17343) and never re-issues, then throws the DUPLICATE_RECORD envelope — the engine's pre-existing #6806 policy, now symmetric across issuers. One retry loop: SqlDriver.create / bulkCreate find the slot filled (fillAutoNumberFields skips a non-empty slot) and reserve nothing, so their own #5495 retry cannot fire on a pre-issued number and the engine's loop is the only loop on that path. Row identity holds where the member needs it: liveRows maps to the same rows[i] objects the member keyed its reservations on (a weak map by row object), the collision call passes those same objects, and create() copies into toInsert only afterwards. Transitive readers: storedExpressionsReading closes over record.x reads through analyzeRelationshipTraversals with a fixpoint loop, counts an entry whose reads it cannot name as a reader (conservative), and applyFormulaPlan folds entries sequentially in plan order, so a reader-of-a-reader sees the fresh value (pinned as headline on fakes and on real SQLite, after a collision too). Measured on both issuers over hand-rolled drivers (objectql), on real SQLite through SqlDriver (runtime, including afterInsert and a caller transaction), and on the Turso remote face.

(b) The IDataDriver member — right, optional, complete. issueAutonumbers?(object, rows, options?, afterCollision?) answering a promise of per-row filled-field-name lists; afterCollision is unknown, not any. The TSDoc states both modes, the row-identity requirement, the typed-value and caller-transaction silences, and the two omission cases. Absence is handled on every driver class: no supports.autonumber (driver-memory, driver-mongodb) ⇒ the engine issues as before and now recomputes; supports.autonumber without the member (a third-party driver) ⇒ the number stays inside create() and the engine reports once per object at error with the consequence and the remedy — the right level by AGENTS.md's one question (the write reports success while the stored value is not what its declaration says), pinned. Detection is typeof driver.issueAutonumbers === 'function' plus supports.autonumber; no instanceof SqlDriver, no driver-name branch anywhere in the diff. Clause-②: yes (widening) names every exported change: IDataDriver.issueAutonumbers (spec), SqlDriver.issueAutonumbers (driver-sql), ObjectQL.registerInsertRecomputeListener (objectql), the plugin's registration (plugin-pinyin-search), and the error-code change on the exhausted path; REMOTE_FACE_ANSWERS is not re-exported from driver-turso's index, so its new row is internal. hook.zod.ts is untouched; DriverInterfaceSchema mirrors the core verbs only and is not widened, consistent with the other optional members. The member is declared to domain:spec on #6017 as ruling item 6 asks.

(c) The __search route — right, in the approved family. registerInsertRecomputeListener(id, fn) sits beside registerWriteGateProbe and registerEffectiveObjectPermissionsResolver on the ObjectQL class (none of the three is on a spec interface), keyed by registrant id so a re-bind replaces itself, feature-detected by the plugin through its MinimalEngine shape. No new HookEvent name; no package-name branch in engine code (the plugin is named only in a docblock). The plugin's listener re-stamps the companion onto the very row the driver is handed, narrowed to the recomputed fields: a companion whose source was not recomputed keeps its beforeInsert stamp (control pinned), an object without a companion column gets none (control pinned), it fires again on every re-issue (pinned on the engine), and stampCompanion never throws. $search finds the record by pinyin and by its number on both issuers.

(d) The dev's deviations, each judged:

  • (1) the __search extension — inside ruling item 4 (a search column without the number is the same defect in another column, recomputed in the same write step) through the seat's call 6340248712, in the approved family, no HookEvent, no package-name branch; the plugin rides the one changeset at patch. Inside.
  • (2) the forced one-row edit to REMOTE_FACE_ANSWERS (issueAutonumbers: 'inherited') — forced by the table's satisfies clause over every key of SqlDriver (turso-driver.ts near 1126): a public member on SqlDriver is a required key and the DTS build fails without it. The value is measured right: turso-remote-autonumber-issue-ahead.test.ts pins not-redeclared, issue from the remote counter the remote create() draws from, collision re-seed to 30 and the slot emptied, CASE-00031 next, and silence on a typed duplicate; turso-remote-inherited-members.test.ts holds every inherited row to the code. Ruling item 5 asked for exactly this measurement and named a non-inheriting face as the fork; this face inherits. Declared in the PR body and the report; fix(plugin-security): a controlled_by_parent detail's read scope is the engine's ReadableIds leaf over its master (ADR-0139 D6, seventh consumer); Q2 A in service-analytics; the Turso remote nested leaf #25198's region is disjoint and the merge was clean. Justified.
  • (4) "one retry loop for every driver" read as the single-row loop — the ruling names "the engine's existing createWithAutonumberResync", which is the single-row loop by construction; the batch door's "re-seed, never re-issue" is the engine's pre-existing policy, now applied to driver-issued numbers through the member (re-seed by resyncSequenceToDataMax, stale slots emptied, batch not re-issued, the next insert past the store — pinned on fakes and on SQLite). Issuing ahead only for objects with a reader follows ruling item 1's purpose clause ("recomputes the stored fields whose expression reads an issued autonumber") and leaves every other object's path, error code and in-driver bulkCreate / upsert 仍会逐次烧号:#5495 的重播种只落在 create(),批次语义是独立决策 #6943 batch heal byte-identical; the literal reading would have carried the error-code change to every autonumber object with no measured producer and dropped a heal the ruling never weighed. Inside ruling A as the conservative reading, declared in the Acceptance notes. If the maintainer meant every autonumber object to issue ahead, that is a follow-up ruling, not a defect of this head.
  • (5) ERR_AUTONUMBER_COLLISION on an exhausted SQL collision for reader objects — the direct consequence of ruling item 1 moving the retry into the engine's loop, whose exhaustion exit has always been this code on the engine-issued path (driver-memory, driver-mongodb); the attempt count is comparable (3 engine attempts against the driver's 4), and every object without a reader is unchanged. Inside the ruling; named as a published behaviour change in the changeset ("One change in what a refused insert reports") and in the PR body; a ledger-registered code under @objectstack/objectql. Level: not a metadata-protocol kind, so patch under AGENTS.md Post-Task step 3. Right.

② Semver level

patch on @objectstack/spec, @objectstack/objectql, @objectstack/driver-sql, @objectstack/driver-turso and @objectstack/plugin-pinyin-search: all five are private: false and each is moved by the diff; driver-sqlite-wasm inherits with no diff and runtime moves only a test, so neither is owed one. AGENTS.md Post-Task step 3 sets the level from the kind alone — Metadata-protocol: none takes patch, and "a non-protocol change, widening or breaking, ships patch" — so the dispatch's minor conflicts with AGENTS.md, which wins. Clause-②: yes (widening) and Metadata-protocol: none are line-initial in both the PR body and the changeset body; the widening is a new optional contract member plus a new engine registration method, so the accept set grows and nothing an author or a driver could write is refused. Check Changeset is green on the head. Right.

③ Boundary flags

  • Dev deviations 1–4 (surface extension; forced Turso row; patch not minor; the retry-loop reading): answered in ① — each inside the ruling or settled by AGENTS.md.
  • Deviations 5 and 6 (the attribution link points at the card thread; no review request on the PR): outside the contract face; noted, nothing owed here.
  • Deviation 7 (merged main before the final gate rerun): the net diff is read against the merge base 15764cd7, the local families were rerun at the merged head, and the check-runs on 54971471c are the verdict — all green.
  • Acceptance note — validation and the RLS write seam judge the pre-number value: by ruling items 1 and 3 (the order binds; a rejected row burns no number); no card owed.
  • Acceptance note — upsert is not covered: the card and the ruling are the insert path. By reading, engine.upsert → driver.upsert is the same defect class on an upsert that inserts, and it is unmeasured. Escalated to the seat: probe it and file on a measured reproduction (filing gate ①); not this PR's.
  • Acceptance note — inside a caller transaction the member answers no collision: matches the driver's own mayRetry today and the ruling's carried confidence gap (a caller-owned PostgreSQL transaction aborts on a failed INSERT); pinned in driver-sql's test.
  • Acceptance note — Turso remote bulkCreate partial application: pre-existing and unchanged; the batch policy above is what protects it.
  • Acceptance note — a driver without the member keeps today's stored value, at error once per object: right level, pinned.
  • open_questions: none.
  • Test evidence: the report gives per-file pass counts and ablation red/green pinned to this head, which reconcile with the files (objectql 2×8 + 4 = 20 with 7 green = 4 controls + member-not-asked + exhaustion + no-member; runtime 10 with 2 controls; plugin 2×5 with 4 controls); the dogfood battery is pinned to the head; the check-runs are the gate verdict.
  • Observations for the seat, not defects of this head: (i) the insert() docblock (engine.ts near 16293) still says the ERR_AUTONUMBER_COLLISION exit is for a number "the engine ITSELF issued" — a number the driver issued through the member on a reader object now exits there too; an internal docblock, a follow-up touch. (ii) The changeset names the error-code change but not the HTTP status a REST caller sees on that exhausted path; ERR_AUTONUMBER_COLLISION carries code and cause and no status, exactly as on the engine-issued exit, so the seat confirms the envelope status in its prose-face ACCEPT if any page names 409 for it. (iii) The docs drift check lists content/docs/kernel/contracts/data-engine.mdx and siblings that name IDataDriver; prose faces, the seat's ACCEPT.

Implemented-by: claude/project-thread-m25d8w
Reviewed-by: session_01FaVyM6YvQLg9CsjPVCYc5K

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:engine seat 2 (seat post #20966) · os-bill · session_01FaVyM6YvQLg9CsjPVCYc5K · 2026-10-22T14:32Z · head 54971471cc12809500c7f7c5c3afa8e2e021892f · card #25073 (p1), maintainer ruling 6335312761 (letter A), seat surface call 6340248712.

  1. Form: draft, Fixes #25073, assignee os-bill, 11 files (+1613 / −20), main merged in, mergeable. check-governed-merges --pr 25218: 0 of 11 paths governed, under 3000 changed lines ⇒ ordinary queue landing, no human-merge tier.
  2. Premise measured: the dev report 6342739367 measured the stored expression reading the issued autonumber on single, batch and resync paths, on hand-rolled drivers, real SQLite (with afterInsert and a caller transaction) and the Turso remote face, with three ablations red and controls green; full dogfood battery exit 0 at this head. The contract review read the ordering in code: issue after validation, recompute over every row that received a number, one retry loop.
  3. Gates: contract review at CONTRACT_REVIEW_TIER PASS on this head, comment 6343187835. Check-runs on this head at this write: 35 success, 7 skipped, none pending or red. dispatch-gates 98 derived / 98 run; driver-conformance unchanged (55 covered, 0 debt).
  4. Accepted deviations:
  5. Out of scope, disposed:
    • upsert that inserts still issues inside the driver; by reading the same defect class, unmeasured. Not filed: the filing gate wants a measured reproduction. Left for a probe.
    • The REST status on the exhausted path follows the engine-issued exit's envelope (code and cause, no status), where the driver's DUPLICATE_RECORD carried 409. Rare path (three exhausted attempts), pre-existing for engine-issued numbers, unmeasured; noted, not filed.
    • insert()'s docblock near engine.ts:16293 still says the collision code is for numbers "the engine itself issued"; prose, noted for a later touch.
  6. Landing: every check on this head is green, so the seat flips ready and arms auto-merge through the relay in this round; the card closes on merge by Fixes #25073, and the landing comment records the queue branch and origin/main readings.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 22, 2026 14:43
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 22, 2026 14:43
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 22, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 22, 2026
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 22, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Queue note · domain:engine seat 2 (seat post #20966) · os-bill · session_01FaVyM6YvQLg9CsjPVCYc5K · 2026-10-22T15:27Z

The merge queue dropped this PR on Lint · CI, release & engine gates (job 119902408825, run 39801302350, group pr-25218-80ec71dc2). The failing step is "Issue citations this change adds resolve on the board", exit 3: check-issue-citations.mjs's EXIT_PREREQUISITE_NOT_MET, a board read that did not complete, not a citation verdict. The same gate is green on this PR's head 54971471c, which is unchanged, and no file of this PR is involved. The seat re-armed auto-merge once to re-queue it. A second failure on that step is treated as real.


Generated by Claude Code

Merged via the queue into main with commit c8f4c43 Oct 22, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-m25d8w branch October 22, 2026 15:54
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/xl tests tooling

Projects

None yet

2 participants