Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions versions/1.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,30 @@ A guard compares a keyed [aggregate](#aggregates--keyed-cross-entity-totals) aga

`immutableWhen` requires a `function: EntityStatus` relation; `immutable: true` needs none and is mutually exclusive with it. System / workflow writes stay possible — corrections to an immutable record are flow-generated reversals, never edits.

### lifecycle — the legal status graph

Everything else about statuses is stated one edge at a time: `init:` names where a record starts, a [`transitions`](#transitions--guarded-status-flips) button guards the flips a user performs through it, a workflow step sets one, 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, an API call, a custom action) can move a document from any status to any other, and nothing notices.

`lifecycle:` states the whole graph, once:

```yaml
- name: SalesInvoice
lifecycle:
edges:
- { from: DRAFT, to: [ISSUED, CANCELLED] }
- { from: ISSUED, to: [PAID, VOIDED] }
```

- One entry per **source** status, listing every status reachable from it. Both sides accept a [seeded status name or its id](#status-references--name-not-number).
- 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).
- A status not listed as any `from` is **terminal**; a status listed nowhere is simply unreachable through this entity.

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

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

It composes with the [`stage:` classification](#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.

### locksWithMaster — a child collection that outlives its master's lock

An entity's immutability covers **that entity**. A composition child is a different entity, so a
Expand Down Expand Up @@ -1380,6 +1404,8 @@ transitions:
body: "The invoice has been cancelled."
```

When the entity declares a [`lifecycle`](#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.

A transition MAY carry a [notify block](#the-notify-block--and-attach-print-sending-the-document-itself) — "on Void, tell the customer" — which is attempted after the flip has committed and can never fail it.

### postings — source document to ledger
Expand Down Expand Up @@ -1560,7 +1586,7 @@ The classification exists because a status **id is data, but its meaning is not*

#### Status references — name, not number

Everywhere the file names a status — a [transition's](#transitions--guarded-status-flips) `from` and `setStatus`, a relation's `init`, a status-setting step's `value`, [`abortOn`](#aborton--cancel-the-instance-on-a-terminal-status)'s `status`, a [check's](#checks--declarative-validations) `status` / `setStatus`, [`immutableWhen`](#immutablewhen--immutable--user-write-immutability), a [posting's](#postings--source-document-to-ledger) event guard, a [report's](#reports) `filter` — the seeded **name** may be written instead of the id:
Everywhere the file names a status — a [transition's](#transitions--guarded-status-flips) `from` and `setStatus`, a relation's `init`, a status-setting step's `value`, [`abortOn`](#aborton--cancel-the-instance-on-a-terminal-status)'s `status`, a [check's](#checks--declarative-validations) `status` / `setStatus`, [`immutableWhen`](#immutablewhen--immutable--user-write-immutability), a [`lifecycle`](#lifecycle--the-legal-status-graph) edge, a [posting's](#postings--source-document-to-ledger) event guard, a [report's](#reports) `filter` — the seeded **name** may be written instead of the id:

```yaml
transitions:
Expand Down Expand Up @@ -1625,6 +1651,7 @@ One line per construct, linking into the chapters above.
| [`format`](#fields) | a named input-format preset (`email`) over `pattern` |
| [`checks`](#checks--declarative-validations) | cross-field / cross-line validations |
| [`immutableWhen` / `immutable`](#immutablewhen--immutable--user-write-immutability) | reject user writes in a status / append-only |
| [`lifecycle`](#lifecycle--the-legal-status-graph) | the whole legal status graph, enforced on every status write |
| [`locksWithMaster`](#lockswithmaster--a-child-collection-that-outlives-its-masters-lock) | a child collection that stays writable while its master is locked |
| [`hierarchy` / `leafOnly`](#hierarchy--leafonly--tree-entities) | tree entities, leaf-only references |
| [calculated fields](#calculated-fields) | server + UI-evaluated expressions, date helpers, call-outs |
Expand Down Expand Up @@ -1671,7 +1698,7 @@ The following are parsed (or reserved) but not yet materialised by a generator;
- Reserved `function` values for upcoming presentations (`Board`, `Gantt`, `Timeline`).
- **`manyToMany`** — parsed but never materialised; the supported shape is the [explicit intermediate entity](#many-to-many).
- **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.
- Event-driven **document** generation (produce a whole document, rather than the mapped rows of [`posts`](#posts--derived-rows-on-an-event), on an event), a declarative state machine, and shadow audit-history entities (audit *columns* via `audit: true` ship today).
- Event-driven **document** generation (produce a whole document, rather than the mapped rows of [`posts`](#posts--derived-rows-on-an-event), on an event), and shadow audit-history entities (audit *columns* via `audit: true` ship today).
- Arbitrary resolver-path task assignment beyond `assignee: personal`.
- **A fan-out that attaches the RECORD's document rather than the row's.** `forEach` + `attach: print`
attaches each row's own document, which is what a per-row document (a payslip) needs. The mirror
Expand Down