Skip to content

docs: gapless numbering, as two variants rather than a nullable field - #68

Merged
btravers merged 2 commits into
mainfrom
docs/number-without-gaps
Aug 13, 2026
Merged

docs: gapless numbering, as two variants rather than a nullable field#68
btravers merged 2 commits into
mainfrom
docs/number-without-gaps

Conversation

@btravers

Copy link
Copy Markdown
Contributor

What

A how-to page and a runnable example for giving an entity a number that is legally required to be consecutive.

  • docs/how-to/number-without-gaps.md + a sidebar entry
  • examples/billing-persistence/src/numbering.ts and its spec
  • a README section on the example workspace

Why

The shape that suggests itself is one entity with number: number | null, stamped in later. It types every read site as nullable forever to describe a state that lasts milliseconds, and it hands you nothing to hold when you want "an invoice that definitely has a number".

Two variants of one root say the same thing without the null: DraftInvoice has no number field at all, IssuedInvoice has one that is generated and immutable, and the transition between them is a factoryAsync call. The page also answers the question a reader arrives with — why there is no "lazy" field flag — since spelling it as a flag would type the field as number | undefined at every read site anyway.

The guarantee itself is a property of a transaction, not of a field, and the page says so plainly: serial, identity and nextval all hand out numbers outside the transaction, so a rollback burns one permanently.

The case the spec pins

The issued variant carries a stricter invariant than the root (total > 0 against >= 0), which is what makes a stamp genuinely fallible. That gives the example the one case gaplessness exists for: an invariant firing after the number was allocated, the number handed back, and the next invoice taking it. Five tests, covering that plus the transition carrying the draft's data and root behaviour over, generated fields winning over a doctored projection, drafts having no number key at all, consecutive output, and an unreachable counter surfacing as a Defect rather than an InvalidEntity.

Notes

  • No changeset: packages/entity is untouched.
  • issue returns an AsyncResult rather than Promise<Result<…>>unthrown/prefer-async-result asked, and it made the defect path explicit: a rejected allocation never took a number, so there is nothing to hand back.
  • InMemorySeriesCounter.release no-ops unless the number is the newest one; handing back any other would open a hole mid-series instead of closing one at the end. It carries a ponytail: comment naming what it stands in for.

Gate is green locally: format --check, lint, typecheck, test, knip, build.

🤖 Generated with Claude Code

A number a regulator counts has to be consecutive, and the shape that
suggests itself — one entity with a nullable `number`, stamped in later —
types every read site as nullable forever to describe a state that lasts
milliseconds.

Model it as two variants of one root instead: a `DraftInvoice` with no
`number` field at all, an `IssuedInvoice` whose `number` is `generated`
and `immutable`, and a `factoryAsync` call between them. Adds the how-to
page and a runnable example in `billing-persistence`, whose stricter
issued-only invariant makes a stamp genuinely fallible — which is what
pins the case gaplessness exists for, an allocated number handed back
after construction fails.

The guarantee itself stays where it belongs: in the transaction. The page
says why a database sequence cannot provide it, and the in-memory counter
carries a `ponytail:` comment naming what it stands in for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 13, 2026 14:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds documentation and a runnable example showing how to model legally gapless numbering (e.g., invoice numbers) using two entity variants (draft vs issued) instead of a nullable field, and demonstrates allocating/releasing numbers in a transaction-like boundary.

Changes:

  • Introduces a new DraftInvoiceIssuedInvoice transition example with an in-memory series counter and a release-on-failure pattern.
  • Adds Vitest coverage for consecutive numbering, transition behavior, and defect vs invalid-entity outcomes.
  • Publishes a new how-to doc page and links it from the VitePress sidebar and the example workspace README.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
examples/billing-persistence/src/numbering.ts New example implementation of gapless numbering using draft/issued variants and an allocator port.
examples/billing-persistence/src/numbering.spec.ts Tests covering consecutive numbering, failed issuance returning numbers, and defect handling.
examples/billing-persistence/README.md Documents the new numbering example and how it relates to persistence/rehydration.
docs/how-to/number-without-gaps.md New how-to explaining transaction-based allocation and modeling numbered state as a variant.
docs/.vitepress/config.ts Adds the new how-to page to the guide sidebar.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread examples/billing-persistence/README.md Outdated
Comment thread docs/how-to/number-without-gaps.md Outdated
Two review comments, both accurate. The README described `issue` as
returning a `Result` when it returns an `AsyncResult`, and the how-to's
allocation snippet branded a driver's `unknown` with an assertion rather
than a parse — which is exactly what the branded-fields guidance tells
readers not to do, in the one snippet showing a value crossing a driver
boundary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@btravers
btravers merged commit 2519f78 into main Aug 13, 2026
13 checks passed
@btravers
btravers deleted the docs/number-without-gaps branch August 13, 2026 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants