Worked example — a reservation/order spec, failures included
This page builds one small spec from nothing: a reservation service for a
web shop. You will see the whole four-layer journey — Intent →
Constraints → Model → Properties — and then the part most tutorials
skip: the mistakes. Every lint finding below is real output from
specodelic 0.10.0 on the file as it stood at that moment; the fixes are
the fixes that were actually made. The finished spec lives at
examples/reservation-order.md and lints clean —
you can check that yourself with spk lint docs/src/examples/reservation-order.md
from the repo root.
The machine truth behind every rule quoted here is the tool itself:
spk explain format, spk explain ears, spk explain lint-rules serve
the distilled format guide from inside the binary. This page teaches the
format through one example; the commands appear only where they earn
their place.
On a TTY spk prints human-readable text (shown throughout); piped, it
emits a JSON envelope instead.
The story
A customer submits an order for several items. The service holds stock for every line item and accepts the order — or rejects it outright, holding nothing, charging nothing. Accepted orders are charged exactly once, fulfilled, or released by cancellation or expiry. Deliberately out of scope: pricing, the payment gateway itself, and multi-order races for the last unit (see What the spec does not decide).
One capability, one file. The file is named reservation-order.md;
the format’s naming law says - maps to the namespace dot, so the
frontmatter id is reservation.order (_ would be literal).
Layer 1 — Intent
The frontmatter carries the whole intent as one EARS sentence; the opening prose is free-form and is never read by the linter.
---
id: reservation.order
kind: intent
statement: "WHEN a customer submits an order THE reservation service SHALL hold stock for every line item and accept the order, or reject it with nothing held and nothing charged."
---
# Reservation & Order
A small order pipeline: submitting an order holds stock for every line
item or rejects the order outright; accepted orders are charged exactly
once, fulfilled, or released by cancellation or expiry.
The statement is Event-Driven (WHEN <trigger> THE <system> SHALL <response>) — one of the five EARS patterns; a statement with no SHALL
or no pattern match fails linter.ears_syntax. spk explain ears has
the full list.
Now lint the file as it stands:
spk lint reservation-order.md
lint: 1 file(s) linted, 1 finding(s), 0 advisory warning(s)
reservation-order.md [linter.model_present] Model section incomplete: 0 states, 0 transitions — both sections must exist (empty-but-present beats absent)
→ Run: fix the reported invariants — each rule's semantics: spk explain lint-rules
linter.model_present fires because the Model section does not exist
yet — a spec with an intent but no model is incomplete by construction,
and the checker says so from the first pass. Keep the finding in mind;
the next two layers exist to resolve it.
Layer 2 — Constraints
The Constraints table holds the invariants the design actually rests on.
Each row is id | kind | expr | traces_to; traces_to is a typed
wiki-link pointing at this file’s own intent row — bare text does not
resolve. Here is the first draft, warts included: the author wrote the
last constraint’s traces_to against a misremembered id
(reservation.orders, plural).
| id | kind | expr | traces_to |
|--------------------------------|-----------|----------------------------------------------------------------------------------------------------------------------|---------------------|
| all_or_nothing | invariant | `the order is accepted only if every line item's requested quantity is available; otherwise no unit is held and nothing is charged` | [[reservation.orders]] |
| held_or_converted | invariant | `every unit held for an accepted order is either converted to a fulfillment or released; no held unit outlives both` | [[reservation.order]] |
| release_is_total | invariant | `when a hold is released, no unit remains held for that order; releasing an already-released hold changes nothing` | [[reservation.order]] |
| charge_iff_fulfilled | invariant | `the customer is charged if and only if the order reaches fulfilled` | [[reservation.order]] |
| cancel_refunds_exactly_charged | invariant | `IF a charged order is cancelled, THEN the refund equals exactly the amount charged — never more, never less` | [[reservation.order]] |
| expiry_releases_holds | invariant | `an unfulfilled reservation expires after its TTL; expiry releases the hold without charging the customer` | [[reservation.order]] |
spk lint reservation-order.md
lint: 1 file(s) linted, 9 finding(s), 0 advisory warning(s)
reservation-order.md [linter.model_present] Model section incomplete: 0 states, 0 transitions — both sections must exist (empty-but-present beats absent)
reservation-order.md [linter.total_refs] dangling reference `[[reservation.orders]]` from constraints.traces_to — target not defined in any spec file
reservation.order [linter.coverage] constraint `all_or_nothing` has no deriving property — ∃ property.derives_from == `reservation.order.all_or_nothing` is required — write `[[reservation.order.all_or_nothing]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `cancel_refunds_exactly_charged` has no deriving property — ∃ property.derives_from == `reservation.order.cancel_refunds_exactly_charged` is required — write `[[reservation.order.cancel_refunds_exactly_charged]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `charge_iff_fulfilled` has no deriving property — ∃ property.derives_from == `reservation.order.charge_iff_fulfilled` is required — write `[[reservation.order.charge_iff_fulfilled]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `expiry_releases_holds` has no deriving property — ∃ property.derives_from == `reservation.order.expiry_releases_holds` is required — write `[[reservation.order.expiry_releases_holds]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `held_or_converted` has no deriving property — ∃ property.derives_from == `reservation.order.held_or_converted` is required — write `[[reservation.order.held_or_converted]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `release_is_total` has no deriving property — ∃ property.derives_from == `reservation.order.release_is_total` is required — write `[[reservation.order.release_is_total]]` in the property's derives_from cell
reservation.order [linter.single_root_reachable] 1 row(s) unreachable from any intent row — an orphaned island (traces_to/derives_from/guard/from-to/emits): reservation.order.all_or_nothing
→ Run: fix the reported invariants — each rule's semantics: spk explain lint-rules
Stop and read what one typo bought. Three different rules fired on one wrong cell, and they say different things:
linter.total_refs— the reference dangles: nothing definesreservation.orders. The target id isreservation.order.linter.coverage— every constraint must have a deriving property (the fourth layer’s job; six of these findings stay until then).linter.single_root_reachable—all_or_nothingis now an orphaned island: with its only link broken, it cannot reach the file’s own intent row, and neither can anything hanging off it.
That last one is the format’s structural conscience. Fix the typo:
| all_or_nothing | invariant | `the order is accepted only if every line item's requested quantity is available; otherwise no unit is held and nothing is charged` | [[reservation.order]] |
and the dangling-reference and orphaned-island findings clear — one
cell, two findings. (The coverage finding for all_or_nothing itself
stays: it fires because nothing derives from the constraint yet, not
because of the typo — all six coverage rows stand until the next layer.)
What remains is linter.model_present plus the six coverage rows; those
are next — the count dropped from 9 to 7, all of it the typo’s:
lint: 1 file(s) linted, 7 finding(s), 0 advisory warning(s)
reservation-order.md [linter.model_present] Model section incomplete: 0 states, 0 transitions — both sections must exist (empty-but-present beats absent)
reservation.order [linter.coverage] constraint `all_or_nothing` has no deriving property — ∃ property.derives_from == `reservation.order.all_or_nothing` is required — write `[[reservation.order.all_or_nothing]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `cancel_refunds_exactly_charged` has no deriving property — ∃ property.derives_from == `reservation.order.cancel_refunds_exactly_charged` is required — write `[[reservation.order.cancel_refunds_exactly_charged]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `charge_iff_fulfilled` has no deriving property — ∃ property.derives_from == `reservation.order.charge_iff_fulfilled` is required — write `[[reservation.order.charge_iff_fulfilled]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `expiry_releases_holds` has no deriving property — ∃ property.derives_from == `reservation.order.expiry_releases_holds` is required — write `[[reservation.order.expiry_releases_holds]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `held_or_converted` has no deriving property — ∃ property.derives_from == `reservation.order.held_or_converted` is required — write `[[reservation.order.held_or_converted]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `release_is_total` has no deriving property — ∃ property.derives_from == `reservation.order.release_is_total` is required — write `[[reservation.order.release_is_total]]` in the property's derives_from cell
→ Run: fix the reported invariants — each rule's semantics: spk explain lint-rules
Layer 3 — Model
The Model section is a finite state machine: a ### States list and a
### Transitions table (id | from | to | guard). The first draft most
people write is wrong in instructive ways. This draft has a missing
guard, a misspelled state name, a state declared but never used — and it
draws the happy path while leaving the failure path mute:
### States
- `submitted`
- `reserved`
- `charged`
- `fulfilled`
- `cancelled`
- `released`
- `expired`
- `reserve_failed`
### Transitions
| id | from | to | guard |
|---------------|-----------|----------------|-----------------------------------------------------------------------------------------------------|
| accept | submitted | reserved | [[reservation.order.all_or_nothing]] |
| reject_order | submitted | reserve_failed | stock unavailable at acceptance time |
| charge | reserved | charged | |
| fulfill | charged | shipped | payment settled |
| cancel_paid | charged | cancelled | `customer cancelled ∧ ¬payment settled ∧ [[reservation.order.cancel_refunds_exactly_charged]]` |
| cancel_unpaid | reserved | cancelled | `customer cancelled ∧ [[reservation.order.held_or_converted]]` |
| expire | reserved | released | `reservation TTL elapsed ∧ [[reservation.order.expiry_releases_holds]]` |
spk lint reservation-order.md
lint: 1 file(s) linted, 13 finding(s), 0 advisory warning(s)
reservation-order.md [linter.guard_required] transition `charge` has an empty guard — every guard must be non-null
reservation-order.md [linter.every_state_used] state `fulfilled` is declared but no transition enters or leaves it — every state must appear as from or to in at least one transition
reservation-order.md [linter.every_state_used] state `expired` is declared but no transition enters or leaves it — every state must appear as from or to in at least one transition
reservation-order.md [linter.every_transition_valid] transition `fulfill` has to `shipped` — not a declared state (states: ["cancelled", "charged", "expired", "fulfilled", "released", "reserve_failed", "reserved", "submitted"])
reservation-order.md [linter.terminal_states_emit] failure terminal `reserve_failed` emits nothing — a mute failure terminal is a finding; add an emits edge to a file-owned effect Constraint: `- reserve_failed (emits: [[reservation.order.<label>]])`
reservation-order.md [linter.guard_negation_total] failure transition `reject_order` cites nothing and is not on the carve-out list (orchestrate's stage-fails) — cite the union of its success siblings' citation sets, or record a carve-out in linter-failure_shape.md
reservation.order [linter.coverage] constraint `all_or_nothing` has no deriving property — ∃ property.derives_from == `reservation.order.all_or_nothing` is required — write `[[reservation.order.all_or_nothing]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `cancel_refunds_exactly_charged` has no deriving property — ∃ property.derives_from == `reservation.order.cancel_refunds_exactly_charged` is required — write `[[reservation.order.cancel_refunds_exactly_charged]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `charge_iff_fulfilled` has no deriving property — ∃ property.derives_from == `reservation.order.charge_iff_fulfilled` is required — write `[[reservation.order.charge_iff_fulfilled]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `expiry_releases_holds` has no deriving property — ∃ property.derives_from == `reservation.order.expiry_releases_holds` is required — write `[[reservation.order.expiry_releases_holds]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `held_or_converted` has no deriving property — ∃ property.derives_from == `reservation.order.held_or_converted` is required — write `[[reservation.order.held_or_converted]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `release_is_total` has no deriving property — ∃ property.derives_from == `reservation.order.release_is_total` is required — write `[[reservation.order.release_is_total]]` in the property's derives_from cell
reservation.order [linter.single_root_reachable] 2 row(s) unreachable from any intent row — an orphaned island (traces_to/derives_from/guard/from-to/emits): reservation.order.expired, reservation.order.fulfilled
→ Run: fix the reported invariants — each rule's semantics: spk explain lint-rules
Thirteen findings, four distinct lessons:
Shape mistakes. charge has an empty guard
(linter.guard_required — a transition without a guard is not a
decision, it is an accident). fulfill points at shipped, which is
not declared (linter.every_transition_valid). And expired was
declared but never wired into any transition
(linter.every_state_used).
A typo cascades. Notice that fulfilled — a state the author fully
intended to reach — is also reported unused. That is the shipped
typo’s doing: with fulfill pointing elsewhere, nothing enters
fulfilled, so the state became an unreachable island, dragging itself
and expired into one linter.single_root_reachable finding. Fixing
the typo clears both.
The failure path is not optional. The two most interesting findings name the failure path:
linter.terminal_states_emit:reserve_failedis a terminal state with inbound transitions and nothing to say — a mute failure terminal. In this format a failure may not be silent: it must emit exactly one labeled effect Constraint owned by this file.linter.guard_negation_total:reject_order’s guard cites nothing. Its success sibling (accept) cites[[reservation.order.all_or_nothing]], so the failure transition must cite exactly the negated disjunction of that citation set. The rule makes “rejection happens for the same reason acceptance does not” a machine-checked shape, not a hope.
Fixing all of this touches four places: delete the redundant expired
state (TTL expiry releases the hold — released is that state), give
charge a guard, point fulfill at fulfilled, and give the failure
terminal something to emit. Emitting needs a new effect row — the
fourth constraint kind the file meets for the first time:
| stock_exhausted | effect | `reservation.order.stock_exhausted(detail)` | [[reservation.order]] |
and the states list gains the emit edge:
- `reserve_failed` (emits: `[[reservation.order.stock_exhausted]]`)
while the transitions become:
| id | from | to | guard |
|---------------|-----------|----------------|-----------------------------------------------------------------------------------------------------|
| accept | submitted | reserved | [[reservation.order.all_or_nothing]] |
| reject_order | submitted | reserve_failed | `¬[[reservation.order.all_or_nothing]]` |
| charge | reserved | charged | `payment authorized ∧ [[reservation.order.charge_iff_fulfilled]]` |
| fulfill | charged | fulfilled | `payment settled ∧ [[reservation.order.charge_iff_fulfilled]]` |
| cancel_paid | charged | cancelled | `customer cancelled ∧ ¬payment settled ∧ [[reservation.order.cancel_refunds_exactly_charged]]` |
| cancel_unpaid | reserved | cancelled | `customer cancelled ∧ [[reservation.order.held_or_converted]]` |
| expire | reserved | released | `reservation TTL elapsed ∧ [[reservation.order.expiry_releases_holds]]` |
spk lint reservation-order.md
lint: 1 file(s) linted, 7 finding(s), 1 advisory warning(s)
reservation.order [linter.coverage] constraint `all_or_nothing` has no deriving property — ∃ property.derives_from == `reservation.order.all_or_nothing` is required — write `[[reservation.order.all_or_nothing]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `cancel_refunds_exactly_charged` has no deriving property — ∃ property.derives_from == `reservation.order.cancel_refunds_exactly_charged` is required — write `[[reservation.order.cancel_refunds_exactly_charged]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `charge_iff_fulfilled` has no deriving property — ∃ property.derives_from == `reservation.order.charge_iff_fulfilled` is required — write `[[reservation.order.charge_iff_fulfilled]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `expiry_releases_holds` has no deriving property — ∃ property.derives_from == `reservation.order.expiry_releases_holds` is required — write `[[reservation.order.expiry_releases_holds]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `held_or_converted` has no deriving property — ∃ property.derives_from == `reservation.order.held_or_converted` is required — write `[[reservation.order.held_or_converted]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `release_is_total` has no deriving property — ∃ property.derives_from == `reservation.order.release_is_total` is required — write `[[reservation.order.release_is_total]]` in the property's derives_from cell
reservation.order [linter.coverage] constraint `stock_exhausted` has no deriving property — ∃ property.derives_from == `reservation.order.stock_exhausted` is required — write `[[reservation.order.stock_exhausted]]` in the property's derives_from cell
advisory reservation.order [linter.observability] effect `reservation.order.stock_exhausted` has no `observes` reference targeting it — a declared output nobody observes (advisory, exit 0) [hint: add an `observes` column on the row that consumes this output, or remove the effect if it is unintentional]
→ Run: fix the reported invariants — each rule's semantics: spk explain lint-rules
The model shape is clean now; what remains is coverage (next layer) — plus one advisory warning: the new effect is a firehose nobody attached a hose to. The warning asks, in the linter’s own words, “who is this output for?” It never gates (exit 0) — but a rejection reason that is part of the contract deserves an observer, so the finished spec adds one. An observer is a second column on the row that consumes the output:
| reject_reason_surfaced | invariant | `every rejection reaches the caller with the exhausted line items named` | [[reservation.order]] | [[reservation.order.stock_exhausted]] |
Layer 4 — Properties
Every constraint — including the effect row — needs a deriving
property. The Properties table is the evidence plan: unit rows pair a
generator with a predicate; law rows would additionally require
**identity:** and **associativity:** case labels (linter.law_cases)
— none of this file’s claims are algebraic, so it needs none.
## Properties
| id | kind | derives_from | generator | predicate |
|---------------------------|------|--------------------------------------------------|---------------------------------------------------|--------------------------------------------------------------------|
| no_charge_on_rejection | unit | [[reservation.order.all_or_nothing]] | `orders_with_mixed_availability()` | `nothing is charged for any rejected order` |
| holds_all_converted | unit | [[reservation.order.held_or_converted]] | `arbitrary_closed_orders()` | `every held unit is converted or released when the order closes` |
| double_release_noop | unit | [[reservation.order.release_is_total]] | `order_released_twice()` | `the second release changes nothing` |
| fulfilled_implies_charged | unit | [[reservation.order.charge_iff_fulfilled]] | `arbitrary_accepted_orders()` | `charged(order) == (outcome(order) == fulfilled)` |
| refund_bounded | unit | [[reservation.order.cancel_refunds_exactly_charged]] | `charged_orders_cancelled_before_settlement()` | `refund(order) == amount_charged(order)` |
| expiry_free | unit | [[reservation.order.expiry_releases_holds]] | `orders_past_ttl()` | `nothing is charged and no unit remains held after expiry` |
| rejection_labels_emitted | unit | [[reservation.order.reject_reason_surfaced]] | `orders_with_exhausted_items()` | `the rejection message names exactly the exhausted line items` |
| stock_exhausted_emitted | unit | [[reservation.order.stock_exhausted]] | `rejections_over_sold_out_items()` | `the rejection carries the stock_exhausted label with the item detail` |
With the observer row and the property table in place:
spk lint reservation-order.md
lint: 1 file(s) linted, 0 finding(s), 0 advisory warning(s)
→ Run: specodelic graph
Zero findings, zero warnings. The spec is structurally complete. Now walk the rest of the pipeline (the exact commands and their shape are in Installation & Quick Start); the essentials:
spk graph reservation-order.md
graph: 1 file(s), 31 node(s), 39 edge(s), 0 dangling, 0 typing violation(s), 0 supersedes cycle(s), 0 external boundary(ies)
→ Run: specodelic lint
spk compile reservation-order.md --out-dir specodelic/
compile: 1 compiled, 0 failed
reservation-order.md [reservation.order] → specodelic/reservation-order.toml, specodelic/reservation-order_props.rs, specodelic/reservation-order.tla
→ Run: specodelic verify (consumes the *_props.rs artifacts)
Compile emits three artifacts byte-stably: the constraints as TOML, the properties as proptest scaffolding, the model as a TLA+ module.
spk model-check reservation-order.md
model-check: 1 checked, 0 failed
reservation-order.md [reservation.order]: exploration_only (7 states explored) → specodelic/reservation-order.check.json
claims: 0 evaluated, 7 unchecked, 0 blocking
unchecked: all_or_nothing, cancel_refunds_exactly_charged, charge_iff_fulfilled, expiry_releases_holds, held_or_converted, reject_reason_surfaced, release_is_total
→ Run: specodelic verify (consumes the *.check.json reports)
Read that verdict carefully: exploration_only is not a clean bill of
health. All seven states were explored and no executable invariant was
violated — but none of the seven claims was evaluated, because
prose-predicate claims cannot be executed. The tool refuses to dress an
unexplored claim as a verified one.
A counterexample — the checker catches the author
That honesty has a payoff. Suppose the author, wanting a stronger verdict, hardens one claim into an executable invariant — and while at it, over-claims. Working on a scratch copy of the finished file, they add:
| reserve_never_fails | invariant | `**rust:** state != "reserve_failed"` — surely the reject path never fires once the flow is written down | [[reservation.order]] | |
plus its coverage property:
| reserve_never_fails_holds | unit | [[reservation.order.reserve_never_fails]] | `arbitrary_orders()` | `no order ever reaches reserve_failed` |
The **rust:** marker makes the invariant executable: the model-check
backend evaluates it as a Rust boolean over the program counter, the
machine’s current state name. Lint is happy — coverage holds, shape
holds:
cp reservation-order.md /tmp/reservation/
cd /tmp/reservation
spk lint reservation-order.md
lint: 1 file(s) linted, 0 finding(s), 0 advisory warning(s)
→ Run: specodelic graph
Lint cannot catch this mistake — it is structural, and this file is structurally impeccable. The claim itself is simply false, and the next stage proves it:
spk compile reservation-order.md --out-dir specodelic/
spk model-check reservation-order.md
compile: 1 compiled, 0 failed
reservation-order.md [reservation.order] → specodelic/reservation-order.toml, specodelic/reservation-order_props.rs, specodelic/reservation-order.tla
→ Run: specodelic verify (consumes the *_props.rs artifacts)
model-check: 1 checked, 0 failed
reservation-order.md [reservation.order]: counterexample_found (3 states explored) → specodelic/reservation-order.check.json
claims: 1 evaluated, 7 unchecked, 1 blocking
blocking: reserve_never_fails (rust, counterexample)
unchecked: all_or_nothing, cancel_refunds_exactly_charged, charge_iff_fulfilled, expiry_releases_holds, held_or_converted, reject_reason_surfaced, release_is_total
model-check findings: the claim aggregate is not clean (see .data.checked)
→ Run: fix the refuted claim(s) named in .data.checked — verify's model gate accepts only a no_counterexample aggregate
counterexample_found — and the report names the violated invariant and
the shortest trace that refutes it (from the emitted
reservation-order.check.json):
{
"outcome": "counterexample_found",
"violated_invariant_id": "reserve_never_fails",
"trace": ["submitted", "reserve_failed"],
"states_explored": 3,
"invariants_checked": ["reserve_never_fails"]
}
The model checker explored the reachable states — states_explored: 3,
the two successors of submitted and the root — and reported the
two-step trace ending in the very state the author swore was
unreachable. Rejection is reachable by design —
all_or_nothing can fail; that is what the failure terminal is for. The
executable invariant claimed otherwise, so the invariant was the bug.
The fix is to delete the over-claiming row and its property (the true
claim — why rejection happens — is already carried by the transition’s
guard). Re-running spk model-check on the restored file returns
exactly the exploration_only verdict shown above. The checker was the
design critic; the counterexample was the design lesson.
This is also where the format’s honesty boundary shows: the program
counter model carries no data binding — an executable invariant sees
only the machine’s current state name, never the order’s contents or its
history. Claims like “no charge without fulfillment” live in the
Constraints table as prose invariants and their Properties plans; they
get executed only when someone writes a real **rust:** fragment in a
backend that can evaluate them.
What verify refuses to pretend
The last stage executes the property scaffolding:
spk verify reservation-order.md
verify: 0 verified, 1 blocked
reservation-order.md [reservation.order]: properties_failed — 8 of 8 compiled blocks failed — a partial pass is a failure of the property
unchecked: all_or_nothing, cancel_refunds_exactly_charged, charge_iff_fulfilled, expiry_releases_holds, held_or_converted, reject_reason_surfaced, release_is_total
verify blocked — the blocking gate, message and hint ride .data.blocked
→ Run: fix the failing predicates (see each block's detail for the shrunk failing input)
Eight of eight blocks fail — because every predicate in this file is
still prose. Compile emitted them as todo_predicate! placeholders that
compile fine and fail the moment verify executes them. That is the
format refusing to pretend: a claim that was never executed is never
reported as passing, and a partial pass is a failure. The next rung on
the progressive-formalization ladder is opting individual predicates
into **rust:** fragments — one property at a time, only where a real
assertion can be written.
Unresolved behaviors
A finished spec is not a finished design. This file deliberately leaves open:
- Payment semantics.
payment authorizedandpayment settledare prose guards; the compiled TLA+ module carries them as comments and stutters at terminals rather than inventing semantics. The spec does not model the gateway. - The value of the TTL.
expirefires “after its TTL” — no duration is decided, and the schema has no column that could carry one. - Refund timing.
cancel_refunds_exactly_chargedbounds the amount; when the refund lands is not decided. - Concurrent orders. The model is one order’s program counter. Two orders racing for the last unit is a different capability — its own file, its own intent — not a row bolted onto this one.
- Partial fulfillment.
all_or_nothingwas a design decision, not an accident of scope: no partial accept is modeled, deliberately. - Unexecuted predicates. All eight predicates are prose. The claims
are as strong as their evidence plan, and the evidence has not been
written yet — see
spk verifyabove.
Each of these would land in a named place if decided: a constraint row, a property row, a transition guard, or a new file — the format refuses to let them remain ambient.
The finished file
The result of every fix above is
examples/reservation-order.md — the standalone
spec exactly as it lints at 0 findings, 0 warnings. For a second worked
example that goes deeper on one mechanism (how the accumulator’s algebra
licenses parallel architectures), see
batch-resume — derived parallelism, worked end to end.