Skip to content

Write a frame ​

A frame is plain JSON: the whole experiment, declared before any call. Gadfly checks it and hashes the checked value, so a changed frame is a different frame, and records made under the old hash are refused. Start from a frame that works, and change one part at a time.


A complete small frame ​

This frame is the test suite's own: one policy rule, one request, every carriage trial, and a battery that needs no model. Here it is in words, in the order a run uses it, each span linked to its line in the file; The keys says what to write for each:

The claim

PartIn this frame
Relationa refund requires a receipt
Grainone rule of the policy
Promisea request without a receipt is declined, however the rule is worded

What the reader is shown, the anchor marked *:

SpanKindText
daysruleRefunds are granted within 30 days of purchase.
receipt *ruleA refund requires a receipt.
giftruleGift cards are never refunded.
methodruleRefunds go back to the original payment.
bindingnoteThese rules decide every request.

What the reader is asked

PartIn this frame
QuestionA customer without a receipt asks for a refund on day 3.
Answersgrant, decline or unclear
InstructionDecide the request under the policy.

The trials, each asked 1 times, 8 calls in all:

TrialWhat changesWhat it tests
baselinenothingthe answer the others are read against
repeatnothing, asked againwhether that answer is stable
ablationcuts receiptwhether the answer needs the anchor
sham-1cuts giftwhether any cut of the same kind moves it
sham-2cuts methodwhether any cut of the same kind moves it
presentationrenames receipt to proof of purchasewhether the wording, not the meaning, drives it
bypasscuts bindingwhether a hidden fallback still carries it
controlcuts days, receipt, gift, methodwhether the reader can move at all

How the answers are read

RuleIn this frame
Without the anchorgrant or unclear may come back
Through the bypassunclear must come back
A level1 of 1 answers alike
Carriageall-checks, so a category waits for the checks it needs
Materializedthe answer is the form the recipient receives
Batteryrule-reader

The setting, hashed with the rest:

PartIn this frame
Rolea support agent deciding a request
Locusthe refund policy, its receipt rule
Purposedecide a refund request under the policy
Trajectorypolicy and request, the agent's decision, the reply to the customer
Crossingthe decision in the reply
Baselinethe policy as written and one request; no rule changed

The frame as written ↗

Run it without a model ​

Its battery, RuleReader in tests/fixtures/refund.py, declines while the receipt rule is in the policy and grants once the rule is cut. These commands run the frame through it at no cost, then read the verdict:

text
$ uv run gadfly run tests/fixtures/refund.json \
    --battery tests.fixtures.refund:RuleReader \
    --calls 8 --out refund.jsonl
$ uv run gadfly report tests/fixtures/refund.json \
    refund.jsonl
frame: refund-receipt (dd916080072539784691a5232645b4d9c5b00e9cfdddd51650d15b347b385f85)
records: 8 of 8 planned calls
overall: carried
  in no-receipt, ablation moved it decline -> grant; held under presentation, 2 shams
  in no-receipt, bypass moved it decline -> unclear, so no hidden fallback
  in no-receipt, the control moved it decline -> grant
no-receipt: carried
  level baseline: decline
  level repeat: decline
  level ablation: grant
  level sham-1: decline
  level sham-2: decline
  level presentation: decline
  level bypass: unclear
  level control: grant
  evidence: ablation moved it decline -> grant; held under presentation, 2 shams
  evidence: bypass moved it decline -> unclear, so no hidden fallback
  evidence: the control moved it decline -> grant

A deterministic battery like this one tests the frame's mechanics, not a model. Swap in a live battery once the frame reads as you meant it.


The keys ​

Every top-level key is checked before a call, and an unknown key is refused. The tables group the keys by what they declare. The parser's error strings are its public contract, so fix a refused declaration rather than adding a key it ignores.

The situation ​

These keys say what is claimed, for whom and where. All are required, and all are hashed with the rest.

KeyRequiredShapeWhat you write
nameyesa stringa short label for the frame
relationyesprosethe premise under test
locusyesprosewhere the relation acts
role, grain, purposeyesprosewhose reading, at which unit, for what
trajectoryyesprosethe path to the recipient
crossing, promiseyesprosethe boundary, and what must cross it
baselineyesa string or an objecteverything pinned for the comparison

The evidence and the question ​

These keys hold what the reader reads and what it may answer.

KeyRequiredShapeWhat you write
body.spansyesa list of {id, text, kind} with unique ids; kind is text by defaultthe evidence, one span per unit a trial may cut; kind matches sham spans
anchorsyesa list of span ids, at least onethe spans the relation rests on
inputsyesa list of {id, text} with unique idsthe test domain, one case each
observe.labelsyestwo or more different stringsthe answers a reader may give
observe.instructionyesa stringwhat each label means; define every one
observe.ordernothe labels, in orderneeded for lower and higher in allowed
any other observe keynoyour battery's choiceread by your battery only

The trials and the reading ​

These keys choose the trials and how their answers are read.

KeyRequiredShapeWhat you write
interventionsyesan object with ablation: truethe trials, whose shapes follow below
allowed.ablationyesa list of labels, or lower or higher with orderwhat the ablation may produce
allowed.bypassnothe same, with a bypass trialthe opening a bypass must expose
allowed.inversionwith an inversionthe samewhat the inverted answer must reach, never the baseline's own label
allowed.inputInversionwith an input inversionthe samewhat the answer must reach when what is asked turns
standing.quorum, standing.agreeyeswhole numbers, 1 <= agree <= quorumcalls per trial, and how many alike make a level
standing.stablenotrue or false, with repeatthat baseline and repeat may not split
carriagenoall-checks (the default) or listedwhether an open check holds a category back

The receiving side and the battery ​

These keys declare what the trials cannot test, and who reads.

KeyRequiredShapeWhat you write
materializednotrue or false, false by defaulttrue only when the observation is the form the recipient receives
outsideScopenoa stringthe receiving owner's exclusion, in their words
scopeCheckno{owner, statement, source, encounter}, with outsideScopethe owner's own check of that exclusion
battery.kindyesa string equal to the battery's namewhich battery reads; the shipped ones read more keys, listed on Batteries

The interventions ​

Each trial is one key of interventions, removing spans by id. Interventions explains what each one tests and costs. Gadfly refuses a trial that would repeat another before it spends a call.

KeyShapeRefused when
repeattrue or falseit is not true or false
ablationtrueit is missing or not true
eachAnchortrue or falseit is not true or false, or the frame has one anchor
cumulativea list of span idsit adds no span to the anchors
shamtrue, or {"draws": n} with n >= 1 and an optional kind mapping anchors to the kind their shams are drawn fromn is not a whole number of at least 1, or kind names a span that is no anchor, a kind no span has, or the anchor's own kind
presentationwords mapped to their replacementsa word repeats in any case, or it renames nothing the reader reads
placement{"span": id, "after": id}the two spans are one, or the move leaves the order as it is
inversionphrases of the anchors mapped to their inversionsa phrase repeats in any case, is in no anchor, or is in a span that is not one; the map changes no anchor; or allowed.inversion is missing
inputInversionphrases of the inputs mapped to their inversionsa phrase repeats in any case, or is in no input; the map changes no input; or allowed.inputInversion is missing
bypassa list of span idsit removes exactly the anchors, which repeats the ablation
controla list of span idsit removes exactly the anchors

Choose a test domain ​

Inputs are cases, not repetitions of a trial, so list each independent case of one promise with a stable id. judge returns one finding per input, and overall combines them: any misaligned input makes the frame misaligned, any unresolved one keeps it unresolved, and carried and cooled combine only when all the inputs agree.

WARNING

outsideScope declares scope; it is not a way to silence a failed test. Record the owner's own check under scopeCheck: until then the finding is unresolved, or, under carriage: listed, outside scope with the check open.


Check before you run ​

Point your editor at midwife/frame.schema.json, its JSON Schema (draft 2020-12), for completion and errors as you type. Then let the parser, which has the last word, check what only code can:

bash
uv run gadfly check-frame my-frame.json

It runs the schema first when jsonschema is installed, and makes no call. Command line shows what it prints.


Troubleshooting ​

Each refusal names what to fix, and check-frame shows them before any call. For problems after a run, see Refused records.

MessageFix
input id 'pr: west' is not one plain namea name, span id, input id or label prints on a line of its own: no line break, and no colon followed by a space
unknown keys '...'every top-level key is checked; remove the typo rather than hoping it is ignored
presentation renames nothing input 'x' readsmap a word present in the body or every input
bypass removes exactly the anchorschoose the separate connection whose removal should expose the opening
cumulative adds no span to the anchorscumulative must remove at least one additional span
two inputs share an idinput ids are record keys and must be unique
allowed 'lower' needs observe.orderdeclare the full label order before using a direction
the frame does not say its baselinepin B0; do not leave the comparison implicit
this frame asks for battery 'x', not 'y', from runmake battery.kind and the battery class's name the same string
carriage is all-checks or listedthe policy is one of the two words, or left out for all-checks
scopeCheck checks an outsideScope exclusionrecord the owner's check only beside the exclusion it checks
placement leaves the order as it ismove the span after one it does not already follow