Poly · v1.4 · examples

Ten programs, ten seeded defects

This article presents ten poly examples that show how your code is checked, what the check finds and what it proves. Each one is a working program checked CLEAN. Beside it is a copy with one defect put in on purpose. The check names that defect with a code and the shortest path that leads to it.

The ten programs, their policies, the defect copies and the tests are in examples/ in the repository, each with a README in docs/examples/.

Goal-oriented systems and proof

Tests cover the paths someone thought of. Agents are goal-oriented systems: they choose their own steps toward a goal, along the paths nobody thought of. Today's large engineering teams answer with more sampling. Stripe's coding agents rely on over three million tests, Amplitude routes each agent pull request by its assessed risk, and Spotify's background agent verifies its changes with builds and lint checks. Each of these checks the paths that ran. A proof covers every path that can run, including the ones no test reached.

The same gap appears at four levels, and at each level the number of paths grows. Trust is often called scarce when agents do the work. What is scarce is trust you can prove.

1

Agent-written code

The check runs before the code does. A defect comes back as a code and a path, which the agent can use to fix it.

Every example; the defect copies show the findings

2

The agent itself

An agent is a program with state. Its model calls, tools, budgets and outside calls are steps the check explores.

pr-reviewer, claim-intake, support-drafts

3

A crew of agents

Agents send each other messages, in any order. The check covers every order, and the properties of the crew as a whole.

research-agents, order-fulfilment

4

The digital twin

A model of the device or business the agents act on. Their actions are checked against what it allows.

flash-controller, a small model of a device

Sources: Stripe, Minions part 2 · Cursor, Amplitude · Spotify Engineering

The parts of each example

Each example has the same parts. The program and its policy are checked together. A copy of the program has one defect, and the check must report exactly the finding named on its first line. A TypeScript host runs the program on recorded traffic: no API key is needed and nothing goes over the network. The run leaves a journal, and replaying the journal gives the same results.

name.poly the program poly check tries every state the program can reach types.d.ts next.cjs lower to JS name.policy.poly signed policy poly check name-defect.poly one defect put in CLEAN every property holds FINDING code + shortest path host code (TypeScript) end-to-end test through the gate recorded traffic provider, services, device journal replays 0 divergences

Each .poly file starts with the verdict it expects, for example // expected: FINDING PL3002. The test suite and the release gate compare the check's verdict with that line.

The ten at a glance

The examples go from the simplest (no outside call, no model) to the richest (three agents checked together). The finding is what the check reports on the defect copy. The path is the sequence of steps that leads to the defect.

ExampleWhat it showsKey propertySeeded defectFindingPath
flash-controllerErase, program, verify, a retry limitNever programmed unless erasedErase check removedPL30022 steps
appointment-bookerModel reads a date, calendar call onceNo slot booked twiceOverlap check removedPL30026 steps
subscription-cancelOne offer, cancel onceA request always endsDecline offers againPL3301 · PL4001loop of 6 · 3
incident-oncallTimers, page once, person when unsureA page is answeredUnbounded snoozePL3301loop of 9
expense-approvalApprover check, person above 200 eurosApprover is not submitterSubmitter can approvePL30013 steps
claim-intakeTwo models, person below 0.8Queued alone only at 0.8+Confidence misused · never checkedREFUSED PL1205 · PL3002 PL6201none · 2
order-fulfilmentUndo steps, two ordersCancelled holds nothing; never oversoldUndo steps skippedPL30014 steps
pr-reviewerAgent tools, budgetNever mergesMerge made reachablePL40015 steps
support-draftsPrivate notes, owner approvesNotes never reach external modelNotes sent to itPL6503 · PL70011 step each
research-agentsPlanner and two workers, shared budgetEvery report has citedA worker that guessesPL54016 steps

Features by example

Read down a column to find the examples that use a feature. A one-shot call is an outside call that cannot be undone (a payment, a booking, a page) and is sent only once. A policy rule denies a step or sends it to a person.

ExampleModel decisionOne-shot callTimersAlways endsPolicy rulesnever ruleBudgetTwo modelsSeveral machinesData audiences
flash-controller··········
appointment-booker●●··●····●
subscription-cancel·●●●●·····
incident-oncall●●●●●····●
expense-approval●●··●····●
claim-intake●···●·●●·●
order-fulfilment·●··●···●·
pr-reviewer·●··●●●···
support-drafts●●··●●···●
research-agents●···●·●·●●

The unknown outcome

A call that cannot be undone, such as a payment, may time out and still have happened. Sending it again could pay, book or page twice. The check refuses such a retry (PL4003) unless the call is sent once, carries an idempotency key, or a person signs off on the risk. Seven of the examples send the call once. When no answer comes back, the program moves to an unknown state. Only a person can move it on, with Resolve or Confirm. The program never sends the call again.

Sending call sent once Done ok Failed definite refusal failed Unknown no answer to trust exhausted Resolve(found: true) Resolve(found: false)
unknown outcome a person's action final state

The state diagrams below use the same colors. Each arrow is a step the program allows, labeled with the actions that take it.

The examples

01

Flash controller

A plain state machine · examples/flash-controller/

One block of a flash memory chip. The block is erased, programmed, then verified, with a limit on retries. There is no outside call and no model: the host drives the chip and tells the program what happened.

Key propertyA block is never programmed unless it is erased.

Erased Programming Program Bad Erasing EraseDone Programmed Erase Verifying Verify ProgramDone, ProgramFailed Verified Erase VerifyFailed VerifyFailed VerifyPassed
Block
The program
step property erased-before-program:  action == Program implies pre.state == Erasedinvariant bad-after-limit: state == Bad implies retries == 2
The seeded defect
on Program(value) {  require state == Erased else reject "not-erased"  state' = Programming  data' = value}
  • PL3002erased-before-program broken: a path of 2 steps

The smallest example. A block that still fails after two retries is marked Bad, and every action on it is refused.

02

Appointment booker

A model answer and a calendar booking · examples/appointment-booker/

A model reads a patient's message and returns a day and an hour, each with a confidence. The program books that slot with one calendar call, which cannot be undone. It refuses a slot that is already taken.

Key propertyNo slot is booked twice.

Idle Parsing Ask Booked Reset Booking Written Failed WriteFailed Uncertain WriteUncertain Conflict Reset Reset Parsed Parsed Unclear Parsed, Unparsed Resolve Resolve Reset
Calendar
The program
on Parsed(day, hour, dconf, hconf) {  require phase == Parsing else reject "not-parsing"  if dconf >= 0.8 && hconf >= 0.8 {    if taken[day * 2 + hour] {      phase' = Conflict    } else {      phase' = Booking      taken' = taken with [day * 2 + hour] = true      emit write(slot: day * 2 + hour)    }  } else { phase' = Unclear }}step property free-slot-only:  (action == Parsed && post.phase == Booking) implies !pre.taken[post.slot]
The seeded defect
if dconf >= 0.8 && hconf >= 0.8 {  if taken[day * 2 + hour] { phase' = Conflict } else {    phase' = Booking    slot' = day * 2 + hour    taken' = taken with [day * 2 + hour] = true    emit write(slot: day * 2 + hour)  }}
  • PL3002free-slot-only broken: a path of 6 steps

Both confidences must reach 0.8: a known day with a guessed hour is not a booking. If the calendar does not answer, the slot stays taken, since the event may exist. Only a person can free it.

03

Subscription cancel

A request that always ends · examples/subscription-cancel/

A subscriber asks to cancel. The service may make one offer to keep them. The subscriber keeps the subscription, declines, or says nothing. After a week the offer lapses and the cancellation goes ahead. The cancellation is a billing call, sent once.

Key propertyA request to cancel always ends, and the offer is made at most once.

Active Cancelling Request Offering Request CancelUnknown Cancelled Resolve Stuck Resolve BillUncertain Billed BillFailed OfferUnknown Resolve Offered Resolve Decline, Lapse Retained Keep OfferFailed OfferUncertain OfferSent
Subscription
The program
path property offer-at-most-once: count("retention.offer") <= 1liveness request-ends:  status == Offering || status == Offered  leads to status == Cancelled || status == Retained || status == Stuck  under fair { OfferSent, OfferFailed, Lapse, Billed, BillFailed, Resolve }
The seeded defect
on Decline {  require status == Offered else reject "not-offered"  status' = Cancelling  status' = Offering  cancel lapse  emit cancel()  emit offer()}
  • PL3301the request never ends: a loop of 6 steps
  • PL4001the offer is sent twice: 3 steps

One change gives two findings. The decline goes back to an offer, so the request never ends, and the offer is sent a second time.

04

Incident on-call

Timers, and a person when the model is unsure · examples/incident-oncall/

A model rates the severity of a report. If it is sure the severity is High, the program pages the on-call engineer once and starts a five-minute timer. With no acknowledgement, it escalates once. The engineer may snooze twice at most. If the model is unsure, a person rates the report.

Key propertyA page is acknowledged or escalated, assuming the timers fire.

Reported Triaging Report Acked Resolved Resolve Escalated Resolve Escalating Escalated EscalationUnconfirmed EscalateUncertain Unreached EscalateFailed Confirm Confirm Logged Paged Ack Timeout Snoozed Snooze Paging Ack PageFailed, Timeout PageSent Unconfirmed PageUncertain Review Classify Classify Wake Triaged Triaged Triaged, Untriaged Ack Timeout
Incident
The program
on Snooze {  require status == Paged && snoozes < 2 else reject "cannot-snooze"  status' = Snoozed  snoozes' = snoozes + 1  cancel ack  after 15m -> Wake() as wake}liveness page-is-answered:  status == Paging leads to status == Acked || status == Escalated || status == Unreached  under fair { Timeout, Wake, PageSent, PageFailed, Escalated, EscalateFailed, Confirm }
The seeded defect
on Snooze {  require status == Paged && snoozes < 2 else reject "cannot-snooze"  require status == Paged else reject "cannot-snooze"  status' = Snoozed  snoozes' = snoozes + 1  cancel ack  after 15m -> Wake() as wake}
  • PL3301the page is never answered: a loop of 9 steps

The path is a loop: Paged, Snooze, Snoozed, Wake, Paged, again and again. With no limit on snoozes, the escalation never happens, even though every timer fires.

05

Expense approval

Who may approve, and a person above a threshold · examples/expense-approval/

An employee submits a receipt. A model picks its category. An approver approves or declines it. The payment cannot be undone and is sent once. Above 200 euros, the policy holds it until a person signs off. The approver's identity comes from the host's login, never from the request data.

Key propertyThe approver of a claim is never its submitter.

Draft Categorizing Submit Submitted Categorized, Uncategorized Declined Failed Held Resolve Settled Resolve Paying PayFailed PayHeld Paid Decline Approve
Expense
The program
on Approve by a: Approver {  require status == Submitted else reject "not-submitted"  require a.principal != submitter else reject "own-claim"  approver' = a.principal  status' = Paying  emit pay(amount: cost)}invariant approver-not-submitter: approver != none implies approver != submitter
The seeded defect
on Approve by a: Approver {  require status == Submitted else reject "not-submitted"  require a.principal != submitter else reject "own-claim"  approver' = a.principal
  • PL3001the submitter approves: a path of 3 steps

Amounts are stored in steps of 50 euros (0 to 10). With a wider range, the check takes too long to finish. This limit is listed in the language findings.

06

Claim intake

Two models and a confidence threshold · examples/claim-intake/

A model sorts an insurance claim into a category. A small model answers first. A larger one takes over when the small one is unsure. A claim goes to its queue on its own only when the confidence is 0.8 or more. Below that, a person decides. A fraud flag makes the assignment wait for a person's approval.

Key propertyA claim is assigned by itself only on a calibrated confidence of at least 0.8.

Received Classifying File Assigned Assigning Assigned Failed AssignFailed Classified Review Classified, Unclassified Decide
Claim
The program
route classify {  tier small provider "small" calibration "maps/classify-small.json" max tokens 20  tier large provider "large" calibration "maps/classify-large.json" max tokens 20  escalate when conf < 0.8 or failed  budget monthly}step property confident-or-person:  (action == Classified && post.status == Assigning) implies conf >= 0.8
The seeded defect
// claim-intake-defect.poly: a confidence added to a marginif conf >= 0.8 {if conf + margin >= 1 { // claim-intake-ignored.poly: the branch tests the categoryif conf >= 0.8 {if category == Auto {
  • REFUSED PL1205a confidence may only be compared with a fixed number
  • PL3002 + PL6201the confidence is never checked: 2 steps each

This example has two defect copies. The first is refused before the check even runs: a confidence can only be compared with a fixed number. The second never looks at the confidence at all.

07

Order fulfilment

Undo steps when a later step fails · examples/order-fulfilment/

Reserve the stock, charge the card, ship. If a later step fails, the earlier ones are undone: the stock is released and the charge is refunded. Shipping and refunds are sent once, and an unanswered one goes to a person. The check also runs two orders against one item in stock.

Key propertyA cancelled order holds no stock and no charge, and the stock is never oversold.

Placed Reserving Place Cancelled Charging ChargeFailed Shipping Charged Done RefundUnknown Resolve Stuck Resolve Refunding Refunded RefundUncertain RefundFailed Rejected Reserved OutOfStock ShipUnknown Resolve Resolve Shipped ShipFailed ShipUncertain
Order
The program
invariant cancelled-holds-nothing: st == Cancelled implies held == 0 && !charged configuration two-orders {  stock    : Stock  order[2] : Order { stock: stock }  bounds   { transit 4 }  invariant never-oversold: sum(order[*].held) <= init(stock.onHand)}
The seeded defect
on ShipFailed {  require st == Shipping else reject "not-shipping"  st' = Refunding  held' = 0  send stock <- Release()  emit refund()  st' = Cancelled}
  • PL3001a cancelled order keeps its charge: 4 steps
  • CLEANthe two-order check still passes

Without the undo steps, a cancelled order stays charged and keeps its stock. The two-order check still passes: it tests that the stock is never oversold, which is a different property.

08

PR reviewer

An agent's tools under a policy · examples/pr-reviewer/

An agent reviews a pull request with its tools: read the diff, review it, comment, add a label. Each use of a tool is a step of the program. The agent also has a merge tool. The program never asks for it, and the policy denies it. A budget caps the cost of each review and of each month.

Key propertyThe program never merges, and the policy proves no merge is ever allowed.

Idle Reading Start Commenting Failed CommentFailed Labelling Commented Done Labelled LabelFailed Merging Merged MergeFailed FetchFailed Reviewing Fetched Reviewed ReviewFailed
Review
The program
workflow review runs Review {  tool "github.diff"    -> gh_diff    why "read the pull request's diff"  tool "agent.review"   -> reviewer   why "review the diff"  tool "github.comment" -> gh_comment why "post the review as a comment"  tool "github.merge"   -> gh_merge   why "merge the pull request"  budget review_spend prices "prices/agent.json" order cap usd 0.05}path property never-merges: count("github.merge") <= 0// policy: never merge-is-never-allowed: allow when effect.kind == "github.merge"
The seeded defect
phase' = Doneif outcome == Good {  phase' = Merging  emit merge(pr: number)} else {  phase' = Done}
  • PL4001a merge can be reached: 5 steps

Merging is drawn dashed: no step reaches it in the correct program. In the defect copy it can be reached, and the gate still denies the merge when the program runs.

09

Support drafts

Private notes kept from an outside model · examples/support-drafts/

For one customer, the program reads the customer's messages and the owner's private notes. An outside model summarizes the messages. An in-house model writes a draft and briefs the owner from the notes. Every draft waits for the owner's approval.

Key propertyThe owner's notes never reach the external model.

Opening Reading Open Briefing Ready Briefed, Unbriefed Created Drafting Failed Unwritten Noting Summed, Unsummed, Written Noted NotesFailed Proposed Made NotMade Uncertain MakeUncertain Read ReadFailed Propose Resolve Resolve
Ticket
The program
audience { public < customer < owner }source notes: records { adapter "postgres" audience owner owned freshness 720h } // the policyobligation external-model-audience "The external model is given nothing above the customer audience." rule {  when proposal.kind == "effect" && effect.kind == "decision.summary"       && !(effect.taint.class in { "public", "customer" })  require false else deny}never notes-to-external-model "Notes never reach the external model.":  allow when effect.kind == "decision.summary" && effect.taint.class == "owner"
The seeded defect
on Noted(items) {  require status == Noting else reject "not-noting"  status' = Briefing  decide brief(notes: items)  decide summary(thread: items)   // the notes sent to the external model}
  • PL6503 + PL7001with the rule removed from the policy: 1 step each
  • gatewith the policy in place: CLEAN, and the gate refuses the call at run time

The check tracks private data into outside calls and messages. What a model call receives is checked by the gate when the program runs (finding 17). In the test, the gate refuses the summary built from the notes.

10

Research agents

Three agents checked together · examples/research-agents/

One planner and two workers. The planner picks a focus. Each worker reads documents and answers with citations. All model calls share one budget. The check runs the three agents together, in every order their messages can arrive.

Key propertyEvery worker that reports has cited its sources (699 combined states).

Idle Planning Ask Done Failed Unplanned Waiting Planned GaveUp, Report
Planner
Idle GaveUp Cancel Reading Task FoundFailed Researching Found Reported Unanswered Answered
Worker
The program
configuration inquiry {  planner   : Planner  worker[2] : Worker { planner: planner }  bounds    { transit 4 }  invariant answers-cite:    forall i in worker: worker[i].status is Reported implies worker[i].cited}
The seeded defect
action Guess(text: Content)on Guess(text) {  require status == Researching else reject "not-researching"  status' = Reported  answer' = text  send planner <- Report()}  inputs { worker[*].Guess, planner.Wire, … }
  • PL5401a worker reports without citing: 6 steps

No single agent has this property: it belongs to the crew. A worker that can guess reports without citing, and the path mixes the steps of all three agents.

Limits of the check

The check finds defects in the program: a missing test, a wrong comparison, a loop. It cannot see what happens outside the program. The recorded tests cover that part, and each README says where the check stops.

Model accuracy

The check accepts any answer of the right type from a model. The program decides what to do with the answer and its confidence.

Service behavior

An idempotency key only works if the service honors it. The check prints this assumption (K17) and does not prove it. An unknown outcome goes to a person.

Approver identity

The program can require that the approver is not the submitter. Who the approver is comes from the host, which verifies each user.

Data sent to a model

The check tracks private data into outside calls and messages. What a model call receives is checked by the gate when the program runs (finding 17). Proposal P10 would check it earlier.

The language problems found while writing the ten, with workarounds and proposed fixes, are in docs/examples/language-findings.md. The proposals are in docs/proposals/v2.0.md. One problem was a defect of the toolchain and is fixed in v1.4: a policy scope that matches nothing is now reported (PL6504).

Conclusion

The ten examples make one point from ten angles. A property takes a few lines to write. The check tests it on every state the program can reach. A one-line defect is found with the shortest path that shows it: two steps for a missing erase check, a loop of nine steps for a snooze that blocks the escalation, six steps across three agents for a worker that guesses. Calls that cannot be undone are sent once, and an unknown outcome goes to a person, so no example can pay, book or page twice. What lies outside the program (a model's accuracy, a service's promise, a person's identity) is covered by the recorded tests and by the gate. Tests cover the paths that ran. The check covers the paths nobody thought of.

Glossary

CLEAN
The verdict when the check runs to the end and finds nothing.
Finding
A property the check shows to be broken, reported with a code (such as PL3002) and a path.
REFUSED
The verdict of a program rejected before the check runs, for example for a type error. It has no path.
Path (witness)
The shortest sequence of steps that breaks a property. When the defect is that something never ends, the path is a loop: a start, then a cycle that repeats.
Seeded defect
The one change a defect copy makes to its program, so the finding can be seen and tested.
Invariant
A property that must hold in every state.
Step property
A property of each single step, comparing the state before (pre.) and after (post.).
Path property
A property of a whole run, such as how many times a call is sent.
Liveness
A property that something eventually happens, assuming timers fire and services answer.
Configuration
Several machines checked together, with a limit on how many messages can be in flight between them.
Decision
A typed question put to a model. The answer comes back with a confidence, which the program must compare with a fixed number.
Effect
A call to the outside world, declared with whether it can be undone and how often it may be retried.
Policy
Signed rules the gate applies when the program runs: allow, deny, or send to a person. A never rule is also proved by the check.
Gate
The part of the runtime that applies the policy to every step before it happens.
Audience
A label for who may see a piece of data, such as customer or owner.
Recorded traffic
The answers of a model or service written down in advance, so a test runs the whole program offline.
Journal replay
Running the recorded journal again and comparing every state and every gate decision. Any difference is a failure.