Skip to content

无 format 的 autonumber 字段两侧渲染不同:driver-sql 兜底成 {0000} 发出 0001,引擎兜底路径发出裸 1 —— 同一份元数据换驱动号形不同 #6555

Closed

Description

@baozhoutao

发现于 #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 该长什么样」这个契约问题:

  1. 引擎跟随 driver-sql,空格式也兜底成 {0000};
  2. driver-sql 跟随引擎,去掉 || '{0000}',无格式即裸计数器 —— 但这会改变已落地
    数据
    的号形(现存 0001 之后接 2),需要兼容判断;
  3. 在 spec 侧把默认写进契约(例如 FieldSchema 对 autonumber 给出 format 的
    声明式默认),让「declared = enforced」,两侧都没有自己的兜底。

⚠️ 与 #6468(PR #6553,播种解析)同区域但不同函数:#6468 动的是 seedAutonumber /
scanMaxNumericTail 的读回,本条在 applyAutonumbers / initObjects 的格式
兜底
。#6468 落地后再动本条不会冲突。

Activity

  1. added
    domain:specDomain lane — seat card indexed by label:pm:seat
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 8, 2026
  2. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    Triage: needs-user-decision + domain:spec.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    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

  4. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 8, 2026
  5. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    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"):


    Generated by Claude Code

  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 10, 2026
  7. self-assigned this
    on Aug 10, 2026
  8. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    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

  9. added
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 10, 2026
  10. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    Spec half LANDED — card stays open, now pm:blocked on 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 on main; 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

  11. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    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):

    half card PR merged
    spec — declared {0000} contract default (DEFAULT_AUTONUMBER_FORMAT + resolveAutonumberFormat + .meta({default}) + DEFAULT_CHANGES_BY_MAJOR[17]) this card (spec half) #7265 06:51Z
    driver-sql — both hardcoded || '{0000}' sites read the contract (behavior-neutral, proven by a 484-document differential harness) #7263 #7334 08:40Z
    engine — applyAutonumbers reads the contract; format-less and '' fields now render 0001-shaped; empty-canonical-masks-shorthand edge fixed; the '' coverage gap (zero tests on either side) closed #7262 #7433 11:41Z

    The 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

  12. removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:specDomain lane — seat card indexed by label:pm:seat

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions