Skip to content

fix(oracle-gaps): the daemon reloads every module a run read, so its reports match a cold run - #37

Merged
KyleAMathews merged 9 commits into
mainfrom
fix/daemon-current-inputs
Nov 12, 2026
Merged

KyleAMathews merged 9 commits into
mainfrom
fix/daemon-current-inputs

Conversation

@KyleAMathews

Copy link
Copy Markdown
Contributor

The gap CLI's daemon (--daemon) could report a result from before an edit. A cold run of the same state reported a different result, which breaks law 9 of the end-to-end gap oracle ("incremental equals cold"). This change closes two ways that a stored or cached run went stale. A warm context now records every module that a run evaluates, batch runs included. A stored run now lists the modules that it really loaded, also a module that only a mutated branch imports.

What went wrong

A warm context keeps the modules it loaded. At the start of each check, the runner invalidates a module that changed, but only in contexts whose ledger records that they read it. A stored unit is reused while the hash of each of its inputs is unchanged. Two gaps broke this.

A replaced context took its record with it. Only a single run (unit) recorded its modules in the ledger. A batch of mutant copies loaded the same modules but recorded nothing. The defect needs three conditions:

  1. One warm context runs a test file alone, for example the unmutated run of b.test.ts. That context records tests/strict.ts.
  2. A hang cuts off a run in that context, so the runner replaces the context. Its ledger goes with it.
  3. Other contexts, or the replacement, ran batches of b.test.ts copies, which loaded strict.ts too.

At the next check no ledger names strict.ts, so no context invalidates it. In the failing trace the edit flips strict, which gates file b's verdicts. The cold run reports src/sut.ts#f1:delete-call survived. The daemon recomputes the unit with the old strict.ts loaded and reports a kill.

A module that only a mutated branch loads was no input. The runner stored a batch copy under its file's unmutated inputs. It assumed that a mutant changes no import. A mutated branch can reach a dynamic import() that the unmutated run never took. Vitest's importDurations also leaves out a module imported at run time. An edit of that module then changed nothing that the stored unit recorded. The daemon reused a verdict that read the old module, also with one context.

The fix

  • Both places that run a batch, unitsBatch and the second run of a hung copy, go through one helper, batchIn. A single run goes through unit. Both now bracket the run with startIn and settleIn.
  • startIn invalidates each known input that changed since the context read it, also during this check, and takes the inputs' hashes at the start.
  • settleIn records what the run read in the context's ledger. If a known input changed while the run ran, it invalidates that input and does not store the run.
  • The capture's closing row now lists the files of the modules that the copy evaluated (__vitest_worker__.evaluatedModules). The worker's map keeps every module it ever evaluated, and the copy's module reset only marks them unevaluated. So the row lists only the modules marked evaluated, which are the copy's own.
  • importsOf adds them to a run's inputs, for single runs and batch copies alike. This includes the copies that finished before a hang cut their batch off. A run is stored under its file's unmutated inputs and the modules it loaded beyond them.

servedHash stays. The lookup and the ledger both need the hash of what a context serves. A context that serves an authored patch serves other text than the instrumented source.

How the oracle reaches it

This change adds two generator dimensions, an edit, and two constructed histories:

  • contexts can now be 1. With one context, every run of a check uses the same context in a fixed order. A hang then always replaces the context that read every file.

  • lazyGate makes file b's verdict wait on tests/lazy.ts, which the test imports only where its output differs. The new edit lazy changes that module. Its gate is open before any edit, so the model needs no change.

  • "an edit that changed the cold result after a hang replaced the only context" has three parts. They are a hanging test in file a, a judging test in file b, and the helper edit. It enters a required state of the same name. The state requires two conditions: the daemon's previous check cut a run off, and the edit changed the cold result. Whether the replacement read the edited file again is not observed. The history's order makes it read the file.

  • "an edit to a module that only mutated runs load" has three parts. They are one function, a judging test in file b with the lazy gate, and the lazy edit. It enters "edit lazy changed the cold result".

  • "a lazy module loaded before a hang in the same batch" has one context, the lazy gate, and the lazy edit. File b owns an if (x > 4) function and tests it twice: a judging test with input 1 and a hanging test with input 7. One batch runs the if-true mutant, which loads the lazy module and is killed, and then a mutant that hangs. The history enters "a lazy edit changed the cold result after a batch was cut off". Which copy loaded the module is not observed. The history's order puts it before the hang.

Each history costs 3 to 13 s locally. Since the e2e oracle registers its campaign parts as units (#35), each history has a fixed-seed, a random, and a determinism unit. scripts/e2e-shards.mjs puts each history's three units in one shard, and its check passes. CI still runs only the fixed-seed units. Both run in every campaign, so CI's fixed-seed job checks them.

Evidence

  • Law: law 9 of the end-to-end gap oracle. A check through the daemon reports what a cold run of the same state reports. Authority: the maintainer's decision v19.3 (2026-11-05), stated in the oracle's header.
  • Reach: the fixed-seed campaign enters both new required states. Every required state of the parent's fixed-seed reach is still entered.
  • RED:
    • The replaced context: on f0cda58 with only the oracle change, ORACLE_SEED=20261105 ORACLE_PATH=27 ORACLE_PROPERTY=oracle-gaps.report fails law 9 in 9 s. The difference is "after helper, the daemon's report differs from the cold run's at src/sut.ts#f1:delete-call".
    • The lazy module: on the first fix (bf440ae) with only the oracle change, ORACLE_PATH=28 fails law 9 in 3 s. The difference is "after lazy, the daemon's report differs from the cold run's at src/sut.ts#f0:delete-call".
    • The lazy module before a hang: on 7870d09 with only the oracle change, ORACLE_PATH=29 fails law 9 in 12 s. The difference is "after lazy, the daemon's report differs from the cold run's at src/sut.ts#f1:if-true". The review found this history.
    • These replay commands come from before feat(oracles)!: split each campaign into named units, replayable with ${prefix}_UNIT #35. On this branch now, each history runs as its own unit, for example -t 'fixed seed 20261105: a lazy module loaded before a hang in the same batch'.
  • GREEN: the three replays pass. After the merge with main (feat(oracles)!: split each campaign into named units, replayable with ${prefix}_UNIT #35), all 31 fixed-seed units pass under the lock (-t "fixed seed 20261105", 305 s locally). The fast gap suites pass (31,249 tests). The full end-to-end oracle passes all 13 tests (2,768 s locally, under a load average of 14 to 23). Its fixed-seed reach has no state that the parent reached and this run misses.
  • Mutants:
    • A batch that records nothing in the ledger fails replay 27.
    • A batch that records only its test file, not the file's imports, fails replay 27.
    • Inputs without the copy's evaluated modules fail replay 28.
    • Cut-off copies that drop their evaluated modules fail replay 29.
  • Receiving witness: the full witness module with one context, edits test then helper, checked cold and through the daemon. On the parent, the daemon differs from the cold run at 8 mutants after the helper edit. With the first fix it differs at none.
  • Limits:
    • Law 9's edits come only between checks. The handling of an input that changes while a run runs has no witness and is fixed by inspection. A witness needs a step that edits a file between a unit's start and its end. The oracle's limits record this.
    • A module that a run first loads, and that changes during that same run, is stored under its later hash.
    • The list of evaluated modules reads Vitest 4's worker state. Where that state is missing, a run falls back to importDurations and misses a module imported at run time. The gap suites run only on Vitest 4.
    • The disk hash is memoized by size and modification time. An edit that keeps both is not seen.
    • Before the per-copy filter, a copy's inputs also named the modules of the copies before it in its batch. That was safe but made more runs recompute after an edit. No observation shows it, because the evidence counts are per check, not per file.
    • Only the helper edit after a replaced context, and only a lazy import in a test file, are constructed. A lazy import in a source module takes the same path through the evaluated modules but has no history of its own.

Scope check

These other evidence keys were checked for the same per-context staleness:

  • A timed-out unit is stored under its file's unmutated inputs, checked against their current hashes. Its copy ended before the capture's closing row, so it lists no evaluated modules.
  • An authored copy is served per context. Setting and clearing the patch invalidate that path in that context. The ledger records the patch hash when the batch runs.
  • Module loads go through the ledger and the evaluated modules that this change adds.

This pull request and its description were written by Isaac.

KyleAMathews and others added 9 commits November 11, 2026 12:54
The grammar gains one warm context (`--contexts 1`), where every run of a
check takes the same context in a fixed order. The constructed history
"an edit after a hang replaced the only context" has a hanging test in
file a and a judging test in file b, and edits file b's helper. It enters
the new required state of that name: the edit changed the cold result
after the daemon's previous check cut a run off and so replaced the
context that had read every file.

On the parent commit the history fails law 9 at once: after the helper
edit the daemon reports f1's delete-call mutant killed, while the cold run
reports it survived (ORACLE_SEED=20261105 ORACLE_PATH=27).

Co-authored-by: Isaac <no-reply@databricks.com>
A warm context invalidates, at the start of a check, every module whose
text changed since a context recorded reading it. Only a single run
(`unit`) recorded its reads. A batch of mutant copies loads its file's
modules too, but recorded nothing. When a hang replaced the context that
had recorded a module, no context kept the record, so another context, or
the replacement, kept the old module after an edit, and the daemon
reported results from before the edit.

Each batch run, and each hung copy's confirming run, now goes through
`batchIn`, which records the modules of its copies' files in that
context before it runs them.

Co-authored-by: Isaac <no-reply@databricks.com>
The grammar gains `lazyGate`: file b's verdict also waits on
`tests/lazy.ts`, which the test imports only where its output differs,
so no unmutated run loads it. The new edit `lazy` changes that module.
The gate is open before any edit, so the model needs no change. The
constructed history "an edit to a module that only mutated runs load"
enters the new required state "edit lazy changed the cold result".

On bf440ae the history fails law 9 at once: after the lazy edit, the
daemon reuses a kill of f0's delete-call mutant that read the old module
(ORACLE_SEED=20261105 ORACLE_PATH=28).

The state of the replaced-context history is renamed to what it measures:
"an edit that changed the cold result after a hang replaced the only
context". The limits record that law 9's edits come only between checks,
so an input that changes while a run runs has no witness.

Co-authored-by: Isaac <no-reply@databricks.com>
…ch run

The capture's closing row lists the files of the modules each copy
evaluated, which include a module that only a mutated branch imports at
run time (Vitest's importDurations leaves it out). A run is stored under
its file's unmutated inputs and those modules, for single runs and batch
copies alike.

Each run is bracketed: startIn invalidates a known input that changed
since the context read it, also during this check, and takes the inputs'
hashes at the start. settleIn records what the run read in the context's
ledger, invalidates an input that changed while the run ran, and then
does not store the run.

Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
…e batch

The constructed history "a lazy module loaded before a hang in the same
batch" runs one context with the lazy gate. File b tests an `if (x > 4)`
function with a judging test (input 1) and a hanging test (input 7), so
one batch runs the if-true mutant, which loads tests/lazy.ts and is
killed, and then a mutant that hangs. It enters the new required state
"a lazy edit changed the cold result after a batch was cut off".

On 7870d09 it fails law 9 (ORACLE_SEED=20261105 ORACLE_PATH=29): the
copies before the hang were stored without their evaluated modules, so
the daemon reused the kill after the lazy edit. The review found the
history.

Co-authored-by: Isaac <no-reply@databricks.com>
…its own

The copies that finished before a hang cut their batch off now carry the
modules they evaluated as inputs, as every other batch copy does. The
capture lists only the modules marked evaluated: the worker's map keeps
every module it ever evaluated, and a copy's module reset only marks
them unevaluated, so a copy listed the modules of the copies before it.
settleIn hashes each path once.

Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Each of the three new constructed histories has a fixed-seed, a random,
and a determinism unit. Each history's three units go to one shard
(e2e-1 to e2e-3); they cost 3 to 13 s each locally.

Co-authored-by: Isaac <no-reply@databricks.com>
@changeset-bot

changeset-bot Bot commented Nov 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9c18836

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@tanstack/oracles Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Nov 12, 2026

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 97ec218c-936f-43a0-a5cb-a48db903d3f3
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@pkg-pr-new

pkg-pr-new Bot commented Nov 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/TanStack/oracles/@tanstack/eslint-plugin-oracles@37
npm i https://pkg.pr.new/TanStack/oracles/@tanstack/oracles@37

commit: 9c18836

@KyleAMathews
KyleAMathews merged commit 908b1f9 into main Nov 12, 2026
13 checks passed
@KyleAMathews
KyleAMathews deleted the fix/daemon-current-inputs branch November 12, 2026 04:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant