You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
|[`checks: kind: guard`](/spec/entities#kind-guard-a-precondition-over-an-aggregate)| a precondition over an aggregate: block, mark for a task, or reject |
22
22
|[`immutableWhen` / `immutable`](/spec/entities#immutablewhen-immutable-user-write-immutability)| reject user writes in a status / append-only |
23
+
|[`lifecycle`](/spec/entities#lifecycle-the-legal-status-graph)| the whole legal status graph, enforced on every status write |
23
24
|[`locksWithMaster`](/spec/entities#lockswithmaster-a-child-collection-that-outlives-its-masters-lock)| a child collection that stays writable while its master is locked |
24
25
|[`hierarchy` / `leafOnly`](/spec/entities#hierarchy-leafonly-tree-entities)| tree entities, leaf-only references |
25
26
|[calculated fields](/spec/entities#calculated-fields)| server + UI-evaluated expressions, date helpers, call-outs |
@@ -174,4 +175,5 @@ The following are parsed (or reserved) but not yet materialised by a generator;
174
175
- **Cross-model status names and stage scopes** — a nomenclature owned by another model is seeded there, so its stages and names cannot be resolved from the referencing file; such references are rejected with the numeric-id fallback named.
175
176
- **`manyToMany`** — parsed but never materialised; the supported shape is the [explicit intermediate entity](/spec/relations#many-to-many).
176
177
- A declarative state machine, and shadow audit-history entities (audit *columns* via `audit: true` ship today).
178
+
- Event-driven document generation (produce a document on an event), and shadow audit-history entities (audit *columns* via `audit: true` ship today).
Copy file name to clipboardExpand all lines: docs/spec/data.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -60,7 +60,7 @@ The classification exists because a status **id is data, but its meaning is not*
60
60
61
61
### Status references — name, not number
62
62
63
-
Everywhere the file names a status — a [transition's](/spec/glue#transitions-guarded-status-flips) `from` and `setStatus`, a relation's `init`, a status-setting step's `value`, [`abortOn`](/spec/processes#aborton-cancel-the-instance-on-a-terminal-status)'s `status`, a [check's](/spec/entities#checks-declarative-validations) `status` / `setStatus`, [`immutableWhen`](/spec/entities#immutablewhen-immutable-user-write-immutability), a [posting's](/spec/glue#postings-source-document-to-ledger) event guard, a [report's](/spec/presentation#reports) `filter` — the seeded **name** may be written instead of the id:
63
+
Everywhere the file names a status — a [transition's](/spec/glue#transitions-guarded-status-flips) `from` and `setStatus`, a relation's `init`, a status-setting step's `value`, [`abortOn`](/spec/processes#aborton-cancel-the-instance-on-a-terminal-status)'s `status`, a [check's](/spec/entities#checks-declarative-validations) `status` / `setStatus`, [`immutableWhen`](/spec/entities#immutablewhen-immutable-user-write-immutability), a [`lifecycle`](/spec/entities#lifecycle-the-legal-status-graph) edge, a [posting's](/spec/glue#postings-source-document-to-ledger) event guard, a [report's](/spec/presentation#reports) `filter` — the seeded **name** may be written instead of the id:
Copy file name to clipboardExpand all lines: docs/spec/entities.md
+23Lines changed: 23 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -311,6 +311,29 @@ The lock also covers the entity's **composition children**. A child declares no
311
311
::: info Normative
312
312
A generator MUST refuse a user create, update or delete of a composition child whose master is currently immutable, unless that child declares `locksWithMaster: false`. The refusal MUST cover every user surface it generates, not only the affordances it renders — permitting the write through a different door undoes the lock as surely as removing it. It MUST NOT extend to system / workflow writes, which are what corrects an immutable record.
313
313
:::
314
+
## lifecycle — the legal status graph
315
+
316
+
Everything else about statuses is stated one edge at a time: `init:`says where a record starts, a [`transitions`](/spec/glue#transitions-guarded-status-flips) button guards the flips a user performs *through that button*, a workflow step sets a status, a [check](#checks-declarative-validations) files a rejected record in another. Nowhere does the file say which moves are legal *at all* — so any writer that is not a transition button (a workflow branch, a glue action, an API call) can move a document from any status to any other, and nothing notices.
317
+
318
+
`lifecycle:` states the whole graph, once:
319
+
320
+
```yaml
321
+
- name: SalesInvoice
322
+
lifecycle:
323
+
edges:
324
+
- { from: DRAFT, to: [ISSUED, CANCELLED] }
325
+
- { from: ISSUED, to: [PAID, VOIDED] }
326
+
```
327
+
328
+
- One entry per **source** status, listing every status reachable from it. Both sides accept a [seeded status name or its id](/spec/data#status-references-name-not-number).
329
+
- The graph is always over the entity's `function: EntityStatus` relation, so it names no column; the nomenclature must be seeded in the same file (a status entity owned by another model is seeded there, and so is its lifecycle).
330
+
- A status not listed as any `from` is **terminal**; a status listed nowhere is simply unreachable through this entity.
331
+
332
+
> **Normative.** A conforming generator MUST validate every status write against the graph — user, workflow, glue, transition button alike — and reject a move no edge declares, with a message naming both statuses. Enforcement therefore belongs to the layer every writer passes through (the generated persistence layer), never to the transition endpoints alone, which would leave every other writer unguarded. Where the status relation declares `init:`, a record MUST also be *created* in that status: entering the lifecycle anywhere else skips the graph rather than travelling it.
333
+
334
+
> **Normative.** With a lifecycle declared, `transitions` become **presentation over its edges**: each `from` status of a transition MUST reach its `setStatus` along a declared edge, and a status written by a workflow step or forced by a check's rejection MUST be one that some edge reaches. A conforming generator reports the disagreement when the file is read, not when the button is pressed — a reject path transiting through an approved status is exactly the mistake the graph exists to catch.
335
+
336
+
It composes with the [`stage:` classification](/spec/data#stage-what-a-status-means-to-the-lifecycle): a stage says what a status *means* (draft, live, cancelled, void) and scopes reports by it; the lifecycle says how a record may *move* between statuses.
314
337
315
338
## locksWithMaster — a child collection that outlives its master's lock
Copy file name to clipboardExpand all lines: docs/spec/glue.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -415,6 +415,8 @@ transitions:
415
415
body: "The invoice has been cancelled."
416
416
```
417
417
418
+
When the entity declares a [`lifecycle`](/spec/entities#lifecycle-the-legal-status-graph), a transition is presentation over its edges: its `from`/`setStatus` pair must be one, and the graph — not the button — is what every other writer is held to as well.
419
+
418
420
A transition may carry a [notify block](#the-notify-block-and-attach-print) — "on Void, tell the customer" — attempted after the flip has committed, and unable to fail it.
0 commit comments