docs(spec,objectql,driver-sql): declare the unanchored autonumber readback boundary (#7287) - #7405
Conversation
…ary (#7287) Maintainer ruling on #7287 (2026-08-10, treatment (2) 「宣告边界」): on an UNANCHORED autonumber format (neither prefix nor suffix), a stored value carrying non-digit content is OUT OF CONTRACT for counter readback, and `readAutonumberCounter`'s `undefined` for that slot is the contract rather than a gap. Zero behavior movement. The two consumers keep their divergent legacy readings — the engine's `readStoredAutonumberCounter` takes the last digit run, driver-sql's `scanMaxNumericTail` concatenates every digit — each now labelled implementation detail outside the declared boundary, pointing at spec's TSDoc. Hoisting either reading was rejected: it would move live behavior on the other side over record numbers already issued (the reason #7247 refused the hoist as a rider). The boundary is drawn by CONTENT and deliberately wider than the observed divergence: pure-digit values (what `renderAutonumber` emits for an unanchored format) are in contract and read the same on both sides, while every mixed-content value is outside — including ones the two readings happen to agree on, since divergence needs two digit runs. Narrowing it to "values the sides actually disagree on" would make membership undecidable from the value alone. Reachability: #6555's Route-3 ruling (PR #7265) made `{0000}` the declared default for format-less autonumber fields, so the default authoring shape now lands in this unanchored slot. New pins in `autonumber-unanchored-boundary.test.ts` assert the `undefined` for eight mixed-content shapes, show the two readings diverging on the inputs the boundary excludes and agreeing on the ones it admits, and pin that `{0000}` really renders an unanchored pair. No existing test is edited. Diff is comments and one new test file only — no executable line is added, removed or moved outside tests. Closes #7287 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184Hrx9PcaQ2KMMt88DRZ2c
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 3 package(s): 111 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
Closes #7287
The ruling this executes
Maintainer ruling on #7287, comment 5238560707 (2026-08-10), quoted verbatim:
Premise check — all three anchors verified on
origin/main@dadf67a, before any editQuoted by content, as they stand on
main(and as they still stand after this PR — none of the three moves).mainundefinedfor the unanchored slotpackages/spec/src/data/autonumber-format.ts:344if (prefix === '' && suffix === '') return undefined;packages/objectql/src/engine.ts:379-386const runs = value.match(/\d+/g);…const digits = runs ? runs[runs.length - 1] : undefined;packages/drivers/driver-sql/src/sql-driver.ts:3669(unanchoredelsearm)n = parseInt(v.replace(/[^0-9]/g, ''), 10);The card cited the engine reader at
~:379— that is exactly where it sits on currentmain, so no re-anchoring was needed. The PM's triage comment verified these atf3f855a;mainhas since advanced todadf67aand all three are unchanged.Both consumers also still carry their own pins for the unanchored readings (
packages/objectql/src/engine-autonumber-seed-suffix.test.ts§4 "legacy unanchored reading",packages/drivers/driver-sql/src/sql-driver-autonumber-suffix.test.ts§4 of the same name), so the divergence this boundary bounds is test-enforced on both sides and cannot drift silently.What changed
1.
packages/spec/src/data/autonumber-format.ts—readAutonumberCounter's TSDoc declares the boundary. A new section after the existing "What this deliberately does NOT decide", stating that the refusal is the contract:It records why declare rather than pick: both consumers' readings are quoted with the issue's own worked example (
'SO-2024-0007'→7in the engine,20240007in the driver), and ruling either to be the contract would move live behavior on the other side over record numbers that are not reclaimable once issued — the reason #7247 refused the hoist as a rider. It cross-references #7287 and links the ruling comment.2. Pin tests —
packages/spec/src/data/autonumber-unanchored-boundary.test.ts(new file). Every case names the boundary in its test name and asserts theundefined:undefinedagainst a bare{0000}—'A1B2','CASE-12','SO-2024-0007','2026-06-17','INV/0042','0007 (void)','v2.1','DRAFT';'0007','10','0000','123456') read the same number on both sides, plus a round-trip over whatrenderAutonumberitself emits for{0000}at seq 0/7/42/9999/123456;{0000}(the 无 format 的 autonumber 字段两侧渲染不同:driver-sql 兜底成{0000}发出0001,引擎兜底路径发出裸1—— 同一份元数据换驱动号形不同 #6555 / PR feat(spec): declare{0000}as the contract default for format-less autonumber fields (#6555) #7265 declared default) really does renderprefix === '' && suffix === ''— the premise that makes this boundary reachable from the default authoring shape;readAutonumberCounter('CASE-0007', 'CASE-', '')still reads7, mixed content and all.3. Comment-only cross-references at the two consumer sites.
readStoredAutonumberCounter(packages/objectql/src/engine.ts) andscanMaxNumericTail(packages/drivers/driver-sql/src/sql-driver.ts) each gain a TSDoc section stating that their unanchored reading is implementation detail outside the declared boundary, not a promise the platform makes, pointing at spec's TSDoc and at the new pin file. Neither file changes executably.One thing the ruling's wording underdetermined, resolved and written down
The ruling declares "含非数字内容的无锚存量值" (mixed-content unanchored values) out of contract. Implementing it surfaced that mixed content is strictly wider than actual divergence: the two readings only differ when a value carries two or more digit runs, so
'CASE-12'and'INV/0042'are mixed-content yet read12and42on both sides. Two initial pins failed on exactly this and were corrected rather than papered over.The boundary is drawn at the ruling's wording — by content — and the TSDoc plus a dedicated pin now say why: narrowing it to "values the two sides actually disagree on" would make membership undecidable from the value alone, since a caller would have to know which consumer is running to know whether its input is in contract. That is the very "same metadata, different driver, different number" property #7287 exists to bound. Recorded here because it sharpens the ruling rather than departing from it; no behavior depends on the choice.
Zero behavior movement — the proof
git diff --statagainstmain: 4 files, 241 insertions, 0 deletions.Filtering the diff's added lines outside test files to those that are not comment or blank returns nothing:
Zero deletions across the whole diff, so nothing existing was reworded or moved either. The three anchors are byte-identical to
main. No existing test file is touched — the pins are a new file, so the three green suites below ran against unmodified test code.Gates
packages/spec—pnpm testpackages/objectql—pnpm testpackages/drivers/driver-sql—pnpm testtsc --noEmit— spec, objectql, driver-sqlpnpm check:changeset-gate-self-testspnpm check:role-wordpnpm check:nul-bytesNote for anyone reproducing: the two consumer suites fail wholesale at import in a fresh worktree until
pnpm --filter "@objectstack/objectql^..." --filter "@objectstack/driver-sql^..." buildhas run (Cannot find package '@objectstack/spec/data', thenFailed to resolve entry for package "@objectstack/types"). That is a build prerequisite, not a regression — the numbers above are post-build.Changeset
None —
skip-changesetapplies, following the precedent of PR #7368 (for #6734), a comment-only PR of the same class. This PR ships no behavior and therefore declares no release of its own, which is the exemption's own wording inpr-automation.yml("this PR declares no release of its own"). The gate has no path-based auto-exemption, so the label is the mechanism; its self-tests were run and pass, confirming which branch applies. The label is applied on this PR — note the known label-latency race (#5580 / #6378), which the gate's settling re-read closes.Coordination and boundaries
{0000}default instead of parsing the empty string (#6555 half 2/3) #7262 (engine de-fallback) is in flight and touchesapplyAutonumbersinengine.ts. This PR's engine edit is a TSDoc block abovereadStoredAutonumberCounter(a different function, ~1,300 lines away fromapplyAutonumbers); the regions do not collide, and the diff is comment-only in any case, so a textual conflict would resolve by keeping both sides.docs/adr/**untouched. ⛔content/docs/releases/untouched, per the binding rules.Generated by Claude Code