Specification · Section 2 of 13
The language, by worked example
Walks the pseudo-language through a full IT-consulting ramavtal, then covers what a receipt looks like, the envelope/body grammar, and the three binding tiers of contract language.
Illustrative strawman (syntax will be forged against the corpus - this extends that example with ports, receipts, idempotency). An IT-consultant ramavtal:
mechanical-contract v0
id: mk-se-2027-000042
title: Ramavtal IT-konsulttjänster
prose: ramavtal-2027-0042.pdf sha256:9f2c44... # Ricardian binding:
annex: [ "§14 Force majeure", "§16 Sekretess" ] # prose-only clauses, hashed,
# visible but not executable
parties:
buyer:
org: 212000-1355 "Lunds kommun"
sign: eIDAS-qualified-seal | freja-orgid
supplier:
org: 556677-8899 "Konsultbolaget AB"
sign: eIDAS-qualified-seal
validity:
from 2027-01-01 to 2029-12-31
extend: 2 x 12mo, notice 6mo, mode: active-renewal # negative option: impossible
money:
currency SEK
vat 25% on services
rounding per-line half-up 0.01
# ── DECLARED STATE ─────────────────────────────────────────────
state:
pool hours.senior capacity 3000 h
pool hours.junior capacity 2000 h
pool budget.total capacity 8_000_000 SEK # takvolym: hard ceiling
register deliveries # accepted, not yet invoiced
register invoices # accepted, not yet paid
# ── EXTERNAL FACTS (signed fixings, never lookups) ─────────────
fixings:
index aki series SCB.AKI.J62 base 2026-09 signed-by scb-ingest revise yearly 01-01
# ── ARTIFACTS: tables & variables by reference ─────────────────
artifacts:
rate-table art:kb-it-rates-2027 pin v2 sha256:d4e1...
update: proposed-by supplier, approved-by buyer,
at-most 1 per year, window January,
bounds within index-clause cap # an illegal update
# cannot become v3
calendar art:se-working-days track latest sha256:88a0...
attest-table art:lund-attestordning track latest-approved # buyer-maintained
rates: from rate-table # variables resolve against the pinned
senior 895 SEK/h indexed-by aki cap 4%/yr # version at each event's
junior 645 SEK/h indexed-by aki cap 4%/yr # effective date
# ── PORTS: the capability manifest ─────────────────────────────
ports:
in allocation.request from buyer payload peppol.order # avrop
in timesheet.submit from supplier payload sfti.timesheet # people
in invoice.submit from supplier payload peppol.bis-billing-3 # invoicing IN: allowed
in payment.confirm from buyer payload iso20022.pain.002
in approval.grant from buyer.attestant payload signed-decision
in notice.send from both payload signed-text
out receipt.emit to submitter always # proof of execution
out notice.emit to both on rule-alerts
out payment.instruct CLOSED # referee mode: machine never moves money
out invoice.issue CLOSED # invoicing OUT: not part of this contract
# material.delivery, rental.claim: NO PORT DECLARED -> materially and
# legally not allowed under this contract. Absence is prohibition.
# ── CADENCE: rate limits, in contract terms ────────────────────
cadence:
invoice.submit at-most 1 per month per supplier # faktureringsintervall
invoice.submit not-before matching delivery # no advance billing
payment.confirm due within 30 days of invoice accept # betalningsvillkor;
# late -> obligation breach, dröjsmålsränta claimable
allocation.request at-most 20 per day per unit # anti-splitting guard
# ── APPROVAL: straight-through vs someone-in-the-loop ──────────
approval:
invoice.submit:
if total <= 50_000 SEK -> straight-through # STP leg: no approval, immediate
if total <= 1_000_000 -> require 1 of buyer.attestant
otherwise -> require 2 of buyer.attestant # four-eyes / tvåhandsprincipen
approver buyer.attestant means:
human eIDAS-signature (attestordning ref: reglemente 2026-11)
| agent signing-key k-ai-01 bound-to policy sha256:ab12... # AI approval = a keypair
# + a HASHED, versioned policy
timeout: approval pending > 10 working-days -> escalate level 1
# ── ESCALATION: throw / catch / finally, for contracts ─────────
escalation:
on pool-exhausted | rejected(3 times, 30d) | obligation-breached | approval-timeout:
level 1 notify avtalsansvariga both parties resolve-within 10 working-days
level 2 suspend port invoice.submit steering-group, 20 working-days
level 3 formal dispute: mediation per prose §15 draws frozen, receipts keep flowing
level 4 termination path opens per prose §17
finally: # the survival clauses - run on ANY exit
sekretess per §16 lives 5y past termination
garantiåtagande 24mo per delivery
return-of-materials within 30d
# ── RULES: if / which, nothing else ────────────────────────────
rule on allocation.request:
if payload.role not in rates -> reject "role not on rate card"
which payload.role is
senior: if pool hours.senior remaining < payload.hours -> reject "senior pool exhausted"
junior: if pool hours.junior remaining < payload.hours -> reject "junior pool exhausted"
reserve pool hours.[payload.role] amount payload.hours for payload.unit
accept
rule on timesheet.submit:
if no reservation matches payload.allocation-ref -> reject "no call-off behind hours"
if payload.hours > reservation.remaining -> reject "exceeds call-off"
record deliveries { role: payload.role, hours: payload.hours, period: payload.period }
accept
rule on invoice.submit: # payload = EN 16931 invoice
if payload.BT-13 not references this.id -> reject "no contract reference" # BT-13: order ref
each line in payload.lines: # bounded map over payload - only iteration allowed
which line.item is
hours.senior: expected := rates.senior at line.period # index math runs here
hours.junior: expected := rates.junior at line.period
otherwise: reject "line item not in contract"
if line.BT-146 != expected -> reject diff { # BT-146: item net price
billed: line.BT-146, contractual: expected,
clause: rates + index aki, excess: (line.BT-146 - expected) * line.qty }
if line.qty > deliveries unbilled for line.item -> reject "no delivery behind quantity"
if invoice.total + pool budget.total consumed > pool budget.total capacity
-> reject "takvolym breach"
draw pools; record invoices; accept # -> then the approval gate decides the leg:
# straight-through, or waits for approval.grant
rule on payment.confirm:
if payload.ref not in invoices -> reject "unknown invoice"
mark invoices[payload.ref] paid
accept
# ── ALERTS: assertions, not dashboard config ───────────────────
alerts:
when pool hours.senior consumed > 80% notify both "senior pool at {level}%"
when today in notice-window(validity.extend) notify buyer "extension decision due"
when fixing aki stale > 13mo notify operator "index feed silent"
# ── EXECUTION CONTRACT (the machine's promises) ────────────────
execution:
idempotency: every submission carries submission-id (uuid v7);
duplicate id -> return original receipt, state unchanged
ordering: events sequenced per contract; receipt N embeds hash(receipt N-1)
receipts: signed by platform seal; delivered over Peppol MLR-style response
replay: state is a pure fold over the receipt chain; any party can verify
determinism: no clock, no I/O, no floats, no unbounded loops -> lint-provable
What a receipt looks like (proof of execution)
An earlier draft drew the receipt flat, transport machinery and judgment mixed in one bag. Structured properly, every receipt is a universal envelope around a typed body:
{
"envelope": { // mk-envelope v1: the SAME fields for
"artifact": "mk/receipt", // every receipt, every port, every profile
"body-type": "invoice.submit/verdict v1",
"receipt": "r-000217",
"contract": "mk-se-2027-000042 @ v3 (hash 9f2c44...)",
"port": "invoice.submit",
"submission-id": "0197f2aa-...-7cde",
"payload-hash": "sha256:77ab01...",
"prev-receipt": "sha256:c41d90...",
"state-before": "sha256:5e12fa...",
"state-after": "sha256:5e12fa...", // unchanged: rejected
"verdict": "REJECTED", // closed enum - lives HERE; the
"body-hash": "sha256:e0a1b2...", // reasoning lives in the body (below)
"at": "2028-04-03T09:14:22Z"
},
"body": { // schema: invoice.submit/verdict v1
"rule-trace": [
"invoice.submit: BT-13 ok",
"line 3: which item -> hours.senior",
"line 3: expected = 895 × aki(2028-03)/aki(2026-09) = 931.70",
"line 3: BT-146 = 950.00 ≠ 931.70 -> REJECT",
"diff: excess 18.30/h × 140h = 2,562.00 SEK, clause: rates.senior + index aki"
]
},
"seal": "JAdES platform seal over the envelope (RFC 3161 timestamp); the body is
bound through body-hash - detachable without breaking chain or signature"
}
The rejection cites the clause, shows the arithmetic, and is signed. A dispute about this invoice is now a conversation about an 8-line receipt, not about a 40-page PDF. And because the supplier can run the same contract file BEFORE sending (pre-validation - the two-sided pull), the honest supplier never sends that invoice at all.
Envelope and body: one grammar for every message
The split is not an aesthetic choice - the stack this design rides has been enveloped at every layer for forty years, and we mirror the pattern one level up (the same move as MLR -> judgment receipt):
| Layer | Envelope | Body |
|---|---|---|
| EDIFACT (1987) | UNB/UNH interchange + message headers | the INVOIC segments |
| Peppol transport | AS4/ebMS3 messaging envelope | payload |
| Peppol business | Business Message Envelope (SBDH) v2.0.1: sender, receiver, document type, instance id | the UBL invoice itself |
| ISO 20022 | Business Application Header (head.001) | the pain/camt Document |
| Modern web | JOSE protected header · CloudEvents attributes | payload / data |
| W3C VC 2.0 | literally called “enveloping proofs” (VC-JOSE-COSE) | the credential claims |
A Peppol access point routes on the SBDH without ever parsing the invoice inside it. The mk grammar wants exactly that property, so:
- The envelope is mk-core’s, frozen and universal. Identity (contract @ version hash, port), correlation (submission-id, payload-hash), sequence (prev-receipt, state hashes), the verdict enum, the body hash, the timestamp, the seal. Chain verifiers, archives, auditors, and dashboards process envelopes without understanding a single body schema - integrity is checkable by software that knows nothing about invoicing.
- The body is typed per port, versioned, profile-constrained.
invoice.submit/verdict v1is a different schema fromallocation.request/verdict v1; the envelope’sbody-typedeclares which schema the body must be held against - “the body must also adhere to some schema”, literally. Bodies evolve by versioning; profiles constrain them, CIUS-style (§6); the envelope never changes. - The verdict sits in the envelope; the judgment sits in the body. The verdict is a closed enum -
ACCEPTED / REJECTED / PENDING / ESCALATED / FAULTED(PENDING generalized per §7.5; FAULTED = never judged on the merits, §4.3) - universal machine semantics, the same five words for an invoice as for a timesheet. The rule trace (the arithmetic, the clause citations, the rate card) is port-specific judgment - and is exactly where the OSL 31:16-sealed material lives (§12: hourly and unit prices). - Which makes redaction fall out of the grammar. Detach the body, keep the envelope: the receipt still chains (prev-receipt and state hashes intact), still verifies (the seal covers the envelope; the body binds via body-hash), and still answers “invoice r-000217: REJECTED”. §12’s scoped receipts stop being custom machinery - a disclosure scope is just who gets bodies: parties get both, a steering group gets envelopes + digests, the public allmän-handling view gets envelopes. Salted per-field hashing inside the body (SD-JWT style) is the v2 refinement, on the same boundary.
- Everything on the wire wears the same envelope. Submissions arrive enveloped (their SBDH maps onto mk-envelope fields), receipts leave enveloped, artifact versions publish enveloped (§5.6’s who-signs table IS an envelope spec), and the contract itself is one - §2’s header block (id, parties, validity, prose hash, seals) is the envelope; state, ports, and rules are the body. One grammar, four artifact classes, zero special cases.
Three tiers of language: executable, soft, comment
Soft contract language is something else - the worked example’s annex: line was this idea in embryo; promoted properly, every piece of language in a mechanical contract sits in exactly one of three tiers:
| Tier | What it is | Binding status |
|---|---|---|
| Executable term | rules, ports, pools, cadences, imports | signed AND machine-enforced; carries implements: ["§7.2"] backlinks to the prose clauses it operationalizes |
| Soft term (prose-term) | force majeure, sekretess, lojalitetsplikt, “skäligt”/“väsentligt” clauses - legally operative language no machine should pretend to run | first-class and hashed: clause id + verbatim text inside the signed Ricardian bundle, visible in every rendering, prose governs on conflict (§10). NOT a comment - it binds the parties, just not the machine |
| Comment | annotations, rationale, examples | non-operative, hashed with the document but explicitly binding nobody |
The tier split makes coverage a computable, honest number: every clause of the source prose is implemented (linked executable terms), soft-carried (prose-term, deliberately non-executable), or unhandled (a translation gap that must be resolved before signing - the linter refuses genesis with unhandled clauses, same spirit as §7.6’s strict linking). Translation in both directions is then well-defined: prose → mk is tier-assignment + rule authoring + clause linking (with the coverage report as the deliverable that keeps the translator honest), and mk → prose is a deterministic projection - Catala’s literate lesson pointed backwards: render each executable term as readable contract language beside its formal block, soft terms verbatim, the port manifest as a capability table. Both directions are running experiments in the reference implementation’s translation and rendering legs.
Findings from the prose-to-mk translation experiment - a 19-clause Swedish ramavtal translated end-to-end: 63% implemented / 32% soft / 1 comment / 0 unhandled, and 18 of 19 clauses tiered cleanly. Two creaks, both spec-relevant for v-next: (1) clauses are not tier-pure - real legal clauses mix an executable spine with soft connective tissue (a tvistlösning clause carries both the eskaleringstrappa AND lagval/“skälig tid” prose), so the tier must eventually attach at sub-clause granularity, not whole clauses; (2) “implemented” is binary but fidelity is not - an index formula translates airtight while “får inte delas upp i syfte att kringgå attest” is enforced only by a structural proxy (the anti-splitting cadence guard); coverage needs a fidelity dimension (faithful / proxy / partial) alongside the tier, or proxies masquerade as full captures.
Findings from the rendering experiment - the reverse projection works and found its own structural truth: the clause↔term relationship is many-to-many (one rates block implements §6 AND §7; one §10 spawns a port, two cadences, and a rule), so Catala-style physical adjacency is impossible without duplicating terms. The renderer’s answer, adopted into the design: two cross-linked spines - the Avtalstext (every clause once, in order, tier-tagged, citing the machine terms that enforce it) and the machine terms (every node once, as tables, citing back to their clauses) - “beside” achieved by bidirectional reference, not layout, which keeps the completeness invariant (every clause and every node rendered exactly once, machine-checked) provable. The projection opens by declaring itself: maskinell projektion; vid motstridighet gäller prosan.