Skip to content

docs(spec,objectql,driver-sql): declare the unanchored autonumber readback boundary (#7287) - #7405

Queued
os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-7287-unanchored-readback-boundary
Queued

docs(spec,objectql,driver-sql): declare the unanchored autonumber readback boundary (#7287)#7405
os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-7287-unanchored-readback-boundary

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #7287

The ruling this executes

Maintainer ruling on #7287, comment 5238560707 (2026-08-10), quoted verbatim:

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 deliberate undefined for that slot is the contract, not a gap.
  • Neither consumer moves: engine readStoredAutonumberCounter (last digit run) and driver-sql scanMaxNumericTail (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.

Premise check — all three anchors verified on origin/main @ dadf67a, before any edit

Quoted by content, as they stand on main (and as they still stand after this PR — none of the three moves).

Anchor Location on main Quoted content Holds?
Spec's shared reader returns undefined for the unanchored slot packages/spec/src/data/autonumber-format.ts:344 if (prefix === '' && suffix === '') return undefined;
Engine reads the last digit run packages/objectql/src/engine.ts:379-386 const runs = value.match(/\d+/g);const digits = runs ? runs[runs.length - 1] : undefined;
driver-sql concatenates every digit packages/drivers/driver-sql/src/sql-driver.ts:3669 (unanchored else arm) n = parseInt(v.replace(/[^0-9]/g, ''), 10);

The card cited the engine reader at ~:379 — that is exactly where it sits on current main, so no re-anchoring was needed. The PM's triage comment verified these at f3f855a; main has since advanced to dadf67a and 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.tsreadAutonumberCounter's TSDoc declares the boundary. A new section after the existing "What this deliberately does NOT decide", stating that the refusal is the contract:

On an UNANCHORED format (prefix === '' && suffix === ''), a stored value carrying any non-digit content is out of contract for counter readback. undefined is this function's answer for that slot, permanently.

It records why declare rather than pick: both consumers' readings are quoted with the issue's own worked example ('SO-2024-0007'7 in the engine, 20240007 in 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 the undefined:

3. Comment-only cross-references at the two consumer sites. readStoredAutonumberCounter (packages/objectql/src/engine.ts) and scanMaxNumericTail (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 read 12 and 42 on 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 --stat against main: 4 files, 241 insertions, 0 deletions.

 packages/drivers/driver-sql/src/sql-driver.ts                  |  22 ++++
 packages/objectql/src/engine.ts                                |  21 +++
 packages/spec/src/data/autonumber-format.ts                    |  51 +++++++
 packages/spec/src/data/autonumber-unanchored-boundary.test.ts  | 147 ++++++++++++++++++ (new)

Filtering the diff's added lines outside test files to those that are not comment or blank returns nothing:

git diff -U0 main -- ':!*.test.ts' | grep '^+' | grep -v '^+++' | sed 's/^+//' \
  | grep -vE '^\s*(\*|/\*|//|\*/)' | grep -vE '^\s*$'
→ (no output)

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

Gate Result
packages/specpnpm test 366 files / 9560 tests passed
packages/objectqlpnpm test 172 files / 3066 tests passed
packages/drivers/driver-sqlpnpm 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)

Note for anyone reproducing: the two consumer suites fail wholesale at import in a fresh worktree until pnpm --filter "@objectstack/objectql^..." --filter "@objectstack/driver-sql^..." build has run (Cannot find package '@objectstack/spec/data', then Failed to resolve entry for package "@objectstack/types"). That is a build prerequisite, not a regression — the numbers above are post-build.

Changeset

None — skip-changeset applies, 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 in pr-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


Generated by Claude Code

…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
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 10:09am

Request Review

@os-zhuang os-zhuang added documentation Improvements or additions to documentation skip-changeset PR has no user-facing published change; bypasses the changeset gate domain:spec labels Aug 10, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-sql, @objectstack/objectql, @objectstack/spec.

111 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-sql)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/driver-sql, @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/objectql, packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-sql, @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/driver-sql, @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-sql, packages/objectql, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/driver-sql, @objectstack/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation domain:spec protocol:data size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants