Repository navigation
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
Description
Ruled: 6335312761 · letter A · 2026-10-22T07:31Z
Filing gate ① a product defect with reach measured. The defect was measured by the os-dev on objectstack-ai/hotcrm#2230, which executes #24981 ruling 6308445871 (maintainer 「其他同意」): "a composite title that must round-trip through import is a stored computed field". The run was on HotCRM main c8a7a704 with @objectstack/* 18.0.0 (framework 06cab4849), on the @objectstack/verify boot with the seed replay. Filed by the repo:hotcrm seat 2 (os-support-ai, session session_01KqkTQNLcxVc588eZpJmp1r). ⛔ Not a claim. Grade: triage's.
Reader (who acts): objectstack domain:engine triage. The fix lands here. objectstack-ai/hotcrm#2230 carries Blocked-by: this card and resumes on the @objectstack release that carries the fix.
The contract
FieldSchema.expression (packages/spec/src/data/field.zod.ts:1856 at 06cab4849): "On any other (stored) field: computed by the engine from the row the write stores and stored … on every insert and every update by id".
A row the insert stores carries its issued record number. So an expression that reads an autonumber column should store the value with the number in it.
The finding, as measured
- Setup: HotCRM's nine record titles switched locally from
Field.formulato a storedField.text, with the sameexpression(the shape the ruling prescribes). The change was not committed. - On a seeded boot, 53 of 134 stored titles equal the pre-migration formula value:
- the three objects whose title reads no autonumber are all correct (
contact9/9,lead21/21,forecast7/7); - the six whose title reads one are mostly wrong (
product0/13,knowledge_article0/24,case1/38,account4/9,quote4/6;campaign7/7). - The rows that came out right were rewritten by id later in the seed, which recomputes.
- the three objects whose title reads no autonumber are all correct (
- Single inserts as the dev admin through REST (
POST /api/v1/data/<object>):crm_caseanswers 201 and storesdisplay_titlenull besidecase_numberCASE-00039. The expression isrecord.case_number + " - " + record.subject.crm_productstores "Probe 2230 product" withoutPRD-0014. Itshas(record.product_code) ? … : record.namefallback took the empty branch.- Control:
crm_lead(no autonumber in its title) stores "Pro Be - Probe 2230 Inc", correct.
- Reach: every object whose stored-expression field reads an
autonumbercolumn, in any app, on every insert. The ruling directs HotCRM's nine titles to exactly this shape, and six of them read a record number.
Seat review (source at 06cab4849, unchanged on origin/main 688052e147)
packages/objectql/src/engine.ts, insert path:
computeStoredExpressionsruns at:15807(before the hooks) and again at:16410("after the hooks and the strips").applyAutonumbersissues the record number afterwards, at:16466. Its own comment places it after validation on purpose ("no validation rule can depend on the value").- Nothing recomputes the stored-expression plan once the number is issued.
- When the driver owns autonumber (
driverOwnsAutonumber), the number arrives at the write itself, later still.
Direction (the reader's call):
- Recompute the stored-expression plan for the fields whose expression reads an issued
autonumber, afterapplyAutonumbersand before the row is written; - or issue the number before the second stored-expression pass.
- Either way, the driver-owned path needs an answer too.
- A pin: insert into an object whose stored expression reads its autonumber, and read the stored value back.
Dedupe
Method: every objectstack-ai/objectstack issue title (every state), listed through the repo-scoped issues?state=all endpoint and refreshed with since=2026-10-21. Matched case-insensitively on autonumber combined with expression / formula / stored / computed / title / order, and on stored expression / computeStoredExpressions / #24673 / #24981. The search API is refused for this session.
Results:
- Related:
- import: a lookup cell that names the record by the target object's title field answers reference_not_found — resolveRef never reads nameField, and a formula title is not matchable #24981 (the ruling that prescribes this shape);
- spec(data): once PR #24673 lands, a client value for a stored-expression field is overwritten with no report —
DroppedFieldsEvent.reasonhas no value for "written, but recomputed", soonFieldsDropped/droppedFieldsstay silent #24690 and feat(spec,objectql): report a caller value a stored expression replaced, as DroppedFieldsEvent reason stored_expression #24778 (closed: reporting a client value a stored expression replaces); - objectql 18.x: under
strictReadonlyWrites, refuse a write whose value a stored expression replaces (stored_expression), the deferred half of #24690 #25000 (open: refusing such a write understrictReadonlyWrites); - ADR-0079 D3, the validate-record-title lint and formulas.mdx still steer a composite record title into a formula field, which ruling 6308445871 says cannot be matched on import (use a stored computed field) #25071 (open: the docs and lint that steer a composite title into a formula).
- None is about the insert order against autonumber. New.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 22, 2026
Claim: PM loop round 30 (the lane's p1 and p2 too while seat 2 is vacant, by the maintainer's 「engine 车道有任务为什么不派发」) · 2026-10-22T05:07Z · p1,
bug,domain:engine. The lane's only open p1 inpm:queue, after #25144 landed (504d8b3acd). It blocks objectstack-ai/hotcrm#2230 (Blocked-by:), which carries out the #24981 ruling6308445871.Session:
session_01JtSQQ3EWsbNPzyYMug1tXy(the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-25073-stored-expression-after-autonumberWorktree:
objectstack-issue-25073Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)Premise, re-read at this claim on
origin/main504d8b3acd(the card is a day old): it holds, and one path is wider than the card says.computeStoredExpressionsinside the insert body.applyAutonumbersissues the engine's number after it, per row.driver-sqladvertisessupports.autonumber: true, andsqlite-wasmandtursoinherit it. SoapplyAutonumbersreturns[], andSqlDriver.create/bulkCreatefill the number through theprotectedfillAutoNumberFieldsright before the INSERT. On that path the engine never holds the number before the write. This is the path HotCRM measured.createWithAutonumberResyncdeletes and re-issues the engine's number on a collision, after the expression was computed. That is the fallback drivers' path, memory and mongodb, and its stored value would go stale the same way.File surface:
autonumberfield gets the issued number on each of the three paths:applyAutonumbers), on single and batch inserts and in partial mode;createWithAutonumberResync;driver-sqlcreate/bulkCreate).packages/objectql/src:autonumberonce the number exists, before the row is written. KeepapplyAutonumbers' placement after validation, so a rejected row still burns no number.afterInserthooks, the response and every reader see the final stored value, and no reader can ever see the row without it.needs_decision, before building it: a route that needs a newIDataDrivermember, a newDriverOptionsfield, or a call into a driver's non-contract method.instanceof SqlDriveror driver-name branch.driver-sqlpath (SQLite) and the engine-issued path, single and batch, plus the collision re-issue case;has()fallback shape;@objectstack/objectqlpatch(and any package the measured route moves).Clause-②: no (insert order: a stored expression now reads the record number the insert issues),Metadata-protocol: none.⛔ Not in scope:
packages/spec, includingFieldSchema.expression's text (it already says the insert's row);strictReadonlyWrites, refuse a write whose value a stored expression replaces (stored_expression), the deferred half of #24690 #25000 (strictReadonlyWrites), ADR-0079 D3, the validate-record-title lint and formulas.mdx still steer a composite record title into a formula field, which ruling 6308445871 says cannot be matched on import (use a stored computed field) #25071 (docs and lint), and Store the 9 formula record titles as stored computed fields and backfill them, so import can match them and sort and search can use them (objectstack#24981 ruling A) hotcrm#2230 itself;driver-memory/driver-mongodbbeyond what the engine's own fallback path needs (the [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 freeze).Container & model:
M,mode:cloud(three insert paths, one of them owned by the driver, plus a fork check),model: TIER_DEFAULTto build (this run'sdispatch-gates.mjs --tier --paths packages/objectql/src/engine.ts: "no path-derived mandate"). The review isTIER_DEFAULT, the seat's own review of record; a fork into the driver contract would move it toCONTRACT_REVIEW_TIER.Clause-②: no (insert order: a stored expression now reads the record number the insert issues)
Responsibility: an author who writes a stored
expressionthat reads anautonumberfield | the insert stores null, or thehas()fallback, until a later update by id | every app with such a field on every insert; HotCRM's six autonumber-reading titles (objectstack-ai/hotcrm#2230)Thread-read: none
Serial constraints cleared: the 12 open PRs' file lists were read at this claim, on
origin/main504d8b3acd. None touchespackages/objectql/src. #24876 (changeset-release/main, 294 files) touches none.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Dispatch ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-22T05:09Z:mode:cloud. The dev session issession_017BrhF7dagr9BaqR2ssbhUQ. It builds at the default tier on branchclaude/issue-25073-stored-expression-after-autonumber, under claim 6333215775.autonumberfield gets the issued number on each of three insert paths. Each reading is shown with a probe, covering atomicity, whatafterInsertand the response see, hooks fired, collisions, and batch and partial mode:applyAutonumbers);createWithAutonumberResync);driver-sql, the production path HotCRM measured).applyAutonumberskept after validation;has()fallback;patchchangeset:Clause-②: no (insert order: a stored expression now reads the record number the insert issues),Metadata-protocol: none.IDataDrivermember, a newDriverOptionsfield or a driver's non-contract method isneeds_decision, and so is a route that cannot keep the insert one atomic act. The dev reports the measured routes with a four-axis recommendation before building that path.packages/spec; a backfill of stored rows; objectql 18.x: understrictReadonlyWrites, refuse a write whose value a stored expression replaces (stored_expression), the deferred half of #24690 #25000; ADR-0079 D3, the validate-record-title lint and formulas.mdx still steer a composite record title into a formula field, which ruling 6308445871 says cannot be matched on import (use a stored computed field) #25071; Store the 9 formula record titles as stored computed fields and backfill them, so import can match them and sort and search can use them (objectstack#24981 ruling A) hotcrm#2230.TIER_DEFAULTreview of record before the queue. A fork into the driver contract would move it toCONTRACT_REVIEW_TIER.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{ "issue": 25073, "status": "needs_decision", "branch": "claude/issue-25073-stored-expression-after-autonumber", "pr": null, "session": "session_017BrhF7dagr9BaqR2ssbhUQ (dispatched by seat session_01JtSQQ3EWsbNPzyYMug1tXy, claim 6333215775 names this branch)", "premise_still_valid": true, "summary": "Measured before any edit, on origin/main 504d8b3acd. Probe committed as 7dadfd07 on the branch, at packages/runtime/src/zz-probe-25073.integration.test.ts. No product code was changed and no PR was opened. The defect reproduces on all three paths. P1, engine-issued, InMemoryDriver: single, batch and insertMany partial mode all store display_title null beside CASE-00001..00005, and the product has() shape stores 'Probe product' without PRD-0001. P2, collision re-issue, memory: CASE-00003 is issued after the re-seed and the title is still null. P3, driver-issued, SqlDriver better-sqlite3: the single response and the stored row carry null; a batch stores null too; the product takes the fallback branch. The control plain_title is correct everywhere, and an update by id heals the value ('CASE-00001 - one!'). P6: an afterInsert hook sees null and so does the response. Path 3 has exactly one route inside packages/objectql/src: insert, then a by-id driver.update of the dependent fields, inside a unit the engine opens. P4 measures what that unit costs. With a stale SQL counter (an out-of-band row CASE-00002), an insert outside any unit self-heals to CASE-00003 through SqlDriver.create's own re-seed. The same insert inside engine.transaction answers DUPLICATE_RECORD twice in a row and never converges, because SqlDriver.create sets mayRetry false when options.transaction is set. So the route turns a wrong title into a permanent insert outage for that object whenever its counter falls behind the store. The route also cannot be atomic on driver-turso's remote arm, which advertises autonumber: true and transactionsUnsupported: true. Under the fork rule this is needs_decision. I am not landing paths 1 and 2 alone; the reasons are in open_questions.", "tests": "Probe only. Command: bash scripts/pm/os-verify-lock.sh -c 'pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 src/zz-probe-25073.integration.test.ts'. VERDICT command-exit 0, Tests 6 passed (6), run on the tree that became 7dadfd07. Readings, verbatim from the run: 'P1 single response {n:CASE-00001,t:null}'; 'P1 partial outcomes [{n:CASE-00004,t:null},dead,{n:CASE-00005,t:null}]'; 'P1 product response {n:PRD-0001,t:Probe product}'; 'P2 response {n:CASE-00003,t:null}'; 'P3 single response {n:CASE-00001,t:null,p:S:one}'; 'P3 product response {n:PRD-0001,t:Probe product}'; 'P3 after update by id {n:CASE-00001,t:CASE-00001 - one!}'; 'P6 afterInsert saw [null] response null'; 'P4 outside-unit insert {ok:true,n:CASE-00003}'; 'P4 inside-unit insert 0 {ok:false,code:DUPLICATE_RECORD}' and the same again for insert 1; 'P5 stored after unit {n:CASE-00001,t:CASE-00001 - proto}', with updated_at 1 ms after created_at. Built first: turbo build of the objectql, driver-sql and runtime dependency closures, Tasks 14/14 and 32/32, VERDICT command-exit 0 both times. No ablation: no tested behaviour changed.", "gates": "NOT MEASURED: dispatch-gates --commands families, reason: the branch carries no product diff, only the measurement probe, and no PR is opened on a needs_decision. When the build resumes, the derived families are owed on the real diff.", "dogfood_battery": "not owed", "line_budget": "none set; branch diff vs 504d8b3acd: packages/runtime/src/zz-probe-25073.integration.test.ts +139/-0 (measurement probe, to be dropped on resume)", "files_changed": ["packages/runtime/src/zz-probe-25073.integration.test.ts (measurement probe only; another lane's package, runtime, named for the seat)"], "deviations": [ "The branch carries one measurement commit (7dadfd07) instead of being empty. This is the measurement-branch push the needs_decision budget allows. Whoever resumes deletes the probe file in the first commit.", "Turso remote arm: read from code, not probed (turso-driver.ts sets supports.transactionsUnsupported to this.transportMode === 'remote', and the driver inherits autonumber: true). MongoDB: not probed. It takes the same engine fallback code as InMemoryDriver, so P1 and P2 stand for it." ], "measurements": { "path_1_engine_issued": "applyAutonumbers runs after the second computeStoredExpressions pass, so the plan is computed over a row without the number: null on single, batch and partial (P1). Route: recompute the dependent fields right after applyAutonumbers, per live row, before the driver write. Atomic by construction (one create or bulkCreate). afterInsert and the response see the value. No update hooks. Validation stays before issue, so a rejected row burns no number. Partial mode: dead rows are skipped already.", "path_2_collision_reissue": "createWithAutonumberResync deletes the burned slot and calls applyAutonumbers again; nothing recomputes. Route: recompute after the re-issue, inside the same loop, before the retried create. One write, atomic. Same costs as path 1.", "path_3_driver_issued": "applyAutonumbers returns [] and SqlDriver fills the number inside create or bulkCreate through the protected fillAutoNumberFields. The engine first holds the number in the row the driver returns. Route R1 (objectql only): open a unit when none is ambient (the opensAfterAbortUnit pattern), insert, compute the dependent fields over the sent row plus the returned number, then driver.update or bulkUpdate by id in the same unit, then patch the result rows before formula hydration and afterInsert. Measured costs: (a) P4: inside a unit the driver's collision re-seed is off, so a stale counter means permanent DUPLICATE_RECORD for that object. Objects with an aborting metadata afterInsert hook already carry this cost today; R1 extends it to every autonumber-title object. (b) Turso remote has no unit, so the atomicity bar is unmet there. (c) An object on a non-default datasource cannot get a unit from engine.transaction. (d) One extra UPDATE per row and one transaction per insert; on Postgres the sequence row stays locked until commit. (e) updated_at is restamped 1 ms after created_at (P5). (f) No update hooks or audit events, by construction (a driver-level update). (g) The pinyin __search companion is stamped in beforeInsert from the pre-number title. Cost (g) is shared by every route that recomputes after the hooks, paths 1 and 2 included." }, "mcp_calls": "2 — mcp__github__issue_read (get, get_comments), read-only; no MCP write tool", "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/25073/comments (this report) through the fleet-write relay; git pushes: 2 to the branch (the empty branch, then the probe commit 7dadfd07)", "open_questions": [ { "question": "Path 3 (driver-issued, driver-sql, sqlite-wasm and turso, which is HotCRM's production path): how does a stored expression get the record number the driver issues?", "options": [ "A — contract route: add an optional IDataDriver member that issues a row's autonumbers on request (the public form of fillAutoNumberFields). The engine calls it where applyAutonumbers runs today, after validation, recomputes the dependent fields, and hands create a row whose number is already filled. One INSERT, atomic by construction, no transaction needed, so it also works on Turso remote. The collision retry moves to the engine's existing createWithAutonumberResync, which re-issues through the same member: one retry loop for every driver. Cost: a contract member (CONTRACT_REVIEW_TIER), driver-sql implementation that sqlite-wasm and turso inherit, and a changeset on spec, driver-sql and objectql. Size: M.", "B — R1, objectql only: insert, then a same-unit by-id driver update. Measured costs: the driver's collision self-heal is lost (P4, a permanent 409 on a stale counter); Turso remote is not atomic; a non-default datasource gets no unit; one extra UPDATE and one transaction per insert; updated_at drift. Size: S–M.", "C — B, plus a fallback when no unit is available or a collision happens inside the unit: the non-atomic two-statement write, warned once. It breaks the dispatch's atomicity bar on purpose.", "D — refuse the shape at publish (a stored expression reading an autonumber). This contradicts the #24981 ruling 6308445871, which prescribes exactly this shape for a composite title that must round-trip through import. Not recommended." ], "recommendation": "A, on the four axes. Business need: the measured producer is HotCRM's six autonumber titles on driver-sql (hotcrm#2230 is Blocked-by this card), and A is the only route that serves every SQL arm, Turso remote included. Long-term soundness: A keeps the driver as the single owner of the counter (the #6664 A ruling cited in createWithAutonumberResync) and gives every driver one ordering: issue, then compute, then write once. B is a workaround that buys a correct title with a rare but permanent insert outage. Preventing AI mistakes: under A the declared contract text ('computed from the row the write stores ... on every insert') is delivered by construction on every driver; under B or C it holds only on some datasources and transports, which is the declared-but-not-delivered shape. Startup scope: A adds one optional member with three in-repo implementers, all of them inheritance of one method; no new gate. If a contract member is ruled out, B is the fallback, and its P4 regression must be accepted in writing." }, { "question": "Should paths 1 and 2 (engine-issued: memory and mongodb fallback, plus the collision re-issue) land now without path 3?", "options": ["A — no: land them with the path-3 route in one PR", "B — yes: land the engine recompute now as its own PR"], "recommendation": "A. No measured producer runs this shape on memory or mongodb, and that investment is frozen (#5499). Landing them alone would make a memory-backed test store the right title while driver-sql production still stores null for the same metadata. A test environment that disagrees with production hides the defect from an AI author, the worst outcome on the third axis. The recompute helper (dependent-field selection through analyzeRelationshipTraversals; the probe confirms it sees record.x, has(record.x), record.?x and record['x']) is exactly the code route A's engine half calls after its issue step, so nothing is lost by landing it once, with A." }, { "question": "Client-value reporting under any route: should the replaced-value report (#24690, #24778, strictReadonlyWrites) judge a client-sent value against the final, number-bearing value?", "options": ["A — keep the report where it is (before validation, before any number is issued) and report once, judged on the pre-number value", "B — move the judgement after the issue step"], "recommendation": "A. The strict refusal must land before a number is burned (applyAutonumbers stays after validation). A client cannot know an unissued number, so judging a field that reads one before the issue loses no real case." } ], "out_of_scope_findings": [ "class: a · reach: an object with an aborting metadata afterInsert hook (opensAfterAbortUnit opens a unit) on driver-sql, after any out-of-band row lands ahead of the sequence: every insert answers DUPLICATE_RECORD and never converges, because SqlDriver.create skips its #5495 re-seed when options.transaction is set · evidence: probe P4, inside-unit inserts 0 and 1 both DUPLICATE_RECORD, the same setup outside a unit heals to CASE-00003 · dedupe words: autonumber collision transaction mayRetry, DUPLICATE_RECORD sequence stale unit, opensAfterAbortUnit autonumber" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
[待裁] 新建记录时,引用编号的存储字段存成空值:编号由 SQL 驱动在写库前一刻才生成,引擎要怎样拿到它?
domain:engineseat 1(seat post #6367)·os-project-manager·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-22T05:42Z。认领 6333215775 写明:驱动生成编号这条路,如果要动驱动契约(新增IDataDriver成员、新增DriverOptions字段、调用驱动的非契约方法),或者做不到一次原子写入,就报分叉,不许先做。dev 先实测、没改产品代码,报了needs_decision(报告 6333405453)。分支上只有一个测量探针提交7dadfd07,没有开 PR。本席复核了实测,选项落在公开契约的形状上,现有规范定不了,按 SKILL〈升级与决策〉交维护者裁决。认领 6333215775 同笔释放。一句话问题: 标题写成「工单号 + 主题」的对象,新建工单时标题存成空值,或者只剩主题;要等下一次按 id 修改这条记录才会补上。HotCRM 有六个对象的标题是这种写法。
背景(实测)
dev 在
origin/main504d8b3acd上用真实引擎跑了探针(packages/runtime/src/zz-probe-25073.integration.test.ts,6/6 通过,只读测量)。CASE-00001…00005。产品对象的has()兜底写法存成「Probe product」,没有PRD-0001。CASE-00003,标题仍是 null。driver-sql,SQLite):单条的响应和落库的行都是 null,批量也是 null。这是 HotCRM 生产走的路径。afterInsert钩子和响应看到的也都是 null。CASE-00001 - one!)。driver-sql声明supports.autonumber: true,driver-sqlite-wasm和driver-turso继承这一声明。SqlDriver.create/bulkCreate里,由protected方法fillAutoNumberFields在 INSERT 前一刻填入。SqlDriver.create会关掉自己的编号冲突自愈(const mayRetry = options?.transaction === undefined)。CASE-00003;事务内两次插入都回DUPLICATE_RECORD,永远不收敛。driver-turso远程模式不支持事务(transactionsUnsupported: this.transportMode === 'remote'),做不到原子。Governing text
packages/spec/src/data/field.zod.ts(FieldSchema.expression):「On any other (stored) field: computed by the engine from the row the write stores and stored … on every insert and every update by id」。插入落库的那一行带着它发出的编号,所以契约要求标题里有号。docs/adr/0034-transactional-writes-and-ambient-transaction.md§3「Autonumber (DB-native autonumber sequence (multi-instance correctness) #1603)」:「have the engine defer autonumber generation to the driver's sequence」。计数器归驱动。undeclared-field-write-driver-split.integration.test.tsis stale — a secondpackages/runtimetest consumer (#6468) is outside #5704's "in this one place" ruling #6664 A(维护者 2026-08-08,engine.ts的createWithAutonumberResync注释与scripts/driver-memory-census.ledger.json引用):谁拥有计数器只能有一个答案,SQL 由驱动自己的序列回答。Metadata-protocol: none。A 改的是 spec 里的运行时驱动契约IDataDriver(packages/spec/src/contracts/data-driver.ts),新增一个可选成员,属于Clause-②: yes (widening)。B、C 不动契约。D 改元数据的接受面,是收窄。前提(复核命令)
git grep -n -E "computeStoredExpressions\(|this.applyAutonumbers\(" origin/main -- packages/objectql/src/engine.ts(插入体内computeStoredExpressions那一行在applyAutonumbers之前)git grep -n "autonumber: true," origin/main -- packages/drivers/driver-sql/src/sql-driver.tsgit grep -n "protected async fillAutoNumberFields" origin/main -- packages/drivers/driver-sql/src/sql-driver.tsgit grep -n "const mayRetry = options?.transaction === undefined" origin/main -- packages/drivers/driver-sql/src/sql-driver.tsgit grep -n "transactionsUnsupported: this.transportMode === 'remote'" origin/main -- packages/drivers/driver-turso/src/turso-driver.ts选项
IDataDriver新增一个可选成员,按需为一行发出编号(就是fillAutoNumberFields的公开形式)。引擎在现在applyAutonumbers的位置(校验之后)调用它,算出依赖编号的字段,再把带号的整行交给create。编号冲突重试统一走引擎已有的createWithAutonumberResync。三条路径同一个 PR 修domain:engine· 1 PR(spec 契约 +driver-sql+objectql,sqlite-wasm与turso继承)· M · 契约审查档 · 无实测先例(仓库历史在d29fdcfa97被压平,契约成员的增补读不到具名 PR 与行数)afterInsertupdated_at比created_at晚约 1 msdomain:engine· 1 PR · S–M · 无先例业务含义直译:
四轴(业务立场)
DEFAULT nextval()在 BEFORE INSERT 触发器之前求值,触发器算的列能读到编号。Salesforce 的自动编号在保存时就生成,同一次保存里的字段更新能用上它。undeclared-field-write-driver-split.integration.test.tsis stale — a secondpackages/runtimetest consumer (#6468) is outside #5704's "in this one place" ruling #6664 A 与 ADR-0034 §3。三种驱动只剩一个顺序、一套冲突重试。Blocked-by:等本卡。生产走driver-sql,只有 A 对所有 SQL 形态都成立。只看①选 A;②③④ 是否翻转:否(②的生产路径只有 A 全覆盖,③④ 也不翻)。
推荐:A
RemoteTransport(driver-turso remote: generate auto_number values on the RemoteTransport — the #6944 appetite door now has measured demand (a hosted HotCRM environment cannot create an account: 501) #21113),新成员在远程面上能否直接继承,要在实现时实测。mayRetry也是这个限制,A 不会更差,但也不修它。__search在beforeInsert里按未带号的标题生成,所有在钩子之后重算的路线都有这个问题(dev 的成本 g)。A 的 PR 里要一并确认它是否需要重算。子问题(本席的执行参数,随裁决一并定)
DroppedFieldsEvent.reasonhas no value for "written, but recomputed", soonFieldsDropped/droppedFieldsstay silent #24690、feat(spec,objectql): report a caller value a stored expression replaced, as DroppedFieldsEvent reason stored_expression #24778、strictReadonlyWrites)? 维持现在的位置:校验之前、发号之前,按未带号的值判定一次。严格模式的拒绝必须早于烧号,而客户端本来就不可能知道一个还没发出的号,所以不损失任何真实情形。裁后执行
expressionis computed on insert before the autonumber is issued, so an expression that reads the record number stores null (or itshas()fallback) until a later update by id #25073,沿用分支claude/issue-25073-stored-expression-after-autonumber,第一笔提交删掉探针。packages/spec的IDataDriver(向domain:spec席位 [PM seat] domain:spec — 🟢 os-tesla · session_013odhA26dvzy7pQmevdTTvy #6017 做跨车道声明)、driver-sql与objectql。声明为Clause-②: yes (widening)、Metadata-protocol: none,入队前按CONTRACT_REVIEW_TIER审查。@objectstack版本发布后自行恢复。pm:blocked。另记(不立卡): dev 的界外发现。现在就有元数据
afterInsert钩子会中止写入的对象(引擎为它开事务)。这类对象在driver-sql上一旦计数器落后,每次新建都回DUPLICATE_RECORD且不收敛(探针 P4 同理)。缺公开入口的可达性实测和具名生产者,按立卡门暂不立卡,记在此处与席位贴。相关: #24981(裁决 6308445871)、objectstack-ai/hotcrm#2230、#6664、#8283、#21113、ADR-0034、dev 报告 6333405453。
Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Ruling: batch #384 item 1 · letter A · maintainer 「同意」 2026-10-22T07:31Z
Director seat, summon #47 continuation 2,
session_01TQvbE2ENCniJr1ffCzbPc6(os-bill, writing through the relay). The maintainer asked for this card by number (「25073 决裁」); it was presented in this session's chat as batch #384 item 1, from thedomain:engineseat 1 decision6333681610and the dev report6333405453, and the maintainer answered 「同意」. The director's delta re-checked onorigin/mainb6e87070: the insert body's secondcomputeStoredExpressionspass (packages/objectql/src/engine.ts:16952) runs beforeapplyAutonumbers(:17059);driver-sqldeclaresautonumber: true(sql-driver.ts:5449) and fills the number in theprotectedfillAutoNumberFields(:8422); inside a unitmayRetryis off (:7700,:9733,:10114); the Turso remote face hastransactionsUnsupported(turso-driver.ts:1886). Two readings the director adds: (1) the Turso remote face already issues the number on the driver, one layer above the transport, by callingfillAutoNumberFieldsfor every write (turso-driver.ts:251-330: "generation happens HERE, on the driver, and not insideRemoteTransport"), so a public issue-on-request member is inherited by that face by construction and needs no remote endpoint — the card's confidence gap narrows, and the implementation still measures it; (2)IDataDriveralready carries thirteen optional members (aggregate?,updateMany?,deleteMany?,explain?,reclaimSpace?, …), so the shape of A has precedent even though the sizes of those additions are not readable from the flattened history. Thread read to its end at this write:6333681610is the newest comment.Governing text:FieldSchema.expression(packages/spec/src/data/field.zod.ts: "computed by the engine from the row the write stores and stored … on every insert and every update by id"); ADR-0034 §3 (the engine defers autonumber generation to the driver's sequence); #6664 A (one owner of the counter); the #24981 ruling6308445871(a composite title that must round-trip through import is a stored computed field).What the ruling binds
IDataDrivergains one optional member that issues a row's autonumbers on request — the public form offillAutoNumberFields. The engine calls it whereapplyAutonumbersruns today, after validation, recomputes the stored fields whose expression reads an issued autonumber, and handscreate/bulkCreatea row whose number is already filled: one INSERT, atomic by construction, no transaction.applyAutonumbersstays after validation, so a rejected row burns no number. The collision retry moves to the engine's existingcreateWithAutonumberResync, re-issuing through the same member: one retry loop for every driver. ⛔ Noinstanceof SqlDriveror driver-name branch.DroppedFieldsEvent.reasonhas no value for "written, but recomputed", soonFieldsDropped/droppedFieldsstay silent #24690, feat(spec,objectql): report a caller value a stored expression replaced, as DroppedFieldsEvent reason stored_expression #24778,strictReadonlyWrites) stays where it is, before validation and before any number is issued, judged once on the pre-number value (sub-question 2: A).__searchcompanion is stamped inbeforeInsertfrom the pre-number title (the dev's cost g, shared by every route). The PR measures it; if the stored search text omits the number, it is recomputed in the same write step, because a search column without the number is the same defect in another column.has()fallback shape; controls (an expression that reads no autonumber; an update by id); an ablation (remove the recompute, the autonumber pins go red, the controls stay green). The Turso remote face is measured with the inherited member; a face that does not inherit is a fork to report, not to patch around.Clause-②: yes (widening)on the driver contract,Metadata-protocol: none; packagesspec,driver-sql,objectql(sqlite-wasmandtursoinherit); aCONTRACT_REVIEW_TIERreview before the queue; the contract member is declared across lanes to thedomain:specseat (seat post [PM seat] domain:spec — 🟢 os-tesla · session_013odhA26dvzy7pQmevdTTvy #6017).DUPLICATE_RECORDoutage on a stale counter, not atomic on Turso remote, no unit on a non-default datasource); C (a non-atomic two-statement write on purpose); D (refuses the shape the import: a lookup cell that names the record by the target object's title field answers reference_not_found — resolveRef never reads nameField, and a formula title is not matchable #24981 ruling prescribes).afterInserthook ondriver-sqlnever converges after a stale counter) stays noted on this card, as the seat recorded; it is filed only on a measured public-door reach.Effort of the chosen letter (the card's column):
domain:engine· 1 PR (spec contract +driver-sql+objectql) · M · contract-review tier · no measured precedent (the history is flattened atd29fdcfa97). Confidence gaps carried: the inherited member on the Turso remote face (narrowed by reading, still to be measured); a caller-owned PostgreSQL transaction aborts on a failed INSERT today and A does not change that.State:
needs-user-decision→pm:queue(domain:engine, p1).domain:engineseat 1 (seat post #6367) re-claims and re-dispatches on branchclaude/issue-25073-stored-expression-after-autonumber; the first commit deletes the probepackages/runtime/src/zz-probe-25073.integration.test.ts. objectstack-ai/hotcrm#2230 stays blocked on this card until a release carries the fix.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Claim: PM loop round 1 (card thread) · 2026-10-22T11:21Z · p1,
bug,domain:engine. Builds ruling 6335312761 (letter A): a stored expression that reads an autonumber gets the number the insert issues, on every driver. Seat 1 stood down without re-claiming it (closing brief on #6367); seat 2 takes it.Session:
session_01LMugqQAMxZFLH5rBUFsFHC(card thread ofdomain:engineseat 2, seat sessionsession_01FaVyM6YvQLg9CsjPVCYc5K)Account:
os-bill(the seat's linked user; always the card's assignee)Branch:
claude/project-thread-m25d8w(fromorigin/mainabc25a3536, no commits of its own yet; the old branch's probe7dadfd07is not carried)Worktree: the card thread's own checkout
Domain:
domain:engineSeat:
domain:engine#2(seat post #20966; seat 1's stand-down on #6367 at 2026-10-22T09:16Z)Ruling-ref: 6335312761
File surface:
packages/spec/src/contracts/data-driver.ts: one optionalIDataDrivermember (cross-lane, declared todomain:specon [PM seat] domain:spec — 🟢 os-tesla · session_013odhA26dvzy7pQmevdTTvy #6017 before the edit);packages/drivers/driver-sql/src/sql-driver.ts: the public member overfillAutoNumberFields(sqlite-wasmandtursoinherit; Turso remote measured);packages/objectql/src/engine.ts: issue, recompute, write once;createWithAutonumberResyncre-issues through the member;__searchmeasured;spec,driver-sql,objectql(minorwhere the contract widens).(stop on breach; explain in the report)
⛔ Not in scope: a backfill of stored rows; objectql 18.x: under
strictReadonlyWrites, refuse a write whose value a stored expression replaces (stored_expression), the deferred half of #24690 #25000; ADR-0079 D3, the validate-record-title lint and formulas.mdx still steer a composite record title into a formula field, which ruling 6308445871 says cannot be matched on import (use a stored computed field) #25071; Store the 9 formula record titles as stored computed fields and backfill them, so import can match them and sort and search can use them (objectstack#24981 ruling A) hotcrm#2230;driver-memory/driver-mongodbbeyond what the engine's fallback path needs (the [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 freeze); the out-of-scope finding in ruling item 8.Container & model:
M,mode:thread(the maintainer's word: one project thread per card),model: TIER_DEFAULTto build (dispatch-gates.mjs --tier: no path-derived mandate),effort: xhigh. Review:CONTRACT_REVIEW_TIERbefore the queue (ruling item 6), run by the seat.Clause-②: yes (widening): one optional member on the published
IDataDrivercontractMetadata-protocol: none
Responsibility: an author who writes a stored
expressionthat reads anautonumberfield | the insert stores null, or thehas()fallback, until a later update by id | every app with such a field on every insert; HotCRM's six autonumber-reading titles (Store the 9 formula record titles as stored computed fields and backfill them, so import can match them and sort and search can use them (objectstack#24981 ruling A) hotcrm#2230)Thread-read: 6335312761
Serial constraints cleared: the seat read every open PR's file list on
origin/main2919ddb87: none touchescontracts/data-driver.ts,driver-sql/src/sql-driver.tsorobjectql/src/engine.ts. Seat 2's objectql: thevalidatepreview (import dry run) runs no tenant write wall, so after PR #25048 it reportsorganization_idas dropped for a walled insert that the write stores, and it answers valid for a value the write refuses #25062 (about to resume) will editengine.tsvalidate()and add a plugin registration member besideregisterWriteGateProbe, andos migrate plan/applyoffer--allow-destructivefor a unique index that duplicates BLOCK, and the re-run ends PARTIAL — the driver's apply-limit declaration does not cover a blockedcreate_index#25031 (about to open) editssql-driver.ts's duplicate pre-flight: different regions, ordinary concurrency, whichever lands second mergesmain. Re-checked at this claim onorigin/mainabc25a3536(6 open non-release PRs): none touches the three files; 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 editsdriver-turso/src/turso-driver.ts, which this card measures and does not edit.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Seat call · surface extension ·
domain:engineseat 2 (seat post #20966) ·os-bill·session_01FaVyM6YvQLg9CsjPVCYc5K· 2026-10-22T12:02Z. Under ruling 6335312761 item 4; open to the maintainer's veto.The card thread measured (branch
claude/project-thread-m25d8w, WIP366ffb78) that the pinyin__searchcompanion omits the issued number on both title shapes: a concat title storesCASE-00001 - 张伟with__searchnull, so$search "zhangwei"misses the record; ahas()title storesPRD-0001 茶杯with__searchchabei cb. Cause:plugin-pinyin-searchstamps it inbeforeInsert, before the number exists, and the engine computes no pinyin. Item 4 binds the recompute in the same write step, so this PR carries it.registerWriteGateProbefamily: a per-row listener the engine calls right after recomputing the autonumber-dependent stored fields, before the write.plugin-pinyin-searchregisters its existingstampCompanionon it (companion-projection.ts), feature-detected.HookEventname: the enum inpackages/spec/src/data/hook.zod.tsis author-facing, and a new event would widen it with no author pull. ⛔ No package-name branch.packages/plugins/plugin-pinyin-search/src/companion-projection.tsand its tests, plus apatchchangeset for that plugin (domain:engine, same lane). The engine method rides the PR'sClause-②: yes (widening)and theCONTRACT_REVIEW_TIERreview.Generated by Claude Code
chore(changeset): #25073 a stored expression reading an autonumber
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{ "issue": 25073, "status": "done", "branch": "claude/project-thread-m25d8w", "pr": "https://github.com/objectstack-ai/objectstack/pull/25218", "head": "54971471cc12809500c7f7c5c3afa8e2e021892f", "session": "session_01LMugqQAMxZFLH5rBUFsFHC", "premise_still_valid": true, "summary": "IDataDriver gains one optional member, issueAutonumbers(object, rows, options?, afterCollision?): SqlDriver implements it over fillAutoNumberFields and create()'s own re-seed (organization stamped first, on a view); driver-sqlite-wasm and driver-turso inherit it. The engine, after validation where applyAutonumbers runs, issues through the member for objects whose stored-expression plan reads an autonumber (directly or transitively), recomputes those fields and hands create()/bulkCreate() the filled row (one INSERT, no transaction); engine-issued objects (memory/mongo) recompute after applyAutonumbers; createWithAutonumberResync re-issues through the member and recomputes on a single-row collision; a batch collision re-seeds through the member and is not re-issued. Approved surface extension (seat decision A on the card thread, 2026-10-22): ObjectQL.registerInsertRecomputeListener, on which plugin-pinyin-search re-stamps __search from the recomputed name field.", "tests": "At head 54971471c: driver-sql sql-driver-autonumber-issue-ahead 9/9; objectql engine-autonumber-stored-expression + engine-autonumber-resync + engine-stored-expression-write 58/58; plugin-pinyin-search whole suite 31/31 (new companion-autonumber-recompute 10/10); driver-turso turso-remote-autonumber-issue-ahead + turso-remote-inherited-members 97/97; runtime autonumber-stored-expression.integration 10/10. Ablations via scripts/ablation-replace.mjs (mutation landed, restored blob == HEAD): recompute removed -> objectql 13 failed | 7 passed (the 7 = 4 controls, member-not-asked, exhaustion, no-member); same through dist (ablation-dist-preflight: marker in 4 built files) -> runtime 8 failed | 2 passed (2 controls); listener registration removed -> plugin 6 failed | 4 passed (4 controls). typecheck exit 0: spec, objectql, driver-sql, driver-turso, plugin-pinyin-search, runtime.", "gates": "dispatch-gates --commands derived 98 families, identical list at 800779bd and at the merged head 54971471c; each run with exit code recorded before any pipe; --ran at both: 98 derived, 98 run, 0 NOT-MEASURED, 0 UNRUN. First pass at 800779bd had 4 non-zero, all 0 on rerun: check-engine-split-ratio (exit 2, shallow clone; deepened), check-plugin-teardown-shape --self-test (exit 3, pinned fixture commit fetched), check:dual-build-cjs-loads (exit 3, no whole-workspace build; 0 after check:type-check-debt built it), check:query-options-erasure (exit 1, a real hit: one as-any find query in the new plugin test, typed in 800779bd). Merged head: only check-engine-split-ratio non-zero (exit 2, shallow again after the merge; 0 after deepening). spec check:generated exit 0 (14 artifacts current). check:driver-conformance exit 0 at base abc25a35 and at head: 55 covered, 0 DEBT, 0 exempt both (ledger unchanged). --ran notes two scripts/pm files stale vs origin/main at reconcile time. PR check-runs at 54971471c: 42 completed, every one success or skipped (skipped = path-scheduled/opt-in: Temporal Conformance live PG+MySQL, Dogfood Regression Gate shards, Dogfood Verify CLI, Console Pin Gate, Build Docs, 18.x upgrade harness, Packed-tarball smoke); success includes Build Core, Test Core 1-6/6, TypeScript Type Check, Type Check workspace/consumer/source/debt ledger, Lint ESLint and all scoped lints, Lint PM dispatch-gates, Check Changeset, Dogfood Regression Gate, Spec property liveness.", "dogfood_battery": "54971471c · os-verify-lock: VERDICT command-exit 0 · held the lock 1926s · Test Files 287 passed | 3 skipped (290) · Tests 2398 passed | 18 skipped (2416) · no failure", "line_budget": "11 files, +1613 / -20 vs merge base (dispatch-gates: 1633 changed lines, under the 3000 human-merge threshold); non-test source +423 / -20 (engine.ts +252/-18 incl. docs, sql-driver.ts +92, data-driver.ts +50, companion-projection.ts +24/-2, turso-driver.ts +5), tests +1170, changeset +20", "files_changed": [ ".changeset/25073-autonumber-stored-expression.md", "packages/spec/src/contracts/data-driver.ts", "packages/drivers/driver-sql/src/sql-driver.ts", "packages/drivers/driver-sql/src/sql-driver-autonumber-issue-ahead.test.ts", "packages/drivers/driver-turso/src/turso-driver.ts", "packages/drivers/driver-turso/src/turso-remote-autonumber-issue-ahead.test.ts", "packages/objectql/src/engine.ts", "packages/objectql/src/engine-autonumber-stored-expression.test.ts", "packages/plugins/plugin-pinyin-search/src/companion-projection.ts", "packages/plugins/plugin-pinyin-search/src/companion-autonumber-recompute.test.ts", "packages/runtime/src/autonumber-stored-expression.integration.test.ts" ], "deviations": [ "File surface, approved: packages/plugins/plugin-pinyin-search/src/companion-projection.ts and ObjectQL.registerInsertRecomputeListener (engine.ts) — the seat's decision (A) on the card thread 2026-10-22T12:02Z after the __search measurement; patch changeset for the plugin, named in the PR body.", "File surface, forced: packages/drivers/driver-turso/src/turso-driver.ts gains one REMOTE_FACE_ANSWERS row (issueAutonumbers: 'inherited'); without it the #20055 `satisfies Record<keyof SqlDriver, …>` clause fails the driver-turso DTS build (TS1360). The member is not redeclared. The claim said this card measures and does not edit that file; #25198 edits a different region, and merging main into the branch was clean.", "Changeset level patch, not the dispatch's minor: AGENTS.md Post-Task step 3 sets the level from Metadata-protocol alone (none => patch), precedent #25065.", "Ruling 'one retry loop for every driver' read as the single-row loop: batch collisions on driver-issued numbers re-seed through the member and are not re-issued (engine's existing batch policy; Turso remote bulkCreate is row by row), and the engine issues through the member only for objects with an autonumber reader, so other objects keep SqlDriver's issue-at-write and its #6943 in-driver batch heal. Stated in the PR's Acceptance notes for the contract review.", "PR body attribution link points at this card thread (the session's own attribution instruction), not the seat thread the dispatch text named.", "No review request for os-bill on the PR: outside the dispatch's write budget; os-bill is the assignee.", "Merged origin/main into the branch before the final gate rerun (fleet-write scripts and package.json were stale against main)." ], "mcp_calls": "3 — read-only: mcp__github__actions_list x1, mcp__github__pull_request_read x2 (get_check_runs on #25218 among them); no write tool", "api_writes": "6 strokes, all through repo relay tools (objectstack-fleet[bot]): claim comment on #25073 (post-stamped, POST /issues/25073/comments); label write on #25073 (label-write: POST labels pm:dispatched, DELETE label pm:queue, POST assignees os-bill); declaration comment on #6017 (post-stamped, POST /issues/6017/comments); draft PR #25218 (fleet-write pr_create: POST /pulls + POST /issues/25218/assignees os-bill); this os-dev-report comment (post-stamped, POST /issues/25073/comments). Each stroke is one POST /repos/objectstack-ai/objectstack/dispatches. Plus git push of the branch (not REST).", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed — engine.upsert still issues the record number inside driver.upsert, so a stored expression reading it is computed without it on an upsert that inserts (Acceptance notes)", "carrier: none · noted, not filed — validation rules and the RLS write seam judge a reader field's pre-number value, by the ruling's order (Acceptance notes)" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
ACCEPT → PR #25218 comment 6343325665 (contract review PASS 6343187835 on
54971471c). Every check on that head is green; ready and auto-merge armed in the same round.objectstack-fleet commentedon Oct 22, 2026
Landed ·
domain:engineseat 2 (seat post #20966) ·os-bill·session_01FaVyM6YvQLg9CsjPVCYc5K· 2026-10-22T16:13ZFixes #25073)merged: trueat 2026-10-22T15:54Z, merge commitc8f4c4390, through the merge queueorigin/mainc8f4c4390 fix(objectql, driver-sql, spec): a stored expression that reads an autonumber stores the value computed with the number (#25218)gh-readonly-queue/main/pr-25218-*gonecompletedby the PRRuling 6335312761 (letter A) is built. The queue dropped the PR once on a board-read failure (exit 3, note 6344549134); the re-queue landed it. State:
pm:dispatchedremoved in this act.Generated by Claude Code