Open Standard · Working Draft
mekaniskt·kontrakt

Worked examples  ·  08 of 09

Regional IT framework

A region's five-year IT-modernisation framework: consultant hours in three roles from capped, index-adjusted pools; hardware by Peppol despatch advice; a fixed-price migration paid on a betalplan with 10% retention; monthly invoicing; and a proforma invoice judged before the binding one - all under one multi-year spend ceiling.

SIZE
very complex, five-year
PROFILE
mk-se-public
STRESSES
Everything at once, across five years: three index-adjusted consultant-hour pools, goods by despatch advice, milestone retention gated by an appointed inspector, monthly invoicing, nested budget ceilings, and a proforma the machine judges before the binding invoice exists.

A region signs a five-year multi-service IT-modernisation ramavtal: consultant hours in three roles drawn from capped pools under an AKI-indexed rate card, hardware and licenses arriving by Peppol despatch advice into a goods register, a fixed-price platform-migration delprojekt paid by a signed betalplan with 10% retention, monthly invoicing at a declared cadence, and - new to the corpus - a proforma invoice the machine judges before the binding one exists. One spend ceiling over everything, per-year caps under it, per-service caps beside them, two extension options exercised by active renewal, an independent granskare deciding when milestones are done, and an index revision landing every January. The school-renovation example proved complexity composes at project scale; this one proves it composes across years - genesis to expiry is roughly sixty months and several hundred receipts, and nothing in between needs a wall clock. It mostly holds; five things creak.

Profile: mk-se-public. Prose backbone: the awarded framework agreement plus its administrative provisions as the hashed Ricardian bundle; confidentiality, force majeure, the data-processing agreement, and exit obligations live in the soft-term layer. One honesty note carried as a soft term: LOU’s four-year default for framework agreements is exceeded here (3 years + 2 x 12 months = 5), on documented särskilda skäl - the migration horizon of the delprojekt. The machine does not check LOU; the profile’s linter flags the duration so the särskilda-skäl clause cannot be forgotten.

The contract

mechanical-contract v0
id:       mk-se-2027-002400
title:    Ramavtal IT-modernisering Region Söderland 2027-2031
prose:    ramavtal-it-modernisering.pdf sha256:b7e2...   # kontrakt + AF; femårig löptid
annex:    [ "§18 Sekretess", "§21 Force majeure",        # på dokumenterade särskilda
            "§23 Personuppgiftsbiträde (GDPR art 28)",   # skäl (LOU 7:2) - soft terms,
            "§25 Exit och överlämning" ]                 # hashed, prose governs

parties:
  beställare:  org 232100-7719 "Region Söderland"      sign eIDAS-qualified-seal
  leverantör:  org 556812-4409 "Nordkraft Digital AB"  sign eIDAS-qualified-seal
  granskare:   APPOINTED, not signed at genesis        # party slot: the independent
    slot: certified per art:oberoende-it-granskare     # inspector gating milestone
    appointed-by: both, via granskare.appoint port     # money - constraints bound now,
                                                       # the person bound later

validity:
  from 2027-01-01 to 2029-12-31
  extend: 2 x 12mo, notice 9mo, mode: active-renewal   # full horizon 2031-12-31.
                                                       # active-renewal: silence =
                                                       # expiry; the negative option
                                                       # is impossible (§2)
money:
  currency SEK
  vat 25% on services and goods
  rounding per-line half-up 0.01                       # öre are the unit; no floats
                                                       # exist anywhere in this file
# ── DECLARED STATE ─────────────────────────────────────────────
state:
  # one ceiling over everything, service sub-caps under it
  pool takvolym           capacity 120_000_000 SEK
       stages available|reserved|retained|disbursed    # declared stages:
                                                       # 'retained' for the 10%
  pool budget.timmar      capacity 70_000_000 SEK   sub-cap of takvolym
  pool budget.material    capacity 28_000_000 SEK   sub-cap of takvolym
  pool budget.milstolpar  capacity 22_000_000 SEK   sub-cap of takvolym
  pool budget.år          keyed-by year within validity          # keyed pool; an
                          capacity 30_000_000 SEK each           # extension adds
                                                                 # year-cells
  # people: role pools over five years (§8.5 human flavor)
  pool timmar.senior      capacity 24_000 h
  pool timmar.specialist  capacity 8_000 h
  pool timmar.junior      capacity 16_000 h

  register avrop           shape peppol.order-line
  register utförda-timmar  shape { avrop-ref: id, roll: code,
                                   timmar: quantity<h>, period: month }
  register leveranser      shape despatch-line        # goods in the house, not yet
                                                      # invoiced: the fact every
                                                      # material line must stand on
  register milstolpar      from betalplan             # rows -> event-dues (§3.4)
  register fakturor                                   # 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
             # labour-cost index for J62 (IT services); ingest-attested per §5.5
             # until SCB seals its series - trust tier declared, not assumed

# ── ARTIFACTS: every table this contract lives by, hashed ──────
artifacts:
  timprislista   art:nordkraft-timpriser-2027   pin v1   sha256:d4a9...
                 class contract-bilaga
                 update: proposed-by leverantör, approved-by beställare,
                         at-most 1 per year, window January,
                         bounds within index-clause cap     # an illegal update
                                                            # cannot become v2 (§5.2)
  prisbilaga     art:nordkraft-materialpriser   track latest-approved   sha256:71c0...
                 class contract-bilaga
                 update: proposed-by leverantör, approved-by beställare,
                         at-most 2 per year, window January|July,
                         bounds within 5% per article       # negotiated moves, governed;
                                                            # NOT indexed
  betalplan      art:betalplan-plattformsmigrering  pin v1  sha256:1f8e...
                 class contract-bilaga               # M1..M5, 18_000_000 SEK total;
                                                     # each row -> an event-due (§3.4)
  kalender       art:se-working-days   track latest          sha256:88a0...
  attestordning  art:soderland-attestordning   track latest-approved
                 class party-maintained-register     # buyer-maintained (§5.6);
                                                     # counterparty receipted on change

rates: from timprislista            # variables resolve against the pinned version
  senior      1_150 SEK/h  indexed-by aki  cap 3%/yr    # in force at each event's
  specialist  1_390 SEK/h  indexed-by aki  cap 3%/yr    # effective date (§5.1)
  junior        795 SEK/h  indexed-by aki  cap 3%/yr
  # material prices are NOT indexed: they move only by governed prisbilaga
  # versions. One revision mechanism per line item, never both.

# ── PORTS: the capability manifest ─────────────────────────────
ports:
  in  avrop.order        from beställare   payload peppol.order          # call-off:
                                                                         # hours or goods
  in  timesheet.submit   from leverantör   payload sfti.timesheet
  in  material.delivery  from leverantör   payload peppol.despatch-advice
  in  milestone.claim    from leverantör   payload signed-claim
  in  granskare.appoint  from both         payload signed-appointment    # fills the slot
  in  leveranskontroll   from granskare    payload signed-inspection
  in  proforma.submit    from leverantör   payload en16931-invoice type-code 325
                         judges invoice.submit, moves nothing            # BT-3 = 325
                                                                         # (proforma): the
                                                                         # judged, non-
                                                                         # binding twin
  in  invoice.submit     from leverantör   payload peppol.bis-billing-3  # BT-3 = 380
  in  payment.confirm    from beställare   payload iso20022.pain.002
  in  approval.grant     from beställare.attestant   payload signed-decision
  in  extension.confirm  from beställare   payload signed-decision       # active renewal
  in  notice.send        from both         payload signed-text
  out receipt.emit       to submitter      always
  out notice.emit        to both           on rule-alerts
  out payment.instruct   CLOSED            # referee mode: the machine judges the
                                           # region's ERP-run payments, never moves money
  out invoice.issue      CLOSED            # invoicing OUT is not this contract's business
  # fordran.assign (factoring): NO PORT -> the receivable is not assignable
  #   under this contract; assignment requires an amendment (transform, §6.4)
  # ue.order (subcontracting): NO PORT -> no back-to-back child under THIS
  #   contract. Absence is prohibition (§1).

# ── CADENCE: the contract's clock-free rhythm (§3.2) ───────────
cadence:
  proforma.submit    at-most 4 per month             # its own bucket - a proforma
                                                     # NEVER consumes the invoice cadence
  invoice.submit     at-most 1 per month             # faktureringsintervall
  invoice.submit     not-before matching delivery | timesheet | milestone-due
                                                     # no advance billing, any service
  payment.confirm    due within 30 days of invoice accept    # betalningsvillkor; late
                                                     # -> breach, dröjsmålsränta per
                                                     # räntelagen, computed from the log
  avrop.order        at-most 30 per month per förvaltning    # anti-splitting guard:
                                                     # attest thresholds cannot be
                                                     # snuck under (§3.2)
  timesheet.submit   at-most 1 per month per avrop
  extension.confirm  within notice-window(validity.extend)   # 9mo before expiry ..
                                                     # expiry; outside = rejected

# ── APPROVAL: straight-through vs someone-in-the-loop (§4.1) ───
approval: invoice.submit
  if total <= 100_000 SEK    -> straight-through
  if total <= 2_000_000 SEK  -> require 1 of beställare.attestant
  otherwise                  -> require 2 of beställare.attestant
  # attestant resolves against art:soderland-attestordning at event date;
  # tvåhandsprincipen above 2M. DMN hit-policy UNIQUE: rows tile, no overlap.
  timeout: approval pending > 10 working-days -> escalate level 1

approval: milestone.claim
  require 1 of granskare                              # leveranskontroll godkänd -
                                                      # third-party gate on every
                                                      # milestone krona
  timeout: leveranskontroll pending > 15 working-days -> escalate level 1

# ── RULES: if / which, nothing else (§9) ───────────────────────
rule on avrop.order:                                  # payload = Peppol order
  which payload.kind is
    tjänst:
      if payload.roll not in rates                    -> reject "roll ej på timprislistan"
      if timmar.[payload.roll] remaining < payload.timmar -> reject "timpott uttömd"
      reserve timmar.[payload.roll] amount payload.timmar for payload.förvaltning
      reserve budget.timmar via takvolym, budget.år[payload.year]
              amount payload.timmar × rates.[payload.roll]      # ONE reservation,
    vara:                                                       # THREE ceilings debited
      if payload.artikel not in prisbilaga            -> reject "artikel ej i prisbilaga"
      reserve budget.material via takvolym, budget.år[payload.year]
              amount payload.qty × prisbilaga[payload.artikel]
    otherwise:                                        -> reject "okänd avropstyp"
  record avrop
  accept

rule on timesheet.submit:
  check reservation matches payload.avrop-ref  else reject "inga timmar utan avrop"
  if payload.timmar > reservation.remaining           -> reject "överskrider avropet"
  record utförda-timmar { roll: payload.roll, timmar: payload.timmar,
                          period: payload.period }
  accept

rule on material.delivery:                            # payload = Peppol despatch advice
  check lines reference accepted avrop  else reject "leverans utan avrop"
  each line in payload.lines:
    if line.qty > avrop[line.order-ref] remaining     -> reject "mer än avropat"
  record leveranser                                   # goods register grows; nothing
  accept                                              # is billable that isn't in here

rule on proforma.submit:                              # the judged twin.
  judge as invoice.submit                             # the ENTIRE invoice gauntlet
                                                      # runs verbatim - BT-13, line
                                                      # prices, index math, delivery
                                                      # backing, all three ceilings -
  effects none                                        # but accepted or rejected,
  accept advisory                                     # NOTHING moves: no draws, no
                                                      # register rows, no invoice
                                                      # cadence consumed. The receipt
                                                      # chains like any other; the
                                                      # binding invoice may cite it.

rule on invoice.submit:                               # payload = EN 16931 invoice
  if payload.BT-3 != 380                              -> reject "fel dokumenttyp"
  if payload.BT-13 not references this.id             -> reject "ingen avtalsreferens"
  each line in payload.lines:                         # bounded map, the only loop
    which line.item is
      timmar.senior | timmar.specialist | timmar.junior:
        let expected := rates.[line.item] at line.period  # index math: base ×
                                                      # aki(period)/aki(2026-09),
                                                      # capped at 3%/yr compounded
        if line.BT-146 != expected                    # BT-146: item net price
          -> reject diff { billed: line.BT-146, contractual: expected,
                           clause: rates + index aki,
                           excess: (line.BT-146 - expected) × line.qty }
        if line.qty > utförda-timmar unbilled for line.item
          -> reject "timmar utan tidrapport"
      artikel:
        if line.BT-146 != prisbilaga[line.artikel] at delivery-date
          -> reject diff { billed: line.BT-146,       # priced at the prisbilaga
                           contractual: prisbilaga[line.artikel],   # version in force
                           clause: prisbilaga }                     # at DELIVERY (§5.1)
        if line.qty > leveranser unbilled for line.artikel
          -> reject "ingen leverans bakom raden"      # the goods covenant
      milstolpe:
        check matching open due from milestone.claim
          else reject "milstolpe ej godkänd"          # the 90% event-due is the
                                                      # only thing billable here
      otherwise:                                      -> reject "rad utanför avtalet"
  if invoice.total would breach takvolym | budget.år[year] | matching sub-cap
    -> reject "takbrott" citing the binding ceiling   # three ceilings, one guard;
                                                      # the receipt names WHICH one
  draw pools per line; record fakturor
  accept                                              # -> the approval table picks
                                                      #    the leg: STP, 1, or 2

rule on milestone.claim:
  if no matching row in betalplan                     -> reject "ej i betalplan"
  expect leveranskontroll godkänd for payload.milstolpe   # §7.5 call-out shape: the
                                                      # claim parks PENDING (a fold
                                                      # state, not an authored verb),
                                                      # clock-blind, resumes on inspection
  reserve budget.milstolpar via takvolym, budget.år[year] amount row.belopp
  row.belopp × 90% -> due payment within 30 days      # billed via invoice.submit
  row.belopp × 10% -> retained                        # innehållet belopp: comes
  accept                                              # home at slutmilstolpen

rule on leveranskontroll:                             # the granskare speaks
  which payload.result is
    godkänd:
      if payload.milstolpe is slutmilstolpe:          # fork (§3.7): one event
        release retained -> due payment within 30 days    # releases retention AND
        open garantitid 24 months                         # opens garanti AND opens
        open exit-åtagande per annex §25                  # the exit obligations
    godkänd-med-anmärkning:
      due åtgärda anmärkningar within 30 days
    underkänd:
      due ny leveranskontroll; claim stays parked
  accept

rule on extension.confirm:                            # active renewal exercised
  check within notice-window(validity.extend)  else reject "utanför fönster"
  extend validity +12 months                          # this does more than bump a
  add budget.år cell for the new year                 # date - it mints a fresh 30M
  accept                                              # year-cell

rule on payment.confirm:
  if payload.ref not in fakturor                      -> reject "okänd faktura"
  record fakturor[payload.ref] = paid                 # late confirm = breach; the
  accept                                              # interest claim computes from
                                                      # the log's due date (§3.4)

# ── ALERTS: assertions over the fold, not dashboard config ─────
alerts:
  when takvolym consumed > 80%                     notify both "takvolym {level}%"
  when budget.år[current] consumed > 90%           notify beställare "årstak nära"
  when timmar.senior consumed > 80%                notify both "seniorpott {level}%"
  when today in notice-window(validity.extend)     notify beställare "förlängningsbeslut krävs"
  when fixing aki stale > 13mo                     notify operator "indexserie tyst"

# ── ESCALATION: throw / catch / finally (§4.2) ─────────────────
escalation:
  on pool-exhausted | obligation-breached | rejected(3 times, 30d) | approval-timeout:
    level 1  avtalsansvariga both parties      resolve-within 10 working-days
    level 2  styrgrupp; suspend avrop.order    20 working-days
    level 3  mediation per prose               draws frozen, receipts keep flowing
    level 4  termination path per prose
  finally:                              # survival, on ANY exit - incl. quiet
    sekretess per §18                   lives 5y past termination      # expiry 2031-12-31
    garantiåtagande                     24mo per delivery / milestone
    exit-åtagande per §25               datamigrering + dokumentation within 90 days
    personuppgiftsbiträde per §23       until data returned or erased, receipted

# ── EXECUTION CONTRACT ─────────────────────────────────────────
execution:
  idempotency:  submission-id (uuid v7); duplicate -> original receipt, state unchanged
  ordering:     single log per contract; receipt N embeds hash(receipt N-1)
  replay:       state is a pure fold over the receipt chain - all ~sixty months of it
  determinism:  no clock, no floats, no I/O, no unbounded loops - lint-provable (§1).
                Every "date" above is an event timestamp already in the log; the
                January index revision, the 9-month notice window, the 30-day due -
                all event-time arithmetic (§3.2, §3.4).

The happy path, as receipts

Sixty months compress to the load-bearing submissions; ... marks the monthly cycle repeating. Every row below is a signed, hash-chained envelope; replaying the chain reproduces the final state bit-for-bit.

# Event Verdict State after
r‑0 genesis (both seals) ACCEPTED pools full, granskare slot EMPTY, milstolpar loaded from betalplan
r‑1 granskare.appoint (both parties) ACCEPTED slot bound: Certezza Granskning AB, per competence register
r‑2 avrop.order - tjänst: 1 200 h senior, förvaltning Vård-IT ACCEPTED timmar.senior 1 200 h reserved; 1 380 000 SEK reserved in budget.timmar + takvolym + år[2027]
r‑3 avrop.order - vara: 40 st accesspunkter ACCEPTED 388 000 SEK reserved in budget.material + takvolym + år[2027]
r‑4 timesheet.submit (2027-02, 130 h senior) ACCEPTED utförda-timmar +130 h unbilled
r‑5 material.delivery (despatch advice, 40 st) ACCEPTED leveranser +40 st unbilled
r‑6 proforma.submit (mars: 130 h + 40 st) REJECTED diff: artikel billed 10 350,00, contractual 9 700,00, clause prisbilaga. Nothing moved, no cadence consumed - the supplier fixes the line without having submitted a defective invoice
r‑7 proforma.submit (corrected) ACCEPTED (advisory) still nothing moved; receipt r‑7 chains and is citable
r‑8 invoice.submit (binding twin, BT-3 380: 149 500 + 388 000 = 537 500 net) PENDING ≤ 2M -> awaiting 1 of beställare.attestant; pools untouched until the leg completes
r‑9 approval.grant (attestant, per attestordning) ACCEPTED fakturor +1; draws land; due: payment ≤ 30 days
r‑10 payment.confirm (pain.002) ACCEPTED faktura paid; disbursed stage grows
monthly cycle repeats: timesheet -> delivery -> proforma (optional) -> invoice -> payment
r‑41 fixing revision: aki 2028-01 published (ingest-attested) ACCEPTED rates for 2028 periods now resolve indexed
r‑42 invoice.submit (jan 2028: senior @ 1 184,50) ACCEPTED (STP, < 100k) rule trace shows the math: raw aki-ratio 1,043 > cap -> 1 150 × 1,03 = 1 184,50/h - the cap binds
r‑55 prisbilaga update proposed (July window, +3,8% on switchar) ACCEPTED prisbilaga v2 in force; all later deliveries price against it
r‑56 prisbilaga update proposed (October) REJECTED outside window - never becomes a version (§5.2); the quiet-raise door does not open
r‑70 milestone.claim M3 “Plattform i drift” (4 000 000 SEK) PENDING parked, awaiting leveranskontroll
r‑71 leveranskontroll M3: godkänd ACCEPTED claim resumes: 3 600 000 due ≤ 30d; 400 000 -> retained
r‑72 invoice.submit (M3 90%, 3 600 000) PENDING -> ACCEPTED > 2M -> 2 attestants (tvåhandsprincipen); then due payment ≤ 30d
r‑102 extension.confirm #1 (2029-05, inside the 9-month window) ACCEPTED validity -> 2030-12-31; budget.år[2030] minted at 30M
r‑160 extension.confirm #2 (2030-04) ACCEPTED validity -> 2031-12-31; år[2031] minted; horizon complete
r‑201 leveranskontroll slutmilstolpe M5: godkänd ACCEPTED fork: all retained (1 800 000 across M1..M5) released -> due ≤ 30d; garantitid 24mo opens; exit-åtagande opens
r‑202 (2031-12-31 passes - no event needed: expiry is event-math, §3.4) - contract expires; finally survives: sekretess, garanti, exit, PUB-avtal keep their own dues

A dispute anywhere in those five years is a receipt, not an archaeology project: “why was the March 2027 invoice wrong” is r‑6’s eight-line diff; “who approved the 3,6M milestone payment” is r‑72’s two attestant signatures; “when did the switch price change and was it legal” is r‑55 versus r‑56.

What this example stresses

Edge Spec section
Five-year validity with active-renewal extensions and notice windows, all event-time §2, §3.2, §3.4
Proforma: the full invoice gauntlet run as a receipted, state-free judgment §2, §3.3 (pre-validation promoted on-log)
Three service lines (hours, goods, milestones) under one port manifest §1, §8.5
One draw debiting sub-cap + year-cap + spend ceiling at once §3.1 (Petri: one transition, several places), §3.4
Keyed per-year budget pools; an extension minting new cells §3.1, §6.4
Goods covenant: no invoice line without a despatch-advice line behind it §2, §10 (Peppol post-award)
Betalplan rows as event-dues; 90/10 retention; release on inspection §3.4
Third-party granskare in an appointed party slot gating milestone money §4.1
Monthly invoice cadence + not-before + anti-splitting + 30-day due, over 60 months §3.2
Two price-revision mechanisms: AKI fixing on hours, governed prisbilaga on goods §5.1-5.2, §2
Straight-through / 1-attestant / 2-attestant legs from a checked table §4.1
Escalation ladder + finally-obligations outliving a five-year natural expiry §4.2
Deliberately absent ports: factoring and subcontracting prohibited by silence §1, §7.1
Index math with a binding cap, replayable in the rule trace §2, §5

Questions this example forced

  • Proforma wants a port kind, not a rule trick. Pre-validation (§3.3) is stateless and off-log: run the file, get an answer, nothing recorded. A proforma is legally and commercially more than that - the supplier wants a citable, signed judgment (“the machine blessed this exact payload on this date”) before committing a binding invoice, and EN 16931 already has the document (BT-3 type code 325). Honest gap: Peppol BIS Billing 3 restricts BT-3 to a subset that excludes 325, so the proforma has the semantic model but no rail - it travels bilaterally or via the pre-validation endpoint. The judge as invoice.submit / effects none strawman works but smuggles a new semantics into rule syntax. Proposed: a declared port kind - probe - whose verdicts carry full rule traces, whose receipts chain normally, and whose ACCEPTED is defined as moving no state and consuming no cadence except its own. The binding submission may cite its probe receipt hash. A probe verdict can go stale (state moves between probe and binding twin), so a probe receipt must embed state-before like every envelope already does - staleness detectable by hash, not by promise.
  • One krona, three ceilings: pools want hierarchy. A consultant-hour reservation here debits budget.timmar AND budget.år[2027] AND takvolym. §3.4’s reserve names one pool; the via strawman is doing something the spec never defined. The Petri math is already bought - a transition consuming from several places is the ordinary case (§3.1) - but the grammar and the conservation story are not: are sub-caps a partition of the parent (sum of children equals parent, linted) or deliberate over-allocation (sum exceeds parent, flexibility)? Proposed: declared pool hierarchy with a per-hierarchy policy partition | over-allocated, draws always debiting the full path, and the linter extending pool-safety to “no path exceeds any ancestor”. Orthogonal axes (service × year) mean a draw sits under two independent hierarchies - exactly encumbrance accounting’s fund / appropriation / allotment structure, sixty years old.
  • Exercising an extension mints state. extend validity +12 months looked like a date bump until the keyed year-pool made it honest: the extension creates budget.år[2030], 30M of fresh capacity that did not exist before. Anything that changes capacity smells like a transform (a signed, conservation-checked delta) - but the extension was anticipated and priced at genesis: at-most 2 extensions, 12mo each, notice window, all declared. Proposed: anticipated change = declared headroom + an ordinary event within it; the conservation lint treats genesis-declared future capacity as latent (counted in the total from day one, activated by the event). The spend ceiling then means the ceiling over the full five-year horizon, extensions included - which is also what LOU’s takvolym case law demands the procurement documents state.
  • Two legal ways for a price to move; the linter must forbid a third. Hours move by formula against a published fixing (AKI ratio, capped - deterministic, nobody decides anything). Material moves by governed artifact update (proposed, approved, windowed, bounded - somebody decides, under rules). Both are in the spec; what it never says is that they are exclusive per line item. A rate that is BOTH indexed AND updatable is double-indexing waiting to happen. Proposed: every priced item declares exactly one revision mechanism (indexed-by XOR an owning artifact with update governance), and updating an indexed item’s base rate is legal only as a transform. The principle: formulaic, source-published adjustment = fixing; negotiated adjustment = governed update; silent adjustment = structurally impossible.
  • When does the region own the goods? The despatch advice records arrival; the invoice bills it; payment settles it - and the ownership verbs demand somebody declare where title moves. Retention of title (moves title when payment.confirm) is standard drafting, but at this contract’s cadence one monthly invoice covers many despatch lines and pays once - so “when paid” is per-line only if the goods register tracks title per line. Public-sector practice mostly takes title at approved delivery instead. Proposed: the profile default for public goods is title on accepted delivery; äganderättsförbehåll must be declared explicitly, and where it is, the linter requires the goods register’s shape to carry a title stage - the creak visible at authoring time instead of at insolvency.

Status: example, not roadmap. What it proves: the five-year, multi-service, everything-at-once framework agreement composes from existing primitives plus five vocabulary creaks, and not one of the creaks touches determinism - probe ports, pool hierarchies, latent capacity, revision exclusivity, and title stages are all declarations, never computation. Sixty months of contract life, one fold, no clock.

Tests

These behaviour tests exercise the contract above. Because the contract is executable it is testable: the same machine that evaluates it runs the tests - Gherkin to read, JavaScript to run.

Feature: Regional IT framework
  Scenario: A proforma flags a mispriced line and moves nothing
    When leverantör submits proforma.submit with an over-priced material line
    Then the verdict is REJECTED
    And nothing in state changes and no invoice cadence is consumed
    And the trace cites "prisbilaga"

  Scenario: The corrected binding invoice is then accepted
    Given the price is corrected to the prisbilaga version in force
    When leverantör submits invoice.submit
    Then the verdict is ACCEPTED or PENDING (per the approval leg)

  Scenario: A goods line with no delivery behind it is refused
    When leverantör submits invoice.submit for goods never despatched
    Then the verdict is REJECTED
    And the trace mentions "ingen leverans bakom raden"

  Scenario: Consultant hours above the indexed rate are rejected
    Given aki(2028-03) makes the capped senior rate 1184.50 SEK/h
    When leverantör submits invoice.submit billing senior at 1250.00
    Then the verdict is REJECTED
    And the receipt cites "rates + index aki"

  Scenario: A milestone not in the betalplan cannot be claimed
    When leverantör submits milestone.claim for "M9"
    Then the verdict is REJECTED
    And the trace mentions "ej i betalplan"

  Scenario: An extension confirmed outside its 9-month window is refused
    When beställare submits extension.confirm 3 months before expiry
    Then the verdict is REJECTED
    And the trace mentions "utanför fönster"

  Scenario: A second invoice in the same month breaks the cadence
    Given one invoice this month is already accepted
    When leverantör submits a second invoice.submit the same month
    Then the verdict is REJECTED
import { test, expect } from "bun:test"
import { createContractMachine } from "@mechanical-contract/test"

test("a goods line with no delivery behind it is refused", () => {
  const mk = createContractMachine("mk-se-2027-002400")
  const r = mk.as("leverantör").submit("invoice.submit", {
    "BT-3": "380",
    "BT-13": "mk-se-2027-002400",
    lines: [{ item: "artikel", artikel: "accesspunkt", qty: "40", "BT-146": "9700.00" }],
  })
  expect(r.rejected).toBe(true)
  expect(r.trace).toContain("ingen leverans bakom raden")
})

test("a milestone not in the betalplan cannot be claimed", () => {
  const mk = createContractMachine("mk-se-2027-002400")
  const r = mk.as("leverantör").submit("milestone.claim", { milstolpe: "M9" })
  expect(r.rejected).toBe(true)
  expect(r.trace).toContain("ej i betalplan")
})

test("an extension outside its 9-month window is refused", () => {
  const mk = createContractMachine("mk-se-2027-002400")
  const r = mk.as("beställare").submit("extension.confirm", {}, { at: "2029-11-01T00:00:00Z" })
  expect(r.rejected).toBe(true)
  expect(r.trace).toContain("utanför fönster")
})

test("a proforma judges without moving state, then the fixed invoice lands", () => {
  const mk = createContractMachine("mk-se-2027-002400")
  const proforma = mk.as("leverantör").submit("proforma.submit", { "BT-3": "325", overpriced: true })
  expect(proforma.movedState).toBe(false) // advisory: no draws, no register rows, no cadence
})