Repository navigation
[finding] Unanchored autonumber counter readback diverges cross-side by design — engine reads the LAST digit run, driver-sql concatenates EVERY digit — and #6555's {0000} declared default makes "unanchored" the default shape #7287
Description
Activity
Findings triage — escalated to the decision box:
finding→needs-user-decision(domain:speckept, routed by the earlier centralized pass).- Trigger fired since filing: PR feat(spec): declare
{0000}as the contract default for format-less autonumber fields (#6555) #7265 merged at 06:51Z today —{0000}is now the declared default for format-less autonumber fields, and{0000}has no prefix/suffix, so the default authoring shape now resolves to the unanchored readback path where the two sides disagree (enginereadStoredAutonumberCounter,packages/objectql/src/engine.ts:379, takes the last digit run; driver-sqlscanMaxNumericTailconcatenates every digit; spec's sharedreadAutonumberCounter,packages/spec/src/data/autonumber-format.ts:343, deliberately returnsundefinedfor that slot rather than picking a side). All three anchors re-verified live onorigin/main@f3f855a. - Why the decision box: the card's three treatments are a public-contract fork — (1) rule which unanchored reading IS the contract and hoist it into
readAutonumberCounter(a behavior change on one side, refused as a rider by refactor(spec,objectql,driver-sql): share the autonumber counter readback as spec's inverse of renderAutonumber (#6560) #7247 for exactly that reason), (2) declare mixed-content unanchored values out-of-contract in the shared TSDoc, or (3) record-only. No ruling exists on which reading is the contract; queueing any option would pre-decide it. The divergence bites only unanchored fields whose stored values carry non-digit content (legacy/migrated data), but that narrow face is now attached to the default declared shape.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Trigger fired since filing: PR feat(spec): declare
Maintainer ruling (2026-08-10, given directly to this session under the expedite instruction 「6555 及相关任务插队处理」): 宣告边界 — treatment (2), declare the boundary.
Maintainer's selection, verbatim from the decision prompt: 「宣告边界(推荐)」 — 在共享
readAutonumberCounter的 TSDoc 把「含非数字内容的无锚存量值」宣告为契约外, 回读维持undefined。零行为移动, 两侧现状都不动。Consequences, recorded for the implementing dev:
- Mixed-content (non-digit-carrying) stored values on unanchored formats are OUT OF CONTRACT for counter readback.
readAutonumberCounter's deliberateundefinedfor that slot is the contract, not a gap. - Neither consumer moves: engine
readStoredAutonumberCounter(last digit run) and driver-sqlscanMaxNumericTail(concatenate every digit) each keep their current behavior as implementation detail outside the contract boundary; both get a comment pointing at the declared boundary. - Options (1a)/(1b) — hoisting either reading into the shared helper — are REJECTED by this ruling (consistent with refactor(spec,objectql,driver-sql): share the autonumber counter readback as spec's inverse of renderAutonumber (#6560) #7247's refusal of the hoist as a rider). Option (3) record-only is superseded.
CLAIM — spec-lane PM seat (#6017), session
session_01PiRUoQkTSBBmpyXBY3cVn2. Branch:claude/issue-7287-unanchored-readback-boundary. Dispatching a cloud dev session (model: Opus — TSDoc/comment + pin-test card, zero behavior movement; the one discipline is that the diff must move nothing executable in the two consumers).needs-user-decision→pm:dispatched.
Generated by Claude Code
- Mixed-content (non-digit-carrying) stored values on unanchored formats are OUT OF CONTRACT for counter readback.
OS-DEV-REPORT — dev seat, cloud session for #7287 (dispatched by spec-lane PM seat #6017 under 「6555 及相关任务插队处理」).
Outcome: CI-green, draft PR #7405 open and waiting on the PM to land it. Auto-merge deliberately NOT enabled.
{ "issue": 7287, "outcome": "ci_green_awaiting_pm_merge", "branch": "claude/issue-7287-unanchored-readback-boundary", "pr_number": 7405, "premise_check": { "verified_on": "origin/main @ dadf67a (PM triage verified at f3f855a; main has since advanced, all three anchors unchanged)", "verified_before_any_edit": true, "anchors": [ { "claim": "spec's shared reader returns undefined for the unanchored slot", "location": "packages/spec/src/data/autonumber-format.ts:344", "quoted_content": "if (prefix === '' && suffix === '') return undefined;", "holds": true }, { "claim": "engine reads the LAST digit run", "location": "packages/objectql/src/engine.ts:379-386 (readStoredAutonumberCounter, exactly the ~:379 the card cited — no re-anchoring needed)", "quoted_content": "const runs = value.match(/\\d+/g); const digits = runs ? runs[runs.length - 1] : undefined;", "holds": true }, { "claim": "driver-sql concatenates EVERY digit", "location": "packages/drivers/driver-sql/src/sql-driver.ts:3669 (scanMaxNumericTail, unanchored else-arm)", "quoted_content": "n = parseInt(v.replace(/[^0-9]/g, ''), 10);", "holds": true }, { "claim": "both consumers' unanchored readings are pinned by their own existing tests", "location": "packages/objectql/src/engine-autonumber-seed-suffix.test.ts and packages/drivers/driver-sql/src/sql-driver-autonumber-suffix.test.ts, both section (4) 'legacy unanchored reading'", "quoted_content": "// ------------------------------------------- (4) legacy unanchored reading --", "holds": true } ] }, "changes": { "files": 4, "insertions": 241, "deletions": 0, "spec_tsdoc": "packages/spec/src/data/autonumber-format.ts — new section on readAutonumberCounter declaring mixed-content unanchored values OUT OF CONTRACT, undefined is the contract not a gap; records both consumers' divergent readings with the issue's worked example, why the hoist is rejected (#7247's rider refusal), the #6555/PR #7265 reachability, and links the ruling comment 5238560707", "pin_tests": "packages/spec/src/data/autonumber-unanchored-boundary.test.ts (NEW file — no existing test edited): 8 mixed-content shapes assert undefined with the boundary quoted in each test name; 4 two-digit-run shapes assert the two readings DIVERGE while spec declines; in-contract green cases assert pure-digit unanchored values read the SAME number on both sides, incl. a round-trip over renderAutonumber's own {0000} output at seq 0/7/42/9999/123456; a pin that {0000} really renders prefix==='' && suffix===''; a scoping guard that anchored formats are untouched", "consumer_comments": "packages/objectql/src/engine.ts (readStoredAutonumberCounter TSDoc) and packages/drivers/driver-sql/src/sql-driver.ts (scanMaxNumericTail TSDoc) each gain a section labelling their unanchored reading implementation detail OUTSIDE the declared boundary, pointing at the spec TSDoc and the pin file", "zero_behavior_proof": "Filtering the diff's added lines outside test files to non-comment, non-blank returns NOTHING. Zero deletions across the entire diff, so nothing existing was reworded or moved either. All three anchors byte-identical to main. No *.test.ts existing file touched.", "changeset": "none — skip-changeset label applied, following precedent PR #7368 (for #6734); this PR declares no release of its own, the exemption's own wording in pr-automation.yml. Check Changeset ran and concluded SUCCESS (label was live in time, no #5580/#6378 stale-red)." }, "gates": { "local": { "packages/spec pnpm test": "366 files / 9560 tests passed", "packages/objectql pnpm test": "172 files / 3066 tests passed", "packages/drivers/driver-sql pnpm test": "81 passed + 4 skipped files, 1156 passed + 48 skipped tests", "tsc --noEmit (spec, objectql, driver-sql)": "clean, exit 0 each", "pnpm check:changeset-gate-self-tests": "pass (118 + 142 + 117 assertions)", "pnpm check:role-word": "pass (44 baselined files, no new occurrences)", "pnpm check:nul-bytes": "pass (6788 text files scanned)" }, "ci": "GREEN — 35 check runs on faeee94, zero failures. Notable: Test Core (all 3 shards + rollup), TypeScript Type Check, ESLint, Build Core, Dogfood Regression Gate (all 3 shards + rollup), Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), Check Changeset, Spec property liveness, ADR maintainer approval, Check PR Size, Check Documentation Links, Console Pin Freshness, 'No other open PR may claim the same issue' — all success; Build Docs / Console Pin Gate skipped by path filter." }, "deviations": [ { "what": "The ruling's wording ('含非数字内容的无锚存量值') is strictly WIDER than the actual divergence, and the implementation adopts the ruling's wording rather than the narrower set.", "detail": "The two readings only differ when a value carries TWO OR MORE digit runs. Mixed-content values with a single run agree on both sides — 'CASE-12' reads 12 and 'INV/0042' reads 42 either way. Two initial pins written on the assumption 'mixed content ⇒ divergence' FAILED and were corrected rather than papered over.", "resolution": "Boundary drawn at the ruling's wording (by CONTENT), with the reason written into both the TSDoc and a dedicated pin: narrowing it to 'values the sides actually disagree on' would make membership undecidable from the value alone — a caller would have to know which consumer is running to know whether its input is in contract, which is the exact 'same metadata, different driver, different number' property this boundary exists to bound. Sharpens the ruling rather than departing from it; no behavior depends on the choice. Flagged here for the PM in case the maintainer wants the narrower reading recorded instead." }, { "what": "Environment note, not a code deviation.", "detail": "In a fresh worktree both consumer suites fail wholesale at import until workspace deps are built ('Cannot find package @objectstack/spec/data', then 'Failed to resolve entry for package @objectstack/types'). Fixed by `pnpm --filter \"@objectstack/objectql^...\" --filter \"@objectstack/driver-sql^...\" build`; the reported numbers are post-build. Recorded in the PR so a reviewer reproducing does not read it as a regression." } ], "findings_filed": [], "docs_adr_touched": false, "open_questions": [ "#7262 (engine de-fallback) is in flight and touches applyAutonumbers in engine.ts. No collision: this PR's engine edit is a TSDoc block above readStoredAutonumberCounter, a different function ~1,300 lines away. The diff is comment-only regardless, so any textual conflict resolves by keeping both sides — no coordination needed unless the PM wants a merge order.", "Auto-merge NOT enabled, per dispatch. PR #7405 is a DRAFT and needs the PM to mark it ready and land it." ] }
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Oct 7, 2026
Observation-class finding, surfaced during #6560's implementation (OS-DEV-REPORT on that card) and filed by the spec-lane PM seat (#6017, session
session_01PiRUoQkTSBBmpyXBY3cVn2) so the deliberately-preserved divergence has a tracking record now that its reachability changes. Unassigned, unrouted — for triage.The fact (measured by the #6560 dev, both sides pinned by their own existing tests)
For the anchored case (non-empty prefix/suffix) the counter readback rule is now shared:
readAutonumberCounterinpackages/spec/src/data/autonumber-format.ts(PR #7247), both sides call it. For the unanchored case (both affixes empty) the two sides deliberately differ and PR #6553 preserved both byte-for-byte:packages/objectql/src/engine.ts,readStoredAutonumberCounterlegacy arm): takes the last digit run —'SO-2024-0007'→7;scanMaxNumericTailunanchored arm): concatenates every digit —'SO-2024-0007'→20240007.Spec's shared export returns
undefinedfor the unanchored slot rather than picking a side — correctly refusing to claim an agreement that does not exist (#7247's recorded rationale).Why file it now
#6555's Route-3 ruling (PR #7265, in flight) makes
{0000}the declared default for format-less autonumber fields — and{0000}has no prefix/suffix, so the default authoring shape resolves to the unanchored readback path on both sides. For pure-digit stored values the two readings agree ('0007'→ 7 both ways); they diverge only on unanchored fields whose stored values carry non-digit content (legacy/migrated data, e.g. dash-separated strings in a field later declared autonumber). That is narrow but it is the same "same metadata, different driver, different number" class as #6555 itself — one seam over (readback instead of render).Suggested treatments (for triage, no recommendation pre-empted)
readAutonumberCounter(a behavior change on one side — the reason refactor(spec,objectql,driver-sql): share the autonumber counter readback as spec's inverse of renderAutonumber (#6560) #7247 refused to do it as a rider);Dedup
readAutonumberCounter/scanMaxNumericTail/ unanchored: only #6560 (closed by PR #7247, which scoped this out explicitly) and #6555 (render-side fork, different function). The #6553 PR documented the preservation but no open card tracks the divergence itself.Refs: #6560 (OS-DEV-REPORT + PR #7247), #6553, #6555 (PR #7265), #6468.