diff --git a/README.md b/README.md index da4cc5b..10fed8f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ manifesto — is published at **[intentfile.org](https://intentfile.org)** (sour | --- | --- | | [1.0](versions/1.0.md) | superseded | | [1.1](versions/1.1.md) | superseded - added `aggregates`, `checks: kind: guard`, `posts`, the notify block (`attach: print`, `forEach`), `pattern` and header-mediated `dependsOn` ([proposal](proposals/0001-keyed-aggregates-guard-checks-and-row-posting.md)) | -| [1.2](versions/1.2.md) | current - adds lifecycle stages + report `scope` + status names, `locksWithMaster`, `generates` prompted input and computed lines, `defaultValue` row seeding, print-template row filtering ([proposal](proposals/0002-print-template-row-filtering.md)), the scope boundary and the authoring assistant's honesty rule ([proposal](proposals/0003-the-scope-boundary.md)), and aligns document numbering with deployed practice | +| [1.2](versions/1.2.md) | current - adds lifecycle stages + report `scope` + status names, `locksWithMaster`, `generates` prompted input, computed lines and event-driven creation, `defaultValue` row seeding, print-template row filtering ([proposal](proposals/0002-print-template-row-filtering.md)), the scope boundary and the authoring assistant's honesty rule ([proposal](proposals/0003-the-scope-boundary.md)), and aligns document numbering with deployed practice | ## Structure diff --git a/versions/1.2.md b/versions/1.2.md index f321557..90b5088 100644 --- a/versions/1.2.md +++ b/versions/1.2.md @@ -7,6 +7,7 @@ This version adds to [1.1](1.1.md): - **Lifecycle-aware aggregation** — [`stage`](#stage--what-a-status-means-to-the-lifecycle) classifies what a status seed *means* (draft / live / cancelled / void), a report's [`scope`](#lifecycle-scope) states which lifecycle rows it counts (an aggregating report over a stage-classified nomenclature defaults to the live rows), and [every site that names a status accepts the seeded name](#status-references--name-not-number) instead of its positional id. - **[`locksWithMaster`](#lockswithmaster--a-child-collection-that-outlives-its-masters-lock)** — a composition child collection that stays writable while its master is locked (settlement against an issued invoice). - **[`generates` prompted input](#prompted-input--prompt)** — a create-from action may collect the values the source cannot derive before the create; and [`generates.items`](#generates--create-from) gains a **computed list form** building synthetic lines from expressions over the source. +- **[Event-driven creation](#event-driven-creation--event)** — a create-from may declare an `event:` and mint the whole document (header and items) when the source reaches a state, at most once, instead of waiting for someone to press a button. - **[`defaultValue` semantics completed](#defaultvalue--field-defaults)** — a default is the column default, the reason a `required` field is not demanded from the caller, and the value a new row is seeded with in the UI. - **[Print-template row filtering](#row-filtering--filter--match)** — `filter` / `match` render one items collection into several purpose-grouped tables (earnings beside deductions, debit beside credit). - **[The scope boundary](#the-scope-boundary)** — what a conforming file deliberately never models (protocol, algorithm, statutory form), each category's designated hand-off, and the honesty a conforming authoring assistant owes when a requirement falls outside the format. @@ -1361,6 +1362,34 @@ generates: - Constraints: the target MUST be a local entity (no `uses:`) declaring a composition to-one relation to `forEntity`; the action's scope MUST be `entity`; a `timestamp` field MUST NOT be prompted. - The create goes through the target's ordinary write layer, so its `-created` event, roll-ups and validations fire unchanged. +#### Event-driven creation — `event:` + +A create-from MAY declare an `event:` instead of relying on the button — the follow-up document is minted the moment the source reaches a state, with nobody clicking. The canonical case is a document that arrives from the outside and is completed by an earlier step: a fine ingested by a webhook, whose responsible person is identified by a transition, must produce a declaration document from the fine and that person. + +```yaml +generates: + - name: declaration-from-fine + from: Fine + to: Declaration + event: { onTransition: Fine, when: "Status == IDENTIFIED" } # or { onCreate: Fine } + map: + Fine: id # REQUIRED with an event — the back-reference, i.e. the guard + Vehicle: Vehicle + defaults: { declaredAt: now } + items: # a whole document — header AND items + - { name: "Fine {number}", amount: Amount } +``` + +**Normative.** Exactly one trigger MUST be declared: `onTransition` (a status write — a `when: " == "` guard is mandatory, the status named or numbered) or `onCreate` (the source's insert — the guard is optional, for a source with no status lifecycle). The entity named there MUST be the entity `from:` declares; the owning model is never repeated (`fromUses:` declares it). The guard MUST be evaluated against the source as re-read at delivery, not against the event payload, which is as-of the event and lacks anything a later step wrote. + +**Normative.** An event-driven create-from is **at-most-once**: `map` MUST copy the source's primary key onto a to-one relation of the target back to the source, and the generated creation MUST return the already existing target instead of creating a second one. A file declaring an `event` without that back-reference MUST be rejected — a redelivery would otherwise mint a duplicate document. A create-from with no `event` carries no such guard: producing several targets from one source by clicking twice is a legitimate manual act. + +**Normative.** Declaring an `event` drops the button unless `button: true` is declared as well; `button: false` without an `event` MUST be rejected (the action would have no trigger at all). When both triggers are declared they MUST share one creation path, and therefore one at-most-once guard. + +`sourceStatus:` composes unchanged: the flip happens once the target exists, and cannot re-trigger the create-from because the guard has already claimed the source. + +Prefer this over [`posts`](#posts--derived-rows-on-an-event) when the result is a document with line items — `posts` emits flat mapped rows and cannot reference the freshly created header. Prefer it over a button plus a [`wait`](#wait--park-the-process-on-a-data-event) step when the step is really waiting for a person to remember to click: an unclicked record parks its process instance indefinitely. + ### transitions — guarded status flips A per-record button that flips an entity's `function: EntityStatus` relation on demand — void, cancel, close, reopen — guarded by allowed source statuses and an optional condition. A flip from any other status (or a failing guard) is rejected; a successful flip publishes a `-transitioned` event that `postings` and integrations can observe. @@ -1652,6 +1681,7 @@ One line per construct, linking into the chapters above. | [`expansions`](#expansions--child-rows-from-a-date-span) | generated child rows per day / week / month | | [`generates`](#generates--create-from) | one-click document-from-document cloning, mirrored or computed lines | | [`generates.prompt`](#prompted-input--prompt) | collect the input the source cannot derive before the create | +| [`generates.event`](#event-driven-creation--event) | mint the document on a source event instead of a click, at most once | | [`transitions`](#transitions--guarded-status-flips) | guarded on-demand status flips (void / cancel / reopen) | | [`postings`](#postings--source-document-to-ledger) | declarative source-document to balanced-document posting | | [`aggregates`](#aggregates--keyed-cross-entity-totals) | keyed cross-entity totals materialised into their own entity | @@ -1671,7 +1701,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). +- A declarative state machine, 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