Skip to content

feat(numbering): a number series is a tenant-level business object - the .numbers artefact declares it - #6476

Merged
delchev merged 4 commits into
masterfrom
feat/numbering-series-artefact
Jul 31, 2026
Merged

feat(numbering): a number series is a tenant-level business object - the .numbers artefact declares it#6476
delchev merged 4 commits into
masterfrom
feat/numbering-series-artefact

Conversation

@delchev

@delchev delchev commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What

First-class document numbering, reworked so the number's shape leaves the model. An intent field declares only number: { series, per?, stampOn } - a reference to a series by name. The series itself is a tenant-level business object: a number renders as a literal prefix + the sequence zero-padded to a total width (no token grammar), and its shape and counter live per tenant, not in any module.

The .numbers artefact (new)

A module DECLARES A REQUIREMENT - "I need series X; if this tenant has none, provision it with this prefix/width" - exactly as .roles declares roles:

{"series": [{"name": "Sales Invoice", "prefix": "SI", "size": 10}]}

NumberSeriesSynchronizer (engine-numbering, multitenant, SynchronizersOrder.NUMBER_SERIES = 45 - before anything that could allocate):

  • absent series -> provisioned per tenant with the declared defaults;
  • present -> left completely alone (the counter is live, the shape may be tenant-configured);
  • declared differently by another module -> that artefact fails loudly naming both locations (an identical re-declaration is a legal shared range and provisions once);
  • DELETE / cleanup remove only the declaration record - never a series row; allocated ranges are business history.

A FAILED declaration is re-evaluated on UPDATE (the other module may have re-aligned) and stays depleted, so the processor never buries the conflict message under "undepleted artefact" noise.

One table, one writer per column

DIRIGIBLE_DOCUMENT_NUMBERS, one row per (series, partition), holds shape AND counter. The synchronizer only INSERTs missing rows; the Settings endpoint writes PREFIX/SIZE and the counter reset; the allocator increments COUNTER. One deliberate exception: the first allocation for a new partition (per: Company - partition values are data, no artefact can pre-provision them) materializes that partition's row from the series' base row, inheriting the tenant's configured shape. Allocating an undeclared series fails loudly - a shape is never invented.

Sequences are continuous and never auto-reset - a jurisdiction that restarts numbering each January does it by setting prefix + next in the settings (BG law requires continuous, so that is the default posture).

Removed keys fail loudly

An intent still carrying number.format / scope / resetOn used to be silently dropped by the typed Gson mapping. IntentParser.rejectRemovedNumberKeys now rejects them on the raw YAML tree with messages pointing at .numbers / per: / the settings.

Settings UI

The application shell's Document Numbering page now edits each series' prefix, total width and next value, with a live example of the next number. Writes are change-tracked (PUT /services/core/numbering/shape for shape, PUT for next) - an unchanged "next" is never written, so a counter that advanced since page load is never rewound.

Tests

  • engine-numbering 17/17 unit tests (synchronizer parse/validation, provision, cross-module conflict, FAILED re-evaluation, delete-never-touches-counters; rendering).
  • engine-intent 285/285 (incl. the new removed-keys rejections; one stale format: fixture the new check caught was migrated).
  • NumberingSdkIT 5/5 green locally (H2): gap-free allocation across re-syncs, undeclared-series 500, differing re-declaration FAILED naming both modules with the original shape surviving, per-partition independence, and a freshly provisioned tenant drawing T-0001 from its own sequence.
  • Javadoc release profile clean on the touched modules; formatter:validate green.

Docs: intent-assistant-guide.md, engine-intent/CLAUDE.md, root CLAUDE.md (this PR); dirigible-io/dirigible-io.github.io#157 (merged); IntentFile/intentfile.github.io#2 (open).

🤖 Generated with Claude Code

delchev and others added 2 commits July 31, 2026 13:16
DSL: NumberIntent keeps series + optional per (a to-one that partitions the series) + stampOn.
format, scope and resetOn are gone - a number's shape is a deployment concern, not a model one.

Runtime reshaped to prefix + sequence padded to size, no token grammar; a series must be declared
before it can be allocated from (allocating an unknown series fails loudly rather than inventing a
shape nobody chose). Store rows are one per (series, partition), each carrying the shape AND the
counter, with every writer touching only its own columns.

Not yet functional: the .numbers artefact + synchronizer that provisions series per tenant is the
next commit. Until it lands, nothing can provision a series, so allocation fails by design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ings edit shape + next

- NumberSeriesDeclaration artefact + repository + service + multitenant
  NumberSeriesSynchronizer (SynchronizersOrder.NUMBER_SERIES = 45, before
  anything allocating): absent series -> provision per tenant; identical
  cross-module re-declaration -> skip; differing -> fail that artefact
  loudly naming both modules; DELETE/cleanup never touch a series row.
  A FAILED declaration is re-evaluated on UPDATE and stays depleted, so
  the processor never overwrites the conflict message.
- .numbers content type (application/json+numbers) in ContentTypeHelper.
- Partition rows materialize on first allocation from the series' base
  row (partition values are data - no artefact can pre-provision them);
  undeclared series still fail loudly.
- IntentParser rejects the removed number keys format/scope/resetOn on
  the raw YAML tree - the typed Gson mapping silently dropped them.
- Application shell Document Numbering settings: per-series prefix +
  total width + next value with a live example, change-tracked writes
  (PUT /shape for shape, PUT for next - unchanged values never written).
- NumberingSdkIT rewritten: declared-series allocation, undeclared 500,
  differing re-declaration FAILED with both locations, per-partition
  independence, and a new-tenant own-sequence case.
- Docs: intent-assistant-guide.md number: section, engine-intent
  CLAUDE.md numbering bullet, root CLAUDE.md summary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/** The current tenant's series. */
@GetMapping
public ResponseEntity<List<DocumentNumberStore.Counter>> list() {
public ResponseEntity<List<SeriesView>> list() {

/** The numeric sequence value of a rendered {@code T-NNNN} number. */
private static int value(String number) {
return Integer.parseInt(number.substring(2));
delchev and others added 2 commits July 31, 2026 15:05
…synchronizer switch

CodeQL on #6476: list() no longer bootstraps the counter table (reads
return an empty list before any writer created it, and read pre-upgrade
shape columns as defaults); the artefact-phase switch names PREPARE
explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…generate, publish and REST

IntentEmissionCoverageIT gains the numbering keyword at both layers: the
generated repository allocates from the DECLARED series by name
(DocumentNumbers.next), and the published app stamps prefix + zero-padded,
gap-free numbers at insert - provisioned from an AUTHORED .numbers file
published with the project (which the intent scrub correctly leaves alone).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +1319 to +1320
assertEquals(Integer.parseInt(firstNumber.get()
.substring(3))
Comment on lines +1322 to +1323
Integer.parseInt(secondNumber.get()
.substring(3)),
@delchev

delchev commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

On the CodeQL check: the one high-severity finding (java/csrf-unprotected-request-type on DocumentNumberEndpoint's GET) is a pre-existing, repo-wide pattern finding, not a regression of this PR - the identical alert is already open against master for this same file (#10969) and for TenantConfigurationsEndpoint (#10921, #10922, merged with #6205).

Commit 216b26f still tightened the actual substance behind it: the GET path no longer performs ANY database write (the create-if-absent table bootstrap moved off the read path entirely - reads return an empty list before any writer created the table). The query keeps flagging the endpoint pattern regardless, same as on master, so it is a candidate for dismissal alongside the master alerts. The remaining three alerts are severity-note NumberFormatException hints inside integration-test helpers, where a parse failure failing the test is the intended behavior.

🤖 Generated with Claude Code

@delchev
delchev merged commit 03ed887 into master Jul 31, 2026
9 of 10 checks passed
@delchev
delchev deleted the feat/numbering-series-artefact branch July 31, 2026 13:24
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