Skip to content

refactor(spec,objectql,driver-sql): share the autonumber counter readback as spec's inverse of renderAutonumber (#6560) - #7247

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6560-read-autonumber-counter
Aug 10, 2026
Merged

refactor(spec,objectql,driver-sql): share the autonumber counter readback as spec's inverse of renderAutonumber (#6560)#7247
os-zhuang merged 1 commit into
mainfrom
claude/issue-6560-read-autonumber-counter

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What this does

packages/spec gains readAutonumberCounter(value, prefix, suffix) — the declared inverse of renderAutonumber — and the two hand-written copies of that readback (ObjectQL engine, SQL driver) are replaced by calls to it. Pure refactor: zero behaviour change, no authorable surface, no Zod, no new vocabulary.

Closes #6560

The rulings this executes

Three consistent maintainer rulings on #6560, quoted verbatim (Chinese lines untranslated):

Nothing was left to decide; this PR is execution only.

Premise re-measurement (on fresh origin/main @ 2e4274d)

The card's premise was two hand-written ~4-line readback copies, one per side, as PR #6553 left them. Re-measured before writing code — it has partly moved, and the finding strengthens the card rather than voiding it:

Side State on main today vs. the card's premise
packages/objectql The four lines were lifted into a module-local readAutonumberCounter (engine.ts) by #6806, which added a second engine consumer (adoptExplicitAutonumber, the adopt-on-exempt-write resync) alongside the seeding scan Moved: no longer inline, but still objectql's private copy
packages/drivers/driver-sql Still inline inside scanMaxNumericTail, byte-for-byte as PR #6553 left it Unchanged

So the cross-package duplication — the thing #6560 is about — still stood in full, and had meanwhile grown a third caller behind objectql's copy. Nobody had unified them: packages/spec carried no such export (confirmed against api-surface/data.json before the change). Proceeding was correct; the STOP condition ("someone already unified them") did not fire.

A repo-wide sweep for other copies of the rule (endsWith(suffix), scanMaxNumericTail) returned only these two sites plus unrelated string-suffix matching in registry.ts, type-compat.ts, analytics-service.ts, memory-analytics.ts.

Changes

File Change
packages/spec/src/data/autonumber-format.ts New export readAutonumberCounter(value, prefix, suffix): number | undefined, placed directly after renderAutonumber. TSDoc cites #6560's ruling and PR #6553, states it is the inverse companion of renderAutonumber and that both sides MUST call it rather than re-derive
packages/spec/src/data/autonumber-format.test.ts 15 unit cases for the new function (below)
packages/objectql/src/engine.ts Local readAutonumberCounterreadStoredAutonumberCounter, now a thin wrapper: anchored ⇒ delegate to spec, unanchored ⇒ this engine's own legacy reading. Both call sites (seeding scan, #6806 resync) follow the rename; TSDoc updated
packages/drivers/driver-sql/src/sql-driver.ts The inline anchored branch of scanMaxNumericTail (6 lines) → one call to spec's export; TSDoc updated
packages/objectql/src/engine-autonumber-resync.test.ts Header comment follows the rename
packages/spec/api-surface/data.json, packages/spec/export-origins/data.json Dual-snapshot regen — one added line each, zero removals
.changeset/autonumber-counter-readback-shared.md patch × @objectstack/spec, @objectstack/objectql, @objectstack/driver-sql

Call sites replaced

  1. packages/drivers/driver-sql/src/sql-driver.tsscanMaxNumericTail — the copy PR fix(objectql,driver-sql): 播种解析按声明的 suffix 定位计数器,两侧收敛 (#6468) #6553 wrote, replaced directly.
  2. packages/objectql/src/engine.tsreadStoredAutonumberCounter, behind which sit two engine consumers: seedAutonumber's scan and adoptExplicitAutonumber (Engine fallback autonumber: counter seeds once and never resyncs; collisions burn numbers with no recheck (residual split from #5495) #6806).

One deliberate design call: only the ANCHORED rule moved

The shared rule — the one both sides must apply identically — is the anchored one: the counter is the digit run at the start of what follows the rendered prefix, after stripping the rendered suffix when the value carries it (stripped when it matches, never required to match, since one counter spans the years a dynamic suffix renders). Out-of-scope values read as undefined, which also preserves the SQL driver's JS-side re-check of a LIKE that matched looser than startsWith under a case-insensitive collation.

The unanchored case (neither affix declared) deliberately stays per-side, because the two sides genuinely differ there and PR #6553 preserved both byte-for-byte on purpose:

  • engine: the last digit run of the value ('SO-2024-0007' → 7)
  • driver-sql: every digit concatenated ('SO-2024-0007' → 20240007)

Both readings are pinned by existing tests on their own sides. Spec therefore returns undefined for an unanchored slot rather than picking one of the two — hoisting either would make the shared contract claim an agreement that does not exist, which is a subtler instance of the very mirror-drift #6560 retires. Each side documents its own fallback at its own call site.

Zero-behaviour-change proof

Every call site keeps its existing guards and its existing result for every input. The packages/runtime cross-side parity suite (autonumber-seed-cross-side-parity.integration.test.ts) — the behavioural guard the ruling names — is unmodified in this PR and passes as-is. That is the evidence: the semantics moved packages without changing.

Reverse-verification framing, stated honestly up front: this is a refactor, not a fix, so there is no red-then-green to show. The prediction was that every pin stays green on both sides before and after, and that is what the runs report — no fabricated failing baseline.

Gates

All run in this worktree on the rebuilt dist (heavy runs serialised on flock /tmp/os-heavy-verify.lock; no lock contention observed — this session was the only holder).

Gate Result
@objectstack/runtime test — the parity suite, unchanged 118 files / 1837 tests passed
@objectstack/objectql test 167 files / 2906 tests passed
@objectstack/driver-sql test 81 passed + 4 skipped (85 files) / 1156 passed + 48 skipped
@objectstack/spec test 360 files / 9416 tests passed (incl. the 15 new cases)
check:generated (all 11 artifacts) ✓ all up to date
check:api-surface (re-run on rebuilt dist) ✓ surface + factory signatures unchanged
check:export-origins (self-test + check) ✓ current — 4971 exports / 16 entry points
check:exported-any ✓ 2402 types + 1506 schemas, none resolve to any
check:dual-source-exports ✓ 0 new dual-source (4801 names, baseline unchanged)
typecheck — spec (incl. test layer), objectql, driver-sql, runtime ✓ all clean

Build discipline (#7122 stale-dist trap): pnpm --filter @objectstack/spec build with real .d.ts ran before gen:api-surface / gen:export-origins, and both snapshots were then re-verified a second time against a full --filter @objectstack/runtime... rebuild. OS_SKIP_DTS was not used for any build whose output a gate reads.

Dual-snapshot rule (new public export ⇒ both): api-surface/data.json and export-origins/data.json, one added line each, zero removals, one shard each.

  • api-surface/data.json: + "readAutonumberCounter (function)"
  • export-origins/data.json: + "readAutonumberCounter": "src/data/autonumber-format.ts#readAutonumberCounter (function)"

New unit tests (spec, 15 cases)

Round-trip against renderAutonumber over 5 formats + a counter grown past the pad width; the #6468 defect inputs directly (003-2026 → 3, not 2026; 001-2026 → 1, not 12026); suffix stripped-when-matching (007-2025 → 7) including a digit-leading suffix; leading zeros; and the undefined cases the card called for — unanchored slot, foreign scope, value shorter than the affixes, non-numeric tail, value that is entirely the suffix, prefix-exactness, and a counter too long to parse finitely.

Special inspection items for the PM

  1. Premise drift — the engine copy had already been hoisted locally by Engine fallback autonumber: counter seeds once and never resyncs; collisions burn numbers with no recheck (residual split from #5495) #6806 and had gained a second consumer. Worth a look that promoting it to spec (rather than leaving objectql's local one) is still the shape wanted, given the card was written before Engine fallback autonumber: counter seeds once and never resyncs; collisions burn numbers with no recheck (residual split from #5495) #6806 landed. It is: the cross-package duplication is untouched by Engine fallback autonumber: counter seeds once and never resyncs; collisions burn numbers with no recheck (residual split from #5495) #6806.
  2. The anchored/unanchored split — the single judgement call in this PR, and the reason spec's export can return undefined for ('', ''). If the preference is instead that spec carry the engine's unanchored reading too, that is a one-line change here plus a driver-side behaviour change — which would no longer be a zero-behaviour-change refactor, hence not taken.
  3. Naming — spec's export keeps the card's proposed name readAutonumberCounter; objectql's now-narrower local wrapper is readStoredAutonumberCounter to avoid shadowing the import.
  4. No content/docs/releases/ and no docs/adr/** touched.

No auto-merge — the PM lands serially.


Generated by Claude Code

…back as spec's inverse of renderAutonumber (#6560)

`packages/spec` gains `readAutonumberCounter(value, prefix, suffix)`, the
declared inverse of `renderAutonumber`, and the two hand-written copies of
that readback — one in the ObjectQL engine, one in the SQL driver — are
replaced by calls to it. Pure refactor: zero behaviour change.

PR #6553 (#6468) taught both seeding paths to locate a counter by the
format's declared prefix/suffix pair, but landed that reading as two
independent copies of the same four lines. That is the exact shape of the
defect they were fixing: two hand-written readings of one composition rule
had already drifted into two different wrong answers over one dataset, so
the record-number band depended on which driver ran.

Only the ANCHORED rule — the one both sides must apply identically — moved.
The unanchored case stays per-side because the two sides deliberately differ
there (engine: last digit run; driver: every digit concatenated), and #6553
preserved both byte-for-byte; spec returns undefined rather than claim an
agreement that does not exist.

The packages/runtime cross-side parity suite is unmodified and passes as-is,
which is the evidence the semantics moved without changing.

Per the maintainer's ruling on #6560 (2026-08-08 ×2, re-confirmed
2026-08-10): non-authorable export, no Zod, no new vocabulary — api-surface
bookkeeping plus two call-site swaps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqCMGGYsvFxMJSU4PMSPo3
@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 3:50am

Request Review

@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.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 10, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 10, 2026 04:57
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit ddd075a Aug 10, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6560-read-autonumber-counter branch August 10, 2026 05:27
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…ary (objectstack-ai#7287) (objectstack-ai#7405)

Maintainer ruling on objectstack-ai#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
objectstack-ai#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: objectstack-ai#6555's Route-3 ruling (PR objectstack-ai#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 objectstack-ai#7287


Claude-Session: https://claude.ai/code/session_0184Hrx9PcaQ2KMMt88DRZ2c

Co-authored-by: Claude <noreply@anthropic.com>
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 size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants