Specification · Section 8 of 13
The data model: declared storage, checked flow, owned values
How a contract declares what it can store, validates everything that enters, and names an ownership verb for every flow, built from a closed set of domain types and four pool flavors.
8.1 Declared storage, declared shape
Everything the contract can remember is declared up front, with a shape: pools (quantity, capacity, stages), registers (typed records - deliveries shape { item: catalog-ref, qty: decimal(2), period: month }), and genesis bindings (the parameters of §6.3). If a field is not in the manifest, the contract cannot store it - the mirror of the port rule (absent port = prohibited flow; absent field = prohibited memory). Two provenances, both visible per field: bound at genesis (immutable; changing one is an amendment) versus accumulated by events (changed only by declared effects, so every current value traces to the events that built it). This closes the data surface the same way ports close the capability surface - and it is quietly load-bearing for sekretess (§12): a disclosure policy enumerates over a CLOSED field list, so “what could this contract possibly reveal” is answerable by reading the state block.
8.2 Checked flow - declared checks, implied floors
Nothing enters unchecked; the only question is whose check. Every in-port payload is validated against its implied floor: the payload type’s own standard (an invoice.submit document that fails Peppol BIS Billing 3 validation never reaches a rule - the §1 gates already do this). Declared checks stack contract-specific guards on top: bounds, enums, cross-field conditions, “lines must reference the genesis basket”. The rule lands as: declare your argument checks; where you don’t, the standard’s own schema applies - silence means the floor, never means unchecked. (Design-by-contract preconditions, Meyer/Eiffel; XSD/Schematron in the Peppol stack; validate-at-the-boundary as universal modern practice.)
8.3 Ownership is declarative: move, copy, borrow
The deep one. When data crosses a boundary - into the contract at genesis, between parties, out through a port - the flow declares which of three verbs it is:
move- ownership transfers; the source no longer has it. Money, goods title, a receivable (factoring, §7.1), a coupon redemption. Move-values are linear: they cannot be copied and cannot be spent twice - and §3.1 already bought this mathematics, because Petri-net tokens ARE move semantics (double-spend = the boundedness question, decided). Conditional moves express retention of title:moves title supplier -> buyer when payment.confirmis äganderättsförbehåll, executable.copy- information shared; both sides hold it. Reports, notices, statistics. A copy cannot be unshared, so the control is the RECEIPT: every copy is logged - who received which fields, when, under which disclosure scope.borrow- read access to something still owned by the grantor: scoped, revocable, by reference. §5’s artifact bindings are borrows (pin/track a table someone else owns and governs); disclosure scopes (§12) are borrows of fields.
state:
pool money.escrow capacity 120_000 SEK stages available|reserved|disbursed
bind basket shape order-lines at genesis # init data: moved in
register deliveries shape { item: catalog-ref, qty: decimal(2), period: month }
flows:
invoice.submit in checks bis-billing-3 (implied) + lines within basket (declared)
payment.confirm in moves money buyer -> supplier # linear: spend once
goods.deliver out moves title supplier -> buyer when paid # retention of title
report.share out copies delivery-stats -> buyer # receipted, irreversible
rate-table ref borrows art:kb-it-rates-2027 pin v2 # §5: owned elsewhere
Prior art, and it is rich:
- Linear logic / linear types (Girard 1987; Wadler’s “Linear types can change the world!”) - a linear value must be used exactly once: the formal reason money cannot double-spend by construction rather than by detection.
- Rust’s ownership model - move by default, explicit
Copy, borrows checked statically: industrial proof that ownership discipline is practical and lintable - which is all our linter needs to promise “no value owned ambiguously, no resource spent twice” the same way it promises pool safety. - Incoterms (ICC, 1936-2020) - THE declarative transfer standard: EXW, FOB, CIF, DDP say in three letters exactly where risk and responsibility pass. A century of global trade agreeing that ownership transfer should be a declared term - it just never had a runtime.
- Äganderättsförbehåll / retention of title - the conditional move, standard drafting in every goods contract.
- GDPR Art. 28 / personuppgiftsbiträdesavtal (DPAs) - data-flow declarations as binding legal documents: roles, purposes, sub-processor chains. Prose declarations of copy/borrow semantics that nobody executes; here, an undeclared flow is not a compliance finding, it is a rejected event.
The composite payoff: storage is closed (8.1), flow is checked (8.2), and every flow names its ownership verb (8.3) - so the contract’s complete data topology (what it holds, what enters, what leaves, who owns what afterwards) is readable off the manifest, diffable between versions, and lintable before signing. Nothing moves silently; nothing is owned ambiguously; GDPR and OSL questions become queries over declarations instead of investigations.
8.4 The type vocabulary: domain types, never naked primitives
(A number + a currency is called money - Fowler’s Money pattern, Analysis Patterns 1997, and the oldest “never use a float for it” lesson in software.) The shapes of §8.1-8.3 are built from a closed set of domain types - no naked ints, no strings-that-mean-something, no floats anywhere. And the core list does not need inventing: EN 16931 already defines semantic data types for exactly this domain (Amount, Unit Price Amount, Quantity, Percentage, Identifier, Code, Date, Text) - we adopt them and add the contract-side types the invoice never needed:
| Type | What it is | Bound to |
|---|---|---|
money |
decimal + currency + declared rounding | ISO 4217; EN 16931 Amount. No cross-currency arithmetic without a declared conversion (a fixing) |
quantity<unit> |
decimal + unit of measure - hours, pieces, kg, tonnes, m², m³, kWh | UN/ECE Rec 20 unit codes (the ones already in BT-130) - area, weight, volume, energy all covered; no unit, no number |
rate / percentage |
bounded decimal; rate × money = money |
dimensional discipline: money×money is a type error |
id<kind> |
opaque identifier, kinded | equality only, no arithmetic; id<receipt> ≠ id<contract> - cross-kind comparison is a lint error |
party |
organization identity + scheme | ISO 6523 ICDs / Peppol participant schemes, orgnr, LEI |
date / period |
calendar values, event-time only | calendar artifacts (§5); no clock reads, ever |
opaque |
content the contract carries but cannot inspect | hash-equality only. The type of SEALED data: a sekretess-scoped field, an attachment, a black-boxed term (§12) - storable, referencable, never branchable-on |
lookupOf<K,V> |
keyed table by reference | §5 artifacts: a rate card is lookupOf<role, money/hour>, a tax table lookupOf<category, rate> |
tableOf<shape> |
append-only typed rows | registers (§8.1), schedules (§3.4) |
linear (modifier) |
move-only value, spend-once | §8.3’s move semantics as a type property: pools of linear money cannot be copied by any expression |
Two disciplines make the starvation stick. The set is closed: new types arrive like new ports do (§9) - vetted primitives when the corpus demands them, never user-defined type constructors; lookupOf<>/tableOf<> parameterize over declared shapes only. Types carry the physics: units and currencies propagate through arithmetic (F# units-of-measure is the production prior art), so “added hours to kronor” or “compared a receipt id to a contract id” is caught by the linter at authoring time, in the same pass that checks pool safety. Primitive obsession - the antipattern where everything is an int and meaning lives in variable names - is where contract bugs breed; a signed document gets no such luxury.
8.5 Resources for every industry: fungible, named, human, unbounded
Everything allocatable is a pool, and pools come in exactly four flavors - which together cover every industry the corpus has thrown at the design:
| Flavor | Declaration | What it models |
|---|---|---|
| Fungible, bounded | pool hours.senior capacity 3000 h |
the default: money, hours, m³ concrete, tonnes grain, kWh, pallet slots, crane-days, license seats |
| Named (non-fungible) | pool unit:villa-saltsjo capacity 1 |
identity-bearing resources: a specific house, machine, vehicle, venue, artwork - capacity-1 pools of an id<kind>; double-booking = the §3.1 boundedness question, already decided (coupon codes in §6.3 were this flavor all along) |
| Human | named person OR fungible role, + calendar + competence guards | staffing/vård/construction: a role pool (3000 senior hours) or a named consultant (capacity-1 with a working-hours window from the calendar artifact); competence/certification requirements are declared checks (§8.2) against a party-maintained register (§5.6) - never free text |
| Unbounded | pool support.tickets capacity unlimited |
“löpande avrop under avtalsperioden”, all-you-can-eat SLAs, unlimited seats: draws are still counted and receipted (the picture and the cadence guards keep working - “unlimited use” ≠ “unmetered use”) but exhaustion is unrepresentable; the linter simply skips pool-safety on them |
The load-bearing claim: no industry vocabulary lives in the core. quantity<unit> (§8.4) carries any UN/ECE Rec 20 unit - weight, area, volume, energy, time, pieces; the pool flavors carry any allocation shape; calendar artifacts carry any industry’s rhythm (banking days, growing seasons, shift schedules); and whatever is genuinely industry-logic (tax, tariffs, freight rating, payroll math) enters as a declared import (§7.6), never as core semantics. A construction ramavtal, a staffing agreement, a venue rental, an energy PPA, and a SaaS subscription differ only in their declarations - the machine underneath is one machine. That is §6’s universality made concrete at the data layer: profiles bind the words, pools and units bind the stuff, imports bind the math.