Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

apd

One test is failing. You have a stack trace and forty functions that ran. Which one actually returned the wrong value?

apd answers that. Point it at the failing test and it names the function:

npx @precisionutilityguild/apd -- vitest run
apd · 3 questions · oracle: fixture

Guilty: lineTotal  (src/cart.mjs:10)
  args:     price=5, qty=2
  returned: 11
  why:      it returned a wrong value and called nothing else — the fault is in its own body

Transcript:
  ✓ taxFor (src/cart.mjs:5) → correct
  ✗ subtotal (src/cart.mjs:15) → incorrect
  ✗ lineTotal (src/cart.mjs:10) → incorrect

How it gets there

apd records the call tree of a single failing Vitest or Jest test, then divide-and-queries it (Shapiro 1982): at each step it asks an oracle "given these arguments, is this return value correct?" and halves the suspect region with the answer. It stops at the function that returned a wrong result while everything it called returned right ones.

The split that matters:

  • The search is deterministic. Which node gets asked about, in what order, and when to stop come from the bisection math alone. Same tree, same answers, same verdict, every time.
  • The judgments are model-dependent. The default oracle is an LLM and needs ANTHROPIC_API_KEY. But the model never sees the tree, the other candidates, or the search state. It answers one yes/no question about one call.

Usage

apd -- vitest run                     # debug the one failing test in the suite
apd -- vitest run test/cart.test.mjs  # scope to a file
apd -- jest test/cart.test.js         # same, with jest as the runner
apd --json -- vitest run              # machine format, byte-stable on stdout
apd --model <id> -- vitest run        # oracle model (default claude-opus-5)
apd --max-questions 20 -- vitest run  # cap oracle questions (default 50)
apd --root ../other-project -- vitest run
apd --help

Everything after -- is your test command, and its first token names the runner: vitest or jest. There is no --runner flag — apd reads it from the command you already type. Note that apd -- jest test/cart.test.js -t "adds tax" has no run subcommand; that's vitest-only syntax.

The runner binary is resolved from your project's own node_modules, never PATH. The runner's output and apd's progress go to stderr; the report is the only thing on stdout, so apd --json > verdict.json is safe to pipe.

Offline oracle. APD_ORACLE=fixture:<path.json> swaps in an oracle answering from a JSON map keyed by function name or file:line — for testing the flow without a network. A function the map does not mention is an error, not a silent "correct".

The --json gotcha. The guilty function's key is function, not name — reach for .guilty.name and you get undefined. outcome is "guilty" or "inconclusive"; on inconclusive, guilty is null and the exit code is 1.

Runner support

Runner Versions Recording Notes
Vitest 4+ Whole-process AppMap map No per-test metadata; test identity comes from the JSON reporter
Vitest ≤3 Per-test AppMap map Carries test_status and the failure message directly
Jest 28 or 29 (>=28 <30), CommonJS only Per-test AppMap map Carries test_status, the exception and real nested call trees; jest 30+ is refused (exit 2)

Recording is done by appmap-node; apd ships no recorder of its own. jest is an optional peer dependency (jest: ">=28 <30"), never a regular one.

Why jest tops out below 30. apd records with appmap-node 2.26.1, the newest release. On jest 30.x it throws ReferenceError: environment is not defined from jest-runtime's FileCache constructor before any test runs — zero tests, zero maps, and --runInBand does not help. appmap-node's own devDependencies pin jest: ^29.6.2. apd refuses jest 30+ up front (exit 2), names appmap-node as the cause, and points at vitest as the escape hatch.

Jest must be CommonJS. apd runs jest without --experimental-vm-modules, so an ESM jest project fails to load its test files. apd refuses with exit 2, printing jest's own SyntaxError: Cannot use import statement outside a module, and notes that even with the flag appmap-node's CommonJS-only recorder writes no call tree. Use a CommonJS jest project, or vitest.

Worth knowing that jest 28/29 records better than vitest 4 — real per-test maps instead of one whole-process map.

Exit codes

Branch on these, not on stdout.

Exit Meaning
0 Verdict produced — a function was named.
1 Runtime failure: oracle/API error, the isolated re-run did not reproduce the failure, or the bisection was inconclusive (every recorded top-level call judged correct).
2 Usage error or refusal: bad flags, a runner that is neither vitest nor jest, that runner missing from the project, a jest outside >=28 <30, more than one failing test, a test file the runner could not load at all (either runner — a setup error, not a green suite), a project with its own appmap.yml, a multi-thread trace, no recorded call tree, or a fixture map with no answer for a function.
3 Nothing to do — the suite is green.

What to expect

Measured worst case over every possible guilty node on synthetic trees: a 1023-node balanced tree resolves in 12 questions (mean 10.2) where a linear scan needs 1022; a 63-node balanced tree in 8 (mean 6.2); linear chains hit exactly log2(n+1). Dogfooded on a real repo: three seeded bugs, all three named exactly, median 0.95s across nine runs (range 0.87–1.05s).

The caveat: this is logarithmic in subtree weight, not node count. A flat tree — one parent, 32 leaf children — has nothing to halve, so the search degenerates toward one question per child. Deep call trees are where the mechanism pays; wide shallow ones are where it does not.

Limitations

  • Sync and awaited code only (v1). Multi-thread traces are refused, not guessed at.
  • One failing test at a time, by design. Several failures → exit 2 with a ready-to-paste -t filter.
  • Vitest 4 records a whole-process map with no per-test metadata (appmap-node's per-test recorder engages only on vitest ≤3). Both are handled. Jest 28/29 records per test, so it does not degrade this way.
  • Jest is 28 or 29 (>=28 <30), CommonJS only. jest 30+ crashes appmap-node before any test runs; an ESM jest project cannot load its test files under apd. Both are refused (exit 2), not approximated.
  • A suite that fails to load is refused, not read as green — either runner. apd exits 2 with the runner's own error; a genuinely green suite still exits 3.
  • A project with its own appmap.yml is refused (exit 2). apd redirects the recorder by writing that file, so it cannot point appmap_dir anywhere it can collect from while yours is in place. Move it aside for the run. apd's own leftover debris from an interrupted run is the exception — that it still reclaims.
  • Concurrent apd runs in one project race on the shared appmap.yml. One at a time per repo.
  • The verdict is only as good as the oracle's answers. A wrong judgment sends the search down the wrong subtree and it will name the wrong function just as confidently — the transcript is there so you can check.
  • The live LLM oracle has never been called end to end. Not once. Its request shape is pinned by tests against an injected stub, and every end-to-end run — including the dogfood — used the offline fixture oracle with authored ground truth. Judgment quality on real code is unmeasured.

Background

Algorithmic (declarative) debugging — Shapiro 1982, ACM Distinguished Dissertation; see the ACM CSUR 2017 survey. Pick the node whose subtree weight is closest to half the suspect region and ask about it. "Incorrect" makes that subtree the new suspect region; "correct" prunes it. Either answer roughly halves the search. Stop when the known-wrong node has no unjudged children.

The technique was validated in 1982 and never productized because the oracle was a human answering dozens of tedious questions. The LLM is the oracle now and the algorithm keeps it confined: the bisection names the function, the model only answers per-node yes/no.

Full docs, the JSON contract and the receipts: github.com/PrecisionUtilityGuild/apd.

Part of a family

Four tools, one idea: answers grounded in what your tests actually execute — evidence an agent can't hallucinate. Pick by the question you're holding:

Your question Tool
"A test is failing — which line is the bug?" culprits — spectrum fault localization (npm)
"One test is failing — which function returns the wrong value?" apd — algorithmic debugging, LLM-as-oracle (npm)
"Where is feature X implemented?" recon — feature location by coverage diff (npm)
"My uncommitted diff broke the tests — which hunks?" diffbisect — delta debugging below commit granularity (npm)

MIT.