Deep dive · the concept that makes TRIF different

Telling a bug from drift — and healing without hiding it.

Every browser test eventually fails because the app changed, not because it broke. Traditional tooling can't tell the difference — so it either cries wolf or silently adapts. TRIF makes drift a first-class, named outcome and repairs it through an explicit, reviewable loop that can never absorb a real regression.

01

The problem: one red X hides three different things

When a step fails, three completely different situations look identical to a normal test runner — so reds get ignored, "flaky" becomes an excuse, and real regressions slip through.

test failed
…but why?
FAIL-BUG — the app genuinely misbehaved. An engineer should look.
FAIL-STALE — the app changed; the test's assumption is out of date. Heal it.
BLOCKED — the run never happened (no engine, app down, auth wall). Fix infra.
Why this is the crux. Conflating "changed" with "broken" is the failure mode of AI and script testing alike. Cry wolf and people mute the suite; silently adapt and you delete the one signal that would have caught the bug. TRIF refuses to collapse them.
02

How drift is detected

Detection is layered — four independent mechanisms, each cheap, that together decide "the element the recipe expects is no longer where/what it was" before anything is called a bug.

a. The anchor resolution ladder

Every element reference (an anchor) resolves by trying progressively more robust strategies. Which rung succeeded is recorded per step as resolvedVia.

1
Engine hint
The stored fast selector, e.g. #firstHeading. Fastest; the one bindings compile to.
2
Semantic role + accessible name
If the hint misses, resolve by role=heading name="Article title" — survives most markup churn.
3
Agentic fallback
If that misses too, a model locates it from the anchor's note + a screenshot.
All rungs miss → FAIL-STALE / anchor-miss
The element as described cannot be found. That is drift, not a bug — and it emits a patch proposal.

b. Freshness fingerprint

A binding is trusted only while a fingerprint over (recipe source + resolved anchors) is unchanged. Edit the recipe, or let an anchor resolve differently, and the fingerprint breaks — the binding retires until the recipe re-earns it. stale reports this per surface, dated, before a run.

c. A binding failure is a signal, never a verdict

If a compiled script stops passing, TRIF does not report FAIL. It drops the binding as suspect and falls back to the intent lane, which re-derives the answer from the recipe. The deterministic fast-path can be wrong; the recipe is the source of truth.

d. The retry-once honesty gate

Before any FAIL, the executor retries once after the page settles — so a slow render never masquerades as drift or a bug.

03

The decision: which verdict, and why

Once a step can't be satisfied, the outcome is decided by a strict, auditable rule — not a guess.

step fails → retry once after settle → still failing? ├─ could the app even be driven?  no → BLOCKED (missing capability · app down · auth) ├─ did the anchor resolve on any rung?  no → FAIL-STALE (anchor-miss + patch proposal) ├─ anchor resolved, but the oracle value is wrong? │   ├─ content legitimately changed → FAIL-STALE (oracle-drift — needs human sign-off) │   └─ app is genuinely wrong → FAIL-BUG (quoted evidence + one re-read) └─ everything satisfied → PASS
The honesty rules are hard constraints: a FAIL-BUG requires quoted observed evidence plus one re-read; an anchor miss is FAIL-STALE, never an improvised alternate path; a missing capability is BLOCKED, never a fabricated FAIL. An executor is read-only — it can propose a fix but never apply one.
04

The healing loop

Drift doesn't fix itself in the dark. It becomes a machine-readable patch proposal that a human reviews and applies — three roles, one writer.

Executor

read-only

Runs recipes, grades oracles, records friction. Can never write to the cookbook — the discipline that catches mis-modeled tests instead of papering over them.

Healer

proposes

Turns friction into a typed patch proposal (a diff), with the evidence that motivates it. Proposes only — applies nothing.

Maintainer

the only writer

Reviews, applies the patch, bumps the version, writes the CHANGELOG. The recipe re-verifies and re-graduates to a binding.

Patch types

PatchWhenGuard
anchor-fixan element moved / its hint went stale — update the anchorauto-applyable
new-quirka newly-observed behavior worth recording in the mapauto-applyable
recipe-step-fixa step needs adjusting to match a changed flowreviewed
binding-stalea compiled script no longer matches — retire/recompile itauto-applyable
oracle-driftthe expected result itself changed (not the path to it)requires --human-signed

oracle-drift is gated hardest on purpose: changing what "correct" means is exactly where a silent auto-heal would erase a real regression. A human must sign it.

# drift surfaced on a run → inspect and apply, explicitly
$ test-harness-kit stale wikipedia.trif           # what's suspect, and since when
$ test-harness-kit heal  wikipedia.trif list       # pending patch proposals
$ test-harness-kit heal  wikipedia.trif apply patch-anchor.json
$ test-harness-kit verify wikipedia.trif --surface article   # re-drive, re-shoot, refresh ledger
05

Worked example: a Wikipedia skin change

A real case from the reference cookbook — the difference between a false alarm and a caught change.

context
A recipe checks the article table-of-contents via #vector-toc (the Vector-2022 skin). Wikipedia rolls the page back to the legacy skin, where the TOC is #toc.
detect
The binding's #vector-toc selector misses. Binding failure → signal, not verdict → drop to intent lane. The anchor's rung-1 hint misses; the healer notes the documented fallback pattern (#toc present) — a skin-version event, not a missing feature.
verdict
FAIL-STALE / anchor-missnot FAIL-BUG. Wikipedia isn't broken; the test's assumption about the skin is out of date.
heal
A anchor-fix proposal updates the TOC anchor's hint to prefer #vector-toc, fall back to #toc. A maintainer applies it; the recipe re-verifies and re-graduates.
outcome
Zero false bug reports filed. The change is captured in the cookbook's version history, with evidence — the next run is green on the fast lane again.
Contrast the alternatives: a brittle selector would have filed a false "TOC is broken" bug; a silent self-healing locator would have quietly re-bound to something else and told you nothing — so if the TOC were actually removed, you'd never know.
06

Why not just "self-healing locators"?

Auto-adapting selectors are common — and dangerous — because they optimize for green, not for truth.

Silent self-healTRIF stale + heal
on driftsilently re-binds to a best-guess elementnames it FAIL-STALE and proposes a reviewable patch
audit trailnone — the test just keeps "passing"a versioned diff + evidence in the CHANGELOG
regression riskhigh — a removed/changed feature is absorbedlow — a human signs off on any change to expected results
who writesthe runner, at run timeonly the maintainer, after review
The principle: speed up maintenance without ever hiding a change. The system does the tedious part — spotting drift and drafting the fix — and a human keeps the one decision that matters: is this a change we accept, or a bug we caught?