Skip to content

Drift & Stability

This page covers the diff half of stepbook's core loop. The Cache makes re-running one step after an edit cheap; diffing tells you what that edit actually changed. Together — edit → cheap re-run → diff — they're what stepbook is for.

A secondary axis, stability, sits further down: when a step is non-deterministic (most llm steps), one output tells you little, so stepbook can sample it repeatedly and measure the variance. Drift compares two runs; stability compares N repeats of one step. You reach for drift constantly; you reach for stability when characterizing a flaky step.

What "drift" means

Every run is snapshotted, so any two runs can be compared structurally instead of by eye. Drift is the set of structural differences between two runs of the same pipeline — computed, not estimated. It's made of two ingredients:

  • Field drift — leaf-level differences between two step outputs, walked recursively. Each difference is one of three kinds, reported at a dotted path (e.g. claims.5.entityType):
    • scalar — the value at that path changed (fromto).
    • added — the path exists in the newer run but not the older.
    • removed — the path existed in the older run but not the newer.
  • Assertion flips — a check with the same name whose verdict went pass → fail or fail → pass between the two runs.

A step is identical when it has zero flips and zero field changes and is present in both runs; anything else has drifted. Note what's not here: there's no similarity score or percentage. Drift is just these concrete, countable differences — so "it drifted" always resolves to exactly which fields changed and which assertions flipped.

Drift: comparing runs

Whole-run diff

stepbook diff compares two whole runs — every step's assertion flips and field changes, with a rollup summary:

sh
stepbook diff                          # previous vs latest (defaults)
stepbook diff --from latest-3 --to latest
diff · 9f3c1a2 → e4b7d05

summary: 1 flip · 1 step drift · 2+/0- entities

STEP     PRESENCE  FLIPS  FIELDS  STATUS
───────  ────────  ─────  ──────  ──────
parse    both          0       0  same
analyze  both          1       3  drift

Assertion flips
  [analyze] pass → fail  density within target
    density=verbose

The summary line rolls up the run: total assertion flips, how many steps drifted, and net entities added/removed (the added/removed field kinds). The table is one row per step — PRESENCE is both unless a step was added or removed between the two runs — and the Assertion flips list spells out every check that changed verdict, with the newer run's reason.

Run selectors are: latest, previous, latest-N, or a timestamp/SHA substring. (all is available for multi-pair diffs in snapshot-diff.)

Single-step snapshot diff

stepbook snapshot-diff <step> (alias sdiff) zooms into a single step's output between two runs:

sh
stepbook snapshot-diff analyze                 # previous vs latest
stepbook snapshot-diff analyze --fields        # per-field drift breakdown
stepbook snapshot-diff analyze --open          # open both in your diff tool
stepbook snapshot-diff analyze --across-last 5 # diff every adjacent pair in last 5 runs
snapshot-diff · step analyze · 9f3c1a2 → e4b7d05

⚠ drift: 1 flip · 3 fields changed

Assertion flips
  pass → fail  density within target
    density=verbose

Field drift
FIELD        KIND    DETAIL
───────────  ──────  ──────────────────────
density      scalar  "balanced" → "verbose"
longWords.4  added   + "thresholds"
longWords.5  added   + "configurable"
  • Assertion flips always print. --fields adds the Field drift table above; without it you get just the ⚠ drift: … summary line and a hint to pass --fields.
  • --open opens both snapshots in your external diff tool ($STEPBOOK_DIFF_CMD, default code --diff).
  • --across-last N diffs every adjacent pair across the last N runs — useful for spotting when, across a series, a step's output started drifting.

Stability: variance across repeated runs

Everything above compares two given runs. Stability is the secondary axis: it asks a different question — not "what changed between these two?" but "how much does this one step vary when I run it again and again?" It only matters for non-deterministic steps, which in practice means llm ones.

For such a step, a single output tells you little. Run it ten times and you learn a lot: are the outputs basically the same, or all over the place?

stepbook variance <step> (alias var) measures this:

sh
stepbook variance classify -n 10           # re-execute the step 10 times, fresh
stepbook variance classify --across-last 5 # use the last 5 persisted runs instead
stepbook variance classify -n 10 --show-clusters
  • -n, --runs <N>fresh sampling: re-execute the step N times right now.
  • --across-last <N>historical: use the last N persisted runs (default 5) rather than running fresh.
  • --show-clusters — list the identical-output clusters with a representative from each.

How clustering works

Stepbook groups the sampled outputs into clusters of similar outputs and flags outliers. The default similarity is a built-in textual metric (cosine similarity over tokenized output). Conceptually:

  1. Each sample is compared pairwise to the others.
  2. Samples that are highly similar collapse into a cluster.
  3. A sample whose mean similarity to the cohort falls below a threshold is flagged as an outlier.

The outlier threshold defaults to 0.6 and is configurable per step via outlierThreshold on the step definition:

ts
export default defineStep({
  name: 'classify',
  type: 'llm',
  outlierThreshold: 0.75, // stricter — flag more samples as outliers
  runner: async (input, ctx) => callModel(input),
})

A step with one dominant cluster and no outliers is stable. A step with several clusters of similar size is telling you the output isn't determined by the input — worth investigating before you rely on it.

Custom similarity with --embed

The default similarity is textual. For semantic similarity, you can supply an embedder. With --embed, stepbook clusters using an embed function exported from your pipeline.ts:

sh
stepbook variance classify -n 10 --embed         # threshold defaults to 0.85
stepbook variance classify -n 10 --embed 0.9     # custom cosine threshold

This compares outputs by embedding-vector cosine similarity rather than token overlap — better for "these say the same thing in different words."

In the dashboard

The Stability panel runs the same analysis interactively: pick a step, choose fresh or historical sampling, and see the clusters, outliers, and per-sample detail. The Compare panel is the visual counterpart to diff / snapshot-diff — assertion flips, entity-presence summary, and field-drift breakdown in one view:

Compare canvas showing FROM/TO snapshot diff with assertion flips and field-drift summary.

See The Dashboard for the full panel walkthrough.

When to reach for which

QuestionTool
Did my edit change this step's output?snapshot-diff <step>
Did anything flip across the whole run?diff
How consistent is this step run-to-run?variance <step> -n 10
Has this step been drifting over the last N runs?snapshot-diff --across-last or variance --across-last
Are two semantically-equal outputs being counted as different?variance --embed

Released under the MIT License.