diff --git a/README.md b/README.md index da4cc5b..4f03bac 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 and computed lines, `resolves` (effective-dated register lookup), `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..4a1e039 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. +- **[`resolves`](#resolves--fill-a-relation-from-a-register-valid-on-a-date)** — fill a to-one from the register row valid on a date the record carries, with `found` / `notFound` / `ambiguous` as three first-class, observable outcomes. - **[`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. @@ -1458,6 +1459,36 @@ posts: `posts` differs from [`generates`](#generates--create-from) in intent: `generates` creates ONE document from a user action, while `posts` emits N mapped rows automatically and idempotently on an event. +### resolves — fill a relation from a register valid on a date + +Set a to-one from the row of a **register** whose validity period covers a date the record carries. The register says "X applied to Y from A to B" — a vehicle assignment, a price list, a contract in force, an org assignment — and the record carries the match key(s) and the date: + +```yaml +resolves: + - name: identifyDriver + event: { onCreate: Fine } # onCreate or onUpdate, optional `when` guard + set: driver # the to-one of Fine this fills + from: VehicleAssignment # the register + match: { vehicle: vehicle } # register property <- record property (one or more) + between: { start: validFrom, end: validTo, value: violationAt } + outcome: resolution # optional string field: found / notFound / ambiguous + found: { setStatus: IDENTIFIED } + notFound: { setStatus: UNRESOLVED } + ambiguous: { setStatus: UNRESOLVED } +``` + +Nothing else in the format reaches this shape: [`dependsOn`](#relations) is an authoring-time copy matched by equality, a [`decision`](#processes) condition is a single comparison, and a `setField` step writes a constant. Without it every application hand-writes the same query-and-classify code. + +**Normative.** A lookup MUST declare exactly one of `onCreate` / `onUpdate` naming a declared entity; `onDelete` MUST be rejected, since there is no record left to fill. `set` MUST name a to-one relation of that entity, `from` a declared register entity, and `match` at least one pair whose left side is a property of the register and whose right side a property of the record. `between.value` MUST name a date field of the record; `between.start` and `between.end` name date fields of the register and MAY each be omitted, in which case that side of the period is open. The end of a period is **inclusive**, and a bound expressed as a date (rather than an instant) covers its whole day. + +**Normative.** The register MUST carry exactly one to-one relation to the entity `set` points at; that relation is the value the lookup copies. Zero or more than one MUST be rejected — a register offering a choice of columns to copy is a modelling ambiguity, and guessing one would defeat the construct's purpose. + +**Normative.** All three outcomes are first-class and MUST be distinguished. Exactly one covering row fills the relation. No covering row (`notFound`) and more than one covering row (`ambiguous`) MUST both leave the relation unset: a conforming generator MUST NOT choose between candidate rows. Each outcome MAY carry a `setStatus` routing the record, which requires the record to declare a `function: EntityStatus` relation and accepts a [status name](#status-references--name-not-number) as well as an id. + +**Normative.** The attempt MUST be observable. When `outcome` names a `string` field of the record, that field MUST be stamped with `found`, `notFound` or `ambiguous`, so unresolved records form a filterable worklist a person can finish and a process [`decision`](#processes) can branch on the result. A conforming generator SHOULD additionally log the keys and the date it checked. + +**Normative.** A record that already carries the relation MUST be skipped, so a manual correction is never overwritten and a re-delivered event is a no-op. The resolved relation, the outcome and the status MUST be written as one targeted update of those columns only, leaving every other column of the record — and any concurrent write to it — untouched. + ### Guardrails - **Curated vocabulary, not a general DSL.** Real logic is a `script` step or a hand-written hook — the escape hatch is non-negotiable. @@ -1656,6 +1687,7 @@ One line per construct, linking into the chapters above. | [`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 | | [`checks: kind: guard`](#kind-guard--a-precondition-over-an-aggregate) | a precondition over an aggregate: block, mark for a task, or reject | +| [`resolves`](#resolves--fill-a-relation-from-a-register-valid-on-a-date) | fill a to-one from the register row valid on the record's date | | [`posts`](#posts--derived-rows-on-an-event) | derived ledger rows emitted idempotently on an event | | [`personal` / `partner`](#personal-and-partner-surfaces) | per-user and per-partner row-scoped surfaces | | [`seeds`](#seeds) | initial data, CSV-backed sets, translations |