Description
发现于 #6468 的实现(PR #6553)途中,未在该 PR 内顺手修 —— #6468 修的是播种时
怎么从已存值里读回计数器,本条是渲染兜底:两侧对「字段没写 format」这件事
给出的默认格式不同。机制不同、代码位置不同,按 Prime Directive #10 单独开单。
缺陷
autonumberFormat / format 在字段上是可选的。两侧对缺省的处理不一致:
- driver-sql 把空格式兜底成
{0000}:
packages/drivers/driver-sql/src/sql-driver.ts:4322与:4404
同一行const fmt = rawFmt || '{0000}';(两处分别在initObjects与外部对象注册路径)。
渲染结果是 4 位零填充 ——0001、0002、… - 引擎兜底路径(驱动不声明
supports.autonumber时)不兜底,直接用空格式:
packages/objectql/src/engine.ts:2115
const tokens = parseAutonumberFormat(typeof fmt === 'string' ? fmt : '');
空 token 列表在renderAutonumber里走width === null分支
(packages/spec/src/data/autonumber-format.ts:210-212),渲染出裸计数器 ——1、2、…
实测
在 #6468 的工作树上,同一份元数据 { rec_no: { type: 'autonumber' } }(无 format),
库内已有 1 / 2 / 10 三行:
SqlDriver.create()⇒rec_no = '0011'- 引擎兜底路径(
InMemoryDriver,supports = {})⇒rec_no = '11'
两侧的计数器都是 11(播种一致,#6468 已 pin 住这一点),分歧纯粹在渲染宽度。
PR #6553 的 packages/drivers/driver-sql/src/sql-driver-autonumber-suffix.test.ts
里那条 legacy 用例已把 driver 侧的 '0011' 显式 pin 住,并在注释里点名这条分歧,
以免下一个读者把它误当成 #6468 的一部分。
可达性
不需要任何异常写法 —— 声明 { type: 'autonumber' } 且不写 format 即可。典型暴露面:
开发/测试跑 memory 驱动、生产跑 SQL 驱动,同一份元数据在两处发出的单号字面不同
(1 vs 0001),测试里断言的号形到了生产就不成立;数据迁移或换驱动时,同一对象
的历史号形也会在切换点前后突变。
供分诊参考(不预设结论)
三条路各有代价,需要先定「无 format 的 autonumber 该长什么样」这个契约问题:
- 引擎跟随 driver-sql,空格式也兜底成
{0000}; - driver-sql 跟随引擎,去掉
|| '{0000}',无格式即裸计数器 —— 但这会改变已落地
数据的号形(现存0001之后接2),需要兼容判断; - 在 spec 侧把默认写进契约(例如
FieldSchema对 autonumber 给出format的
声明式默认),让「declared = enforced」,两侧都没有自己的兜底。
seedAutonumber /
scanMaxNumericTail 的读回,本条在 applyAutonumbers / initObjects 的格式
兜底。#6468 落地后再动本条不会冲突。
Activity
os-zhuang commentedon Aug 8, 2026
Triage:
needs-user-decision+domain:spec.1to0001, route 2 changes driver-sql's already-landed numbering mid-sequence, route 3 writes the default into the contract. "What does a format-less autonumber render as" is a product-semantics / public-contract call with stored-data continuity implications ⇒ maintainer inbox, not the queue. No prior ruling found: 自增号格式带「序号槽之后的后缀」时,播种解析读错数字段:引擎读成年份(2026),driver-sql 读成拼接串(12026),两侧还互不一致 #6468's scope is counter seeding, and the open decision card 决策:自增号「计数器反解」规则是否作为 renderAutonumber 的逆进 packages/spec(PR #6553 的 open question,维护者裁) #6560 is about the counter-inverse rule — neither answers the render default.origin/main@f549a0d:packages/drivers/driver-sql/src/sql-driver.ts:4330/:4412—const fmt = rawFmt || '{0000}';(both sites);packages/objectql/src/engine.ts:2115parses the empty string when no format; thewidth === nullbranch inpackages/spec/src/data/autonumber-format.tsrenders the bare counter and its own comment calls that "legacy behaviour". Body's cited lines (:4322/:4404) drifted; code unchanged.renderAutonumberand any declared default live inpackages/spec/src/data/, and a declared default changes normative metadata semantics ⇒domain:specunder the acceptance-face criterion (not spec-surface). If the ruling picks route 1 or 2, the execution card re-routes to engine-core/drivers at that point.readonly契约的「表单不可编辑」那一半对单号字段不成立 #5628, insert 路径 stripRuntimeOwnedFields 同样删的是 hook 覆写后的当前值 —— beforeInsert hook 写入的 autonumber 在调用方也提交了同名键时被连坐删除(与其自身文档矛盾) #6339 — none covers the render-fallback divergence.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 8, 2026
Maintainer ruling (2026-08-08): Route 3, with the default fixed at {0000}. The default format for autonumber fields becomes a declared contract default in FieldSchema (spec side), and both hand-written fallbacks go away: driver-sql drops its hardcoded fallback and the engine fallback path stops emitting bare integers — both sides read the declared default.
Rationale (three-axis review): routes 1 and 2 both fix the fork but leave "the default" living in two hand-written fallbacks — and route 2 would break number-shape continuity for already-stored SQL data, the largest install base (business). Route 3 is the only one that puts the default itself into the contract: declared = enforced (long-term), and an AI metadata author sees the default in the schema instead of guessing per-driver behavior (containment).
Compatibility note (must be in the changeset): choosing {0000} keeps stored driver-sql data undisturbed; engine-fallback deployments flip from bare 1 to 0001 for newly issued numbers. Counter continuity itself is unaffected (#6468 pinned it).
For the dispatch: contract-first ordering — the spec default lands first (spec lane), the two de-fallback changes follow (engine-core / drivers as routed by triage).
State: needs-user-decision removed; pm:queue restored — spec lane leads.
Maintainer directive (verbatim, covering all 14 inbox cards): 「你的建议全部接受」. Recorded by PM session session_01JaVVMrSxt7Tgi1uwEuDtH7.
Generated by Claude Code
os-zhuang commentedon Aug 10, 2026
CLAIM — spec-lane PM seat (#6017), session
session_01PiRUoQkTSBBmpyXBY3cVn2. Branch:claude/issue-6555-autonumber-default-format. Dispatching a cloud dev session now, executing the maintainer's 2026-08-08 Route-3 ruling above.Re-confirmation: the maintainer re-affirmed the number shape today, 2026-08-10 (verbatim): 「统一为 {0000}(推荐)」 — consistent with the standing ruling; no conflict.
Scope of THIS dispatch — the spec half only, per the ruling's contract-first ordering ("the spec default lands first (spec lane), the two de-fallback changes follow"):
{0000}for format-less autonumber fields lands inpackages/spec(FieldSchema-level declaration + exported default constant besiderenderAutonumberinautonumber-format.ts, exact mechanics per house style), with pins and the gen chain.packages/objectql/src/engine.ts:2115; driver-sqlsql-driver.ts:4330/:4412dropping|| '{0000}') are NOT in this card's PR — the dev files them as two follow-up cards (unassigned, unrouted — triage routes to engine-core / drivers per the ruling), each Blocked-by this card, so no lane window opens where declared and enforced disagree silently. The PR says "Part of 无 format 的 autonumber 字段两侧渲染不同:driver-sql 兜底成{0000}发出0001,引擎兜底路径发出裸1—— 同一份元数据换驱动号形不同 #6555"; the card stays open until the de-fallback halves land.Generated by Claude Code
os-zhuang commentedon Aug 10, 2026
OS-DEV-REPORT
{ "issue": 6555, "outcome": "pr_opened", "branch": "claude/issue-6555-autonumber-default-format", "pr_number": 7265, "premise_check": { "base": "origin/main @ 3566e55", "verdict": "all premises hold — nothing had landed ahead of the card", "driver_sql_fallback": "PRESENT at sql-driver.ts:5353 and :5435 (both `const fmt = rawFmt || '{0000}';`). Line numbers DRIFTED again from triage's :4330/:4412 (and the body's :4322/:4404); code text unchanged.", "engine_fallback": "PRESENT at engine.ts:2553-2554 (`autonumberFormat ?? format`, then `parseAutonumberFormat(typeof fmt === 'string' ? fmt : '')`). NOT at :2115 — that line is now audit-hook session code.", "bare_counter_branch": "PRESENT at autonumber-format.ts:210-213, comment still reads 'legacy behaviour'", "fieldschema_default_absent": true }, "declaration_mechanics_chosen": "JSON-Schema `default` annotation via `.meta({ description, default: DEFAULT_AUTONUMBER_FORMAT })` on FieldSchema.autonumberFormat — house style (approval.zod.ts, schemaless-node-config.zod.ts) — PLUS the exported constant `DEFAULT_AUTONUMBER_FORMAT` and a resolver `resolveAutonumberFormat(field)` in autonumber-format.ts, PLUS registration in DEFAULT_CHANGES_BY_MAJOR (the #4666 ratchet). Zod `.default()` was measured and REJECTED. Parse output is byte-identical for every field type.", "blast_radius_measured": { "option_rejected": "z.string().optional().default('{0000}')", "method": "applied to a scratch tree, spec suite + a direct parse probe run, then reverted and the same test file re-run as control", "finding_1": "`autonumberFormat` is FLAT on FieldSchema, shared by all 49 field types — not scoped to type:'autonumber'. Measured: FieldSchema.parse({type:'text',label:'Name'}) returns …,\"autonumberFormat\":\"{0000}\",\"externalId\":false} — a counter format on a text field. Parse output is read by metadata loaders, the metadata API, and both drivers' initObjects.", "finding_2": "28 spec tests go red, all in packages/spec/scripts/build-schemas-check-mode.test.ts (28 failed | 9369 passed). Control run on the same tree with the patch reverted: 60 passed in that file — the reds are caused by the patch, not pre-existing.", "finding_3": "z.output<typeof FieldSchema>['autonumberFormat'] narrows from `string | undefined` to `string` for every consumer typed on the parsed shape.", "chosen_option_not_blocked": "the annotation route breaks nothing measurable — no STOP condition was reached", "corroboration": "gen:schema REFUSED the build until the default change was declared ('data/Field:autonumberFormat: (none) → \"{0000}\" (added)'), so the repo's own machinery treats the annotation as the contract default — declared = enforced" }, "changes": [ "packages/spec/src/data/autonumber-format.ts — new DEFAULT_AUTONUMBER_FORMAT ('{0000}') beside renderAutonumber; new resolveAutonumberFormat(field) + AutonumberFormatSource; TSDoc note on the width===null bare-counter branch", "packages/spec/src/data/field.zod.ts — autonumberFormat declares the default via .meta({description, default}); TSDoc on omitting the key and on why an annotation, not .default()", "packages/spec/scripts/lib/default-changes.ts — declares data/Field:autonumberFormat: (none) → \"{0000}\" under major 17 with a consumer-facing reason", "packages/spec/src/data/autonumber-format.test.ts — +6 cases", "packages/spec/src/data/field-autonumber-default-format.test.ts — NEW, +4 cases pinning the FieldSchema contract incl. parse-output-unchanged", ".changeset/autonumber-default-format-contract.md — minor on @objectstack/spec, compatibility note verbatim", "regenerated: authorable-defaults/data.json, api-surface/data.json, export-origins/data.json, content/docs/references/data/field.mdx" ], "precedence_decision": "resolveAutonumberFormat treats anything that is not a NON-EMPTY string as undeclared — driver-sql's truthiness rule, not the engine's `??`. The two also disagreed on `format: ''`; resolving '' to the default is the direction that leaves stored SQL numbers untouched, per the ruling's compatibility note. Consequence: #7263 (driver) is behaviour-neutral by construction, #7262 (engine) carries the whole behaviour move.", "gates": { "spec_build": "PASS — run BEFORE every generator (#7122 stale-dist trap)", "gen_schema": "PASS after declaring the default; the FIRST run correctly refused the undeclared default change", "check_generated": "PASS — all 11 generated artifacts up to date (3 stale → --fix → re-verified clean)", "dual_snapshot": "PASS — api-surface/ AND export-origins/ both regenerated after the real build; +3 exports, 0 removed", "spec_typecheck": "PASS (incl. check:test-typecheck; debt unchanged)", "spec_test": "PASS — 361 files / 9407 tests", "driver_sql_autonumber": "PASS — all 8 sql-driver-autonumber-* suites, 64 tests, UNCHANGED", "objectql_autonumber": "PASS — all 7 engine-autonumber-* suites, 97 tests, UNCHANGED", "runtime_cross_side_parity": "PASS — autonumber-seed-cross-side-parity.integration.test.ts, 5 tests, UNCHANGED", "heavy_lock": "flock /tmp/os-heavy-verify.lock on every heavy run — NO contention, every acquisition immediate (0s wait)", "reverse_verification": "direction predicted per pin BEFORE running, both sides run. Pre-change: 8 failed | 26 passed. Post-change: 34 passed. All 5 predictions correct. One mechanism mis-predicted: I expected the pre-change run to fail at IMPORT; instead the missing exports arrived as undefined and only the 8 asserting cases failed, leaving the 26 pre-existing pins green on both sides." }, "new_exports": [ "DEFAULT_AUTONUMBER_FORMAT (const) — src/data/autonumber-format.ts", "resolveAutonumberFormat (function) — src/data/autonumber-format.ts", "AutonumberFormatSource (interface) — src/data/autonumber-format.ts" ], "followups_filed": [ {"number": 7262, "title": "engine de-fallback — applyAutonumbers reads the declared default instead of parsing ''", "assignee": null, "routing_labels": "none (triage routes)", "blocked_by": "#6555 spec-half PR #7265", "note": "THIS is the half that moves behaviour: bare 1 → 0001 on engine-fallback deployments; must re-pin any engine test asserting a bare counter"}, {"number": 7263, "title": "driver-sql de-fallback — drop both `|| '{0000}'` sites for resolveAutonumberFormat", "assignee": null, "routing_labels": "none (triage routes)", "blocked_by": "#6555 spec-half PR #7265", "note": "behaviour-neutral by construction; all 8 driver autonumber suites must stay green UNEDITED"} ], "followup_dedup": "searched open issues for autonumber / autonumber fallback — only #6555 itself; no duplicate", "deviations": [ "The dispatch phrased the bare-branch note as 'the bare branch remains only for explicitly-empty formats'. That is not what the code does after this change: an explicitly-empty format resolves to the default too (truthiness rule, chosen for stored-data continuity). The branch now remains for a DECLARED format carrying no {0..0} slot ('CASE-' → CASE-1). The TSDoc and a pin state the accurate version.", "Did NOT update the authoring form help text (object.form.ts:187, 'leave blank → 0001') — it regenerates *.metadata-forms.generated.ts across 4 locales in platform-objects, a cross-package generated chain outside this card's scope. Flagged in the PR as an optional follow-up.", "Added packages/spec/scripts/lib/default-changes.ts to the diff — not named in the dispatch, but gen:schema hard-refuses the build without it (#4666 ratchet)." ], "docs_adr_touched": false, "releases_touched": false, "special_inspection_notes": [ "Line drift, THIRD occurrence: both anchor sets moved again (:4330/:4412 → :5353/:5435; :2115 → :2553). The follow-up cards carry re-measured numbers PLUS the code text so the next dev re-anchors by content.", "A declared-but-not-yet-enforced window is deliberately open between this PR and #7262/#7263: the contract says {0000} while the engine still emits bare 1. That is the ruling's contract-first ordering. #6555 must stay OPEN until both halves land.", "`format: ''` semantics move on the engine side too — a second, smaller behaviour change beyond the ruling's compatibility note. Called out in #7262; belongs in that card's changeset.", "PR is DRAFT, says 'Part of #6555' (not Closes), no auto-merge. Session is subscribed to PR activity and will drive CI to green." ] }Generated by Claude Code
os-zhuang commentedon Aug 10, 2026
Spec half LANDED — card stays open, now
pm:blockedon the two de-fallback halves. PR #7265 merged 2026-08-10T06:51:25Z (two-read confirmed). Per the Route-3 ruling's contract-first ordering, the declared default{0000}is onmain; the deliberately-open declared-but-not-yet-enforced window closes when #7262 (engine de-fallback — the behaviour-moving half) and #7263 (driver-sql de-fallback — behaviour-neutral by construction) land. Both are filed, Blocked-by discharged, awaiting triage routing to engine-core / drivers. This card closes on the second of the two; the unlock sweep should treat their merges as the restart signal.Generated by Claude Code
os-zhuang commentedon Aug 10, 2026
FAMILY COMPLETE — closing. Spec-lane PM seat (#6017), session
session_01PiRUoQkTSBBmpyXBY3cVn2.All three halves of the 2026-08-08 route-3 ruling are merged (each verified by two-read):
{0000}contract default (DEFAULT_AUTONUMBER_FORMAT+resolveAutonumberFormat+.meta({default})+ DEFAULT_CHANGES_BY_MAJOR[17])|| '{0000}'sites read the contract (behavior-neutral, proven by a 484-document differential harness)applyAutonumbersreads the contract; format-less and''fields now render0001-shaped; empty-canonical-masks-shorthand edge fixed; the''coverage gap (zero tests on either side) closedThe original defect — 同一份元数据换驱动号形不同 — is resolved: both sides read one declared default, the cross-side parity suite now pins shared rendering (format-less field, stored 1/2/10, both implementations mint
0011), and the ruling's compatibility note rides both behavior-moving changesets. Related boundary ruling #7287 (宣告边界, mixed-content unanchored readback out-of-contract) landed separately as PR #7405's TSDoc + pins.Executed under the maintainer's expedite instruction 「6555 及相关任务插队处理」.
Generated by Claude Code
feat(spec): declare `{0000}` as the contract default for format-less …
refactor(driver-sql): read the autonumber default from the contract, …
fix(objectql): the autonumber fallback reads the declared `{0000}` de…
docs(spec,objectql,driver-sql): declare the unanchored readback bound…
fix(spec): the protocol 16 → 17 conversion summaries and three descri…
test(spec): the first data/ file group's test titles state each cited…
test(spec): the second data/ file group's test titles state each cite…