# Feature: Stack Adapters and Test Evidence

<!-- toc -->
- [1. The adapter table](#1-the-adapter-table)
- [2. `evidence-gate.mjs --stack <id>`](#2-evidence-gatemjs---stack-id)
- [3. `test-summary.mjs`](#3-test-summarymjs)
- [4. `test-strength.mjs`](#4-test-strengthmjs)
- [Reference](#reference)
<!-- /toc -->

**Pattern**: one set of markers cannot read "the build passed" and "the tests
passed" for every stack. It does not know Maven's `BUILD SUCCESS` or a silent
`go build`, and `** TEST SUCCEEDED **`, Gradle's `BUILD SUCCESSFUL` and
`go test`'s `ok` are all printed for a run that executed zero tests. A pass
claim needs the stack's own markers and the test count, and a new test needs to
fail without the change it claims to cover.

## 1. The adapter table

`$HOME/.claude/schemas/stack-adapters.json` (shape:
`stack-adapters.schema.json`) holds one adapter per stack: `ios`, `android`,
`web`, `backend-node`, `python`, `go`, `rust`, `jvm`, plus `unknown`. Each carries
detection rules, build/test command templates, success and failure markers, the
test-summary parser order, test-file patterns, truth-source globs (localization
keys, strings.xml, i18n JSON, generated endpoints) and reference patterns with a
`(?<ref>...)` group for the symbol they name. No company or product names: the
table describes toolchains, not projects.

`$HOME/.claude/scripts/_stack-adapter.mjs` loads and validates it (no schema
library; every pattern is compiled at load) and resolves a repo:

- `stack-detect.sh` answers the coarse class (ios / android / web / backend) and
  keeps the rules that need file content - the Android plugin versus a JVM
  Gradle build, and which side of a package.json a repo is on.
- `backend` is refined by the manifest that names the language (`go.mod`,
  `Cargo.toml`, `pyproject.toml`, `pom.xml`, ...), searched root first and then
  three levels deep with the same prune set, so a vendored checkout or a build
  directory never decides the stack.
- Several adapters may match. `order` picks the primary: a language manifest
  outranks a package.json, which is often only tooling.
- Nothing matched is the `unknown` adapter, never an error.

Commands render through `commandFor`: a template slot inside double quotes is
inserted after refusing any quote-breaking character, a bare slot is
single-quoted, and a package script resolves through `package-manager.mjs`.
Tools that print nothing on success carry an `exitMarker` the shell echoes only
on exit 0.

## 2. `evidence-gate.mjs --stack <id>`

Without `--stack` the gate is unchanged. With it:

| Step | Rule |
|---|---|
| markers | the adapter's success/failure lists replace the defaults; a caller `--success-pattern` / `--failure-pattern` still wins |
| failure | decisive, before anything else |
| counts (test claim) | `test-summary.mjs` with the adapter's parsers, then `--junit` / `--xcresult` / `--xcresult-json` when the log has none: `executed == 0` fails ("no tests executed"), `failed > 0` fails. A `skipped` marker of the adapter (a test task that did not run) counts as zero when no count was read. Still unread: with the quality gates active the claim fails, since markers alone do not say how many tests ran; attended, the markers decide and the verdict says so |
| `unknown` | present evidence with no default failure marker is `ok` with `notApplicable: true`; missing or failing evidence still fails |
| a typo in the id | usage error (exit 2), so a misspelling cannot switch the success markers off |

## 3. `test-summary.mjs`

Reads counts from the runner's own output: xcodebuild (XCTest and Swift Testing
lines), xcresult (`xcrun xcresulttool` when a bundle is given, or a captured JSON
summary), JUnit XML (Gradle, Surefire, pytest), Maven, Gradle, pytest, jest,
vitest, `node --test`, `go test -v` and `-json`, `cargo test`. Output:
`{executed, passed, failed, skipped, parser, source, status, reason?}`.

- `executed` excludes skipped tests, so a run that skipped everything is a zero run.
- `status` is `passed | failed | no-tests | unparsed`; exit 0 / 1 / 3 / 4.
- A runner recognised without counts (Gradle on success, `go test` without `-v`,
  a build that failed first) is `unparsed` with the reason. Text no parser
  recognises is `unparsed` too. Never a guess.

## 4. `test-strength.mjs`

For `--base`/`--head`, runs each changed test file in a temporary worktree at
`head` (it must pass there), then restores every changed production file to
`base` (files the change added are removed) and runs it again:

| Verdict | Meaning |
|---|---|
| `red` | fails on a test without the production change: it covers the change |
| `still-green` | passes without it: it does not exercise the change |
| `unjudged` | fails without it by not compiling or loading (the adapter's `loadError` markers; every adapter's when none is named), so it never reached an assertion |
| `not-run` | fails on head, timed out, executed zero tests, or no production change to revert |

Exit 0 all red, 1 any still-green, 4 anything else, 2 usage. In the gate
ledger red plus unjudged is a pass and unjudged alone not-applicable.

The limit is the revert itself: production files go back whole and added
files are removed, so a test that references new code fails to compile or
load without it whatever it asserts. That red is no evidence, so it is not
counted as red. On a compiled stack (Swift, Kotlin) this covers every test
that calls a new symbol; only tests that compile against the base are judged. The worktree lives
under the system temp directory (never inside the repo), each run is killed as a
process group at `--timeout-ms`, and the worktree is removed and pruned on exit,
including on SIGINT/SIGTERM. The caller's checkout is not touched.

`--head worktree` judges the uncommitted working tree: it is snapshotted into a
commit object through a private index, so the caller's index, HEAD and refs are
not touched. Phase 2 ends before anything is committed, which is where it runs.

It runs every changed test twice, so it belongs to unattended runs, where
nobody is watching the clock and a weak test would otherwise pass review
unnoticed. The unattended Phase 2 exit gate calls it, with `evidence-gate
--stack` and `test-summary`, and each records its verdict in `state.gates[]`
(`features/unattended-gates.md`, section 4). An attended run does not invoke it.

## Reference

Tests: `test/stack-adapter.test.mjs`, `test/evidence-gate-stack.test.mjs`,
`test/test-summary.test.mjs`, `test/test-strength.test.mjs`. Fixtures:
`test/fixtures/stack-logs/<stack>/` (success, failure, zero-test and mixed logs
per stack; the xcodebuild, xcresult, pytest and `node --test` ones are captured
output).
