Caches and proposals
The cache: a reviewable git artifact
Each scenario caches to one JSON file under .saffron/cache/, a per-step
list of actions with semantic targets: ARIA role + accessible name
first, then label / text / testId / placeholder, with CSS selectors only as
fallbacks. Caches are meant to be committed and reviewed in PRs, like
snapshots.
Deleting a scenario, or the whole feature file, leaves its recording
behind: nothing reads it, but it stays in git, and a reviewer cannot tell a
dead recording from a live one. saffron prune lists every cache and
pending proposal no scenario owns any more, and says why (the scenario is
gone from the file, or the file itself is gone). It deletes nothing until
you add --yes, and --check exits 1 without deleting, so CI can keep the
directory honest. Ownership is decided by path, never by content: a cache
that is stale or even unreadable is kept as long as a live scenario claims
it, because stale means re-record and unreadable means a bug, and neither
is a reason to throw a recording away. While any feature file fails to
parse, prune refuses to judge anything at all, since a syntax error hides
that file's scenarios.
Two mechanisms keep caches valid over time:
<param>placeholders: a Scenario Outline records once and replays every Examples row; literal example values are factored out automatically. A row value that the agent quoted inside a longer page sentence becomes the placeholder outright for assertions and text locators (they match by containment), so every row can satisfy it. Within one run only the first row records; later rows replay the pending proposal at zero AI, and a row the proposal does not fit turns red with an explanation instead of overwriting the recording.{date±N[:fmt]}templates, literal ISO dates near today are rewritten to relative templates at record time (Select 2026-07-04 …becomesSelect {date+1} …), so date-picker locators survive the calendar moving. Templates resolve insidenameRegexpatterns too.- Data tables: steps like "I enter the following guest information"
carry their Gherkin table into the recording. For 2-column key/value
tables, recorded values become
<table:key>references resolved from the scenario's own table at replay: change the table values and the cache replays unchanged at zero tokens; change the keys and the step is honestly stale and re-records. - Multi-row record tables: wider tables (header row + data rows, a
list of records) parameterize per cell: recorded values become
<table:1:firstName>,<table:2:email>, … with 1-based data-row indices. Editing any cell value replays free; adding/removing a row or renaming a header column changes the key set and is honestly stale. A 2-column table is always key/value, never header-mode. - Doc strings: a step's
"""block becomes a<docstring>reference in the recording, so editing the content replays at zero tokens. A recording that baked the content in literally (e.g. hand-written caches) stays protected by exact-content staleness. - Secrets:
{env:VAR}tokens resolve from the environment at action execution and never persist: recordings mask referenced values back to tokens, and narratives, errors, reports and history get the same pass. Missing variables fail fast by name before the run; a git-ignored.envfills gaps (real env always wins). Honest limitation: during recording the agent types the real value once, replays never involve it. Record with rotatable staging credentials. - Cross-step references: unambiguous table/docstring params are
visible to every step of the scenario, exactly like Examples params.
So "Then the saved note should be shown" records as an assertion on
<docstring>even though that step carries no doc string itself, edit the note, and both the fill and the assertion follow. A key claimed by two steps with different values is ambiguous and stays step-local.
Every recorded action also carries a page fingerprint (SHA-256 of the
normalized accessibility snapshot, numbers masked) used for token-free drift
detection, surfaced as a drift chip in reports.
Proposals: the review gate
saffron diff is the evidence. A proposal's narrative is the agent's
account of what it did; the action list is what you are approving.
saffron diff [files...] prints the committed recording against the
proposed one, step by step: a rewritten action appears as one
before-and-after pair with the fields that moved named underneath, a
changed assertion is counted separately because that is the scenario's
verdict, and a first recording says plainly that there is nothing to
compare against. Actions are compared by their fields, never by how they
read, so a dropped response-body check or a selector repointed from
#save to #delete cannot pass as "no change"; steps are aligned in
order, so a scenario that does the same thing twice does not hide the loss
of one of them. The same diff appears in the HTML
report and in both IDE panels, so every surface shows one answer.
AI output never lands in a cache directly. Recordings and heals write
proposals to .saffron/proposals/, carrying the new cache plus metadata:
narrative, adaptations, token usage, and suggested .feature edits.
Verified proposals close the recording loop: before a proposal is
filed, Saffron replays the candidate cache zero-AI in a fresh browser
context. If the proof replay fails, one bounded refinement pass feeds
the failure (and the live browser state) back to the still-warm agent,
"the app works; your recording doesn't replay; fix the recording", then
re-verifies. Proposals arrive stamped verified: true, or honestly
verified: false with the replay error attached (and saffron accept
warns before promoting an unverified one). Heal candidates are never
refined around assertion failures; committed assertions stay the
contract. Disable with --no-verify / "verifyProposals": false (e.g.
for scenarios with non-idempotent side effects, since verification
re-executes the scenario).
saffron acceptpromotes a proposal to the committed cache;saffron rejectdiscards it (the agent retries next run).--with-feature-editalso applies the agent's step rewrites to the.featurefile itself: located via Gherkin AST line numbers, keeping indentation and keyword, skipping shared Background steps, and syncs the cache's step text so it doesn't go stale. Documented truth tracks executable truth.--propagateapplies an accepted heal's locator fixes to every other cache whose target shares the same semantic core (role/name/nameRegex/ label/text/testId: CSS fallbacks excluded). Identical core means an identical Playwright query, so the identity proof is exact, not heuristic: one heal repairs N scenarios before they ever fail. Fan-out is printed per scenario; nothing is silent.