import type { ProbeOrigin, SurfaceProbe } from "./check-count.js"; import { COVERAGE_ARTIFACT_FILE } from "./local-files.js"; import type { Surface, SurfaceKind } from "./test-coverage.js"; /** Bumped when the record shape changes in a non-additive way. */ export declare const COVERAGE_ARTIFACT_VERSION = 1; /** The artifact filename under `.vigiles/` — defined on the one local-files list. */ export { COVERAGE_ARTIFACT_FILE }; /** * Which runner produced a record. Mirrors the discovery split in * `test-coverage.ts`: `vigiles test` runs `*.harness.*` (free, every push), * `vigiles eval` runs `*.eval.*` (paid, scheduled). Kept per record so a run * cannot credit the tier it did not belong to — an executed harness must not * silence "nothing ever measured whether this fires". */ export type CoverageTierName = "harness" | "eval"; /** * How a RECORD came to name its surface. * * The three {@link ProbeOrigin} values are what the run REPORTED about itself * from inside its own process — a command line it executed, a transcript it got * back. `colocated` is the fourth and is different in kind: nothing inside the * test said it. The RUNNER observed that the script it just executed is the * surface's colocated test, by the same {@link isColocatedTest} rule that is * already willing to credit that file for merely existing. * * 🔴 THE SPLIT IS THE POINT, AND IT IS WHY THIS IS NOT ON {@link ProbeOrigin}. * `ProbeOrigin` is the wire vocabulary: `parseCheckReport` reads it out of a * scratch file the CHILD writes. Putting `colocated` there would let a harness * hand-write `{"how":"colocated","ref":"…"}` and mint execution coverage for a * surface by declaring it — which is `vigiles:covers` returning through the back * door, the tier this file's header records being deleted for exactly that. Kept * off `ProbeOrigin`, a script cannot spell it at all: `toProbe` has no branch * that produces it, so the only producer in the program is {@link recordsFrom}. */ export type RunAttribution = ProbeOrigin | "colocated"; /** One surface, exercised by one script, at one moment, against one version. */ export interface CoverageRun { readonly kind: SurfaceKind; /** Repo-relative path of the surface file. */ readonly path: string; readonly name: string; readonly tier: CoverageTierName; /** How the run named it — see {@link RunAttribution}. */ readonly how: RunAttribution; /** The script file that did the exercising. */ readonly by: string; /** ISO-8601 timestamp of the run. */ readonly at: string; /** Content hash of the surface file AS IT WAS when the run happened. */ readonly sha: string; } export interface CoverageArtifact { readonly v: number; readonly generated: string; /** Short commit the run was made at, when the repo is a git checkout. */ readonly commit?: string; readonly runs: readonly CoverageRun[]; } /** Content hash of a surface file — the staleness key. */ export declare function surfaceSha(content: string): string; /** * ONE spelling for one script file — the identity a coverage record is keyed by. * * 🔴 SEPARATORS WERE NOT THE ONLY WAY TO SPELL THE SAME FILE, and keying on the * raw string reopened the defect the retraction set exists to close. Measured * 2026-08-12 on the two-hook fixture in `cli-coverage-record.test.ts`: * * vigiles test → record `by: t.harness.mjs` * (empty the harness) * vigiles test ./t.harness.mjs → `hooks/a.sh` STILL "MEASURED BY A RUN" * * because `./t.harness.mjs` and `t.harness.mjs` are different keys. Nothing * expires it either: freshness is keyed to the SURFACE's text, which emptying * the test does not touch, so that record stays green forever. The same spelling * split also DOUBLES records — running once by `./t.harness.mjs` and once by the * default glob left two entries for one script, measured on the same fixture. * * `discoverScripts` is where the spellings come from: it passes an argument that * names an existing file through VERBATIM (`./x`, an absolute path, `a/../x`) * and only globs otherwise, which yields the bare relative form. So both are * ordinary, both are what a person types, and neither is wrong. * * `root` is needed only to bring an ABSOLUTE spelling back to repo-relative; a * caller that cannot say where the repo is gets separator + `./` + `..` * collapsing, which is what it could do before, and no worse. */ export declare function canonicalScript(file: string, root?: string): string; /** * The surfaces a probe refers to, or `[]`. * * 🔴 CONSERVATIVE BY KIND. A `command` probe is matched only against HOOKS: a * command runs a program, and the only surface kind that IS a program is a hook. * A skill's bundled helper script being executed is a test of that script, not * of the skill — the exact substitution ("a test NEAR it" for "a test OF it") * that colocation-by-directory was making. Today the check is belt-and-braces * (skills and agents are `.md`, and a command ref always ends in a script * extension, so the paths cannot collide); it is asserted anyway, because a * guard nothing tests is one that quietly stops holding when discovery changes. * * A `fired` probe matches by NAME, after dropping a `plugin:` namespace, because * that is the only identity the transcript carries. * * 🔴 AN AMBIGUOUS PROBE RESOLVES TO NOTHING. When two surfaces share a name — a * skill shipped at `skills/foo/` and overridden at `.claude/skills/foo/` — the * transcript says `plugin:foo` and cannot say which file ran. Returning BOTH * used to look conservative and is the opposite: exactly one of them ran, so the * shadowed copy is handed execution coverage it never earned, and the record * asserts a fact that is false. Dropping loses a real record for the copy that * did run; recording both INVENTS one. The same rule the rest of this tier * already follows — an unresolvable ref is never guessed into a match — so the * only consistent answer is to drop. * * A `command` probe is resolved by a LADDER, most precise first: an exact path, * then the ref as a ROOTED path (a harness legitimately points at * `${CLAUDE_PROJECT_DIR}/.claude/hooks/x.sh`, or at an absolute path inside the * repo), then the ref as a SHALLOWER one (a harness ran with a `cwd` inside the * repo, so it named `guard.sh`). A lower rung is consulted only when the one * above it is empty, because the rungs overlap: with both `hooks/pre.sh` and * `.claude/hooks/pre.sh` discovered, the ref `.claude/hooks/pre.sh` is an EXACT * match for one and a rooted match for the other — flat filtering made a named * file ambiguous. * * 🔴 THE MIDDLE RUNG USED TO ACCEPT ANY REF WHOSE TAIL MATCHED, so an absolute * path in SOMEBODY ELSE'S tree credited a local file. Measured 2026-08-12 against * a single surface `hooks/pre.sh`: * * ``` * /workspace/vigiles/hooks/pre.sh => ["hooks/pre.sh"] (genuinely ours) * /tmp/fixture/hooks/pre.sh => ["hooks/pre.sh"] FALSE GRANT * /home/other/repo/hooks/pre.sh => ["hooks/pre.sh"] FALSE GRANT * /nix/store/abc123-plugin/hooks/pre.sh => ["hooks/pre.sh"] FALSE GRANT * ``` * * Keeping the full qualified path did not fix it — the bottom rung's repair was * about a ref that carried NO directories, and any external path that happens to * end in the complete repo-relative tail still miscredits. It is the same defect * the bottom rung had, one level up: a SUFFIX taken for an identity. * * A path is only ours if it is anchored at our root, so the rung now demands an * anchor and the caller supplies one ({@link ResolveOptions.root}). Two anchors * exist and no third is invented: * * - the repository root itself, for an absolute ref — it must lie beneath it, * and the remainder is then a repo-relative path compared EXACTLY; * - an unexpanded PROJECT-root variable ({@link ROOT_VARS}), which is how a * settings-file hook reaches this tier (`commandRefs` leaves the name as * written when nothing in `env` expands it). * * Anything else — an absolute ref outside the root, an absolute ref with NO root * supplied, a relative ref carrying extra leading directories (`../other/…`, * `vendor/plugin/…`) — resolves to nothing. Absent a root the rung abstains * wholesale rather than falling back to the tail match, which is what a caller * that cannot name its own root honestly knows. * * 🔴 THE BOTTOM RUNG USED TO MATCH BY BARE NAME, and that is the third appearance * in this PR of a NAME accepted as an IDENTITY — after the coverage metric's * "mention" tier (deleted; it produced 9 of 10 covered surfaces, ≥3 falsely) and * the foreign-runner gate's identifier search (replaced by a call site). It threw * the ref's directories away, so executing `/tmp/guard.sh` in a passing harness * credited the repo's sole `hooks/guard.sh` — a file that never ran. * * It is now a TAIL ALIGNMENT in the other direction rather than a qualified * name: every segment the ref carries must lie on the surface's path. That * distinguishes it from the two deletions, and the distinction is the whole * argument for keeping a third rung at all — those matched on a PROJECTION of * the evidence (a name pulled out of a file, an identifier pulled out of a * module), discarding what the rest of it said. Here nothing is discarded: a ref * with no directories contradicts no directory, and a ref with directories must * agree with them. `/tmp/guard.sh` now resolves to NOTHING, because no repo * surface path can end in `//tmp/guard.sh`. * * The new rung is a strict SUBSET of the old one (tail alignment implies equal * basenames), so this can only ever remove grants, never add one. * * ⚠️ MEASURED ON THIS REPO'S OWN CORPUS, and the honest answer is that the corpus * does not vote. `vigiles test` over all 13 example harnesses yields THREE * `command` probes, all of which fell to the bottom rung and all of which * resolved to NOTHING: * * ``` * basename → miss examples/harness/protect-main.hook.mjs * basename → miss examples/harness/warn-on-failure.hook.mjs * basename → miss /workspace/vigiles/test/dogfood/oh-my-claudecode@…/scripts/run.cjs * exact : 0 calls * suffix : 0 calls * ``` * * A second real corpus (a 43-harness consumer repo) produced ZERO command probes * at all. So no rung has granted coverage on any corpus available here, and the * bottom rung cannot be justified by what it has resolved — only by what it * would resolve for a harness using the `{ cwd }` idiom this very file's sibling * (`leafArgvSource`) recommends. The third line above is the live near-miss that * settles the direction: a VENDORED third-party script would have credited a hook * of this repo named `run.cjs`, had one existed. */ export declare function resolveProbe(probe: SurfaceProbe, surfaces: readonly Surface[], opts?: ResolveOptions): Surface[]; /** What {@link resolveProbe} needs beyond the probe and the surfaces. */ export interface ResolveOptions { /** * Plugin namespaces that mean THIS repo (`plugin:skill` / `plugin:agent`). * Absent → every qualified ref drops; see {@link localName}. */ readonly selfNamespaces?: readonly string[]; /** * The repository root, so an ABSOLUTE ref can be told from somebody else's * absolute ref. Absent → every absolute ref drops, which is what a caller that * cannot name its own root honestly knows. */ readonly root?: string; } /** Everything one script run contributes, already resolved to real surfaces. */ export interface ScriptRunRecord { /** The script file that ran. */ readonly file: string; readonly probes: readonly SurfaceProbe[]; /** * How many checks the run REPORTED, or `undefined` when it reported nothing. * * The distinction is load-bearing and is not this file's invention — see * `statusFor` in run-scripts.ts. `undefined` means the script never imported * vigiles and therefore *could not* report; that silence is the legacy branch * and is never a verdict. A positive number is the script saying, through the * library, that it made an observation. Only the second is evidence, which is * why {@link recordsFrom} attributes colocation on `> 0` rather than on * "it exited 0". */ readonly checks?: number; } /** * The runs worth recording, out of a whole `vigiles test` / `vigiles eval`. * * 🔴 ONLY A PASS WRITES. A record here says "this surface was exercised and it * behaved", so every other status is disqualified for its own reason, and the * filter is an ALLOW-list because the reasons do not generalise: * * - `fail` — it exercised the surface, but established nothing about it. * Recording it would let a RED test paint a surface covered. * - `vacuous` — it exited clean having asserted nothing at all. * - `skip` — 🔴 THE ONE THAT WAS LEAKING. A skip is not "nothing happened * first": a script may drive a hook, THEN discover a missing capability and * call `skip()` (say, `claude` is absent). The probes it already recorded are * still attached to the result, and the old deny-list (`status !== "fail"`) * let them through — so a run whose own exit code says "I did not finish" * wrote or refreshed execution-tier coverage. `runHook` is precisely the tier * that runs before the skip decision, so this was reachable, not theoretical. * * ⚠️ THIS IS NOT THE SAME RULE AS {@link executedScripts}, AND THE ASYMMETRY IS * DELIBERATE. Writing and RETRACTING ask different questions: * * | status | writes a record? | retracts its old records? | * |---|---|---| * | `pass` | yes | yes | * | `fail` | no | yes — it ran and proved nothing | * | `vacuous` | no | yes — same | * | `skip` | no | **no** — it did not run | * * A skip must not retract, because the deterministic tier skips when `claude` is * missing and deleting yesterday's measurement because today's machine lacks a * CLI would destroy a real result. A skip must not write, because it did not * finish. Both follow from "a skip is not an execution"; only the direction of * the conclusion differs. * * ## 🔴 A RUN WITH NO PROBES IS NO LONGER DISCARDED * * The second clause used to be `surfaces.length > 0`: a passing run that named no * surface contributed nothing. That threw away the ordinary case. MEASURED on a * 52-surface consumer repo, 23 of its 27 covered surfaces were carried by a * colocated `*.harness.mjs` that `vigiles test` RUNS on every push — and every one * of them reported `colocated` evidence, *"this says the file EXISTS, not that it * ran"*, because a harness asserting through `node:assert` reports a CHECK COUNT * and no probe. The run happened, the runner watched it happen, and the metric * described it as a directory listing. * * So a passing run that reported at least one CHECK is kept even with no probes: * {@link recordsFrom} can attribute it by where the script sits. `checks` travels * with it because "exited 0" is not the bar — see {@link ScriptRunRecord.checks}. */ export declare function runsFromResults(results: readonly { readonly file: string; readonly status: string; readonly checks?: number; readonly surfaces?: readonly SurfaceProbe[]; }[]): ScriptRunRecord[]; /** * Turn resolved probes into records. `readSurface` supplies the current content * of a surface file (so the hash is stamped from what was on disk during the * run); a surface it cannot read is skipped — an unhashable record could never * be checked for staleness and would therefore be permanent, unfalsifiable * coverage. * * Two sources of attribution, in one pass: the probes a run reported, and — for a * run that reported a check — the surface its own script is colocated with * ({@link colocatedSurface}). The dedupe key already carries `how`, so a run that * BOTH probed a surface and sits beside it records once per attribution and the * report can still say which kinds of evidence exist. */ export declare function recordsFrom(opts: ResolveOptions & { readonly runs: readonly ScriptRunRecord[]; readonly surfaces: readonly Surface[]; readonly tier: CoverageTierName; readonly at: string; readonly readSurface: (path: string) => string | null; }): CoverageRun[]; /** * The scripts a run actually executed, and the tier it executed them in. * See {@link executedScripts} for what "executed" means, and {@link mergeRuns} * for why the merge needs to be told. */ export interface ExecutedScripts { /** Script files, exactly as `by` records them (`ScriptRunResult.file`). */ readonly scripts: readonly string[]; readonly tier: CoverageTierName; /** * Repo root, so an ABSOLUTE spelling (`vigiles test /abs/x.harness.mjs`) * retracts the record a repo-relative run wrote. Optional: without it the * canonicalisation still collapses separators, `./` and `..`, which is all a * caller that does not know the root can honestly do. */ readonly root?: string; } /** * The scripts a run EXECUTED — the retraction set for {@link mergeRuns}. * * A skip is the one status that is not an execution: the deterministic tier * skips when `claude` is absent, and dropping a record because the machine of * the day lacks a CLI would delete a real measurement taken on the machine that * had it. Every other status ran the file. `fail` and `vacuous` are deliberately * in: a script that ran and proved nothing must not keep yesterday's green * record alive, which is the same "activity taken for the property" that * {@link runsFromResults} refuses to write in the first place. * * Deny-list here, ALLOW-list there, on purpose — see the table on * {@link runsFromResults} for why the two rules are not each other's negation. */ export declare function executedScripts(results: readonly { readonly file: string; readonly status: string; }[]): string[]; /** * Merge new records over old, keeping the newest per (surface, tier, script). * * Merging rather than replacing is what makes the two cadences composable: the * deterministic tier runs on every push and the paid tier runs on a schedule, so * a `vigiles test` today must not erase what `vigiles eval` measured last week. * Scoping the key by SCRIPT means running one file by name doesn't drop the * records of the files that were not run. * * 🔴 `executed` RETRACTS, AND WITHOUT IT COVERAGE NEVER EXPIRES. Merging alone * can only overwrite keys that appear in `next`, so a script edited to stop * touching a surface — or to assert nothing at all — leaves its old record in * place, and that record stays FRESH as long as the surface file itself is not * edited (staleness is keyed to the surface's text, not to the test's). Measured * 2026-08-11 on a two-hook fixture: a harness pointed from `hooks/a.sh` to * `hooks/b.sh` and re-run produced records for BOTH, and `lint` reported * "2 MEASURED BY A RUN" — execution-tier coverage for a hook nothing executes, * with no expiry and no way to notice. So the records of the scripts that ran * are dropped BEFORE the new ones are added: what a run says about its own * script replaces everything that run's script said before, including silence. * * The retraction is scoped to (script, tier) — the merge key minus the surface — * so it can only ever retract what that script previously claimed. Omitting * `executed` keeps the old merge-only behaviour, which is what a caller that * does not know which scripts ran must get. * * 🔴 BOTH KEYS GO THROUGH {@link canonicalScript}. One script has many * legitimate spellings (`x.mjs`, `./x.mjs`, an absolute path — `discoverScripts` * passes an existing file's argument through VERBATIM), and keying on the raw * string made one run's records unretractable by the next. The MERGE key is * canonicalised too, not only the retraction set: the second symptom was a merge * -key split — one script accumulating two records, one per spelling — and * fixing only the retraction would have left it standing. */ export declare function mergeRuns(previous: readonly CoverageRun[], next: readonly CoverageRun[], executed?: ExecutedScripts): CoverageRun[]; /** Read the artifact, or `undefined` when there is none / it is not one. */ export declare function readCoverageArtifact(root: string): CoverageArtifact | undefined; /** Write the artifact. Best-effort: recording must never fail a green run. */ export declare function writeCoverageArtifact(root: string, artifact: CoverageArtifact): void; /** A run record judged against the surface's CURRENT text. */ export interface ExecutedRecord { readonly run: CoverageRun; /** The surface's content is unchanged since the run measured it. */ readonly fresh: boolean; } /** * Index the artifact by surface path, judging each record fresh or stale against * the surface as it is NOW. * * A record names TWO files — the surface it is about (`path`) and the script that * did the exercising (`by`) — and **it is counted only while BOTH are still * present.** `currentSha` returns `null` for a surface that is gone; `scriptExists` * answers the same question for the script. Either missing ⇒ the record is * dropped, not counted and not reported as stale. * * 🔴 THE SURFACE HALF WAS PINNED AND THE EXECUTING HALF WAS NOT, so the guarantee * was half-built. Delete or rename a passing harness and its record stays FRESH * forever: freshness is keyed to the SURFACE's text, which removing the test does * not touch. Worse, that credit is PERMANENT — retraction is scoped to the scripts * a run executed, and a file that no longer exists can never appear in * `discoverScripts` output again, so no future `vigiles test` can ever withdraw * it. Execution-tier coverage for a surface with nothing left to execute it, with * no expiry and no way to falsify it. * * ## The guarantee chosen, and the one deliberately not chosen * * **Chosen: PRESENCE of both files, judged at read time.** It is the exact * symmetric twin of the rule already here for the surface, and it closes the * half retraction structurally cannot reach — the deleted and the renamed script. * It only ever REMOVES credit (the surface falls back to colocation, which needs * the same file to exist anyway), and it is non-destructive: nothing here rewrites * the artifact, so a sparse or partial checkout stops counting a record without * destroying it, and restoring the file restores the credit. * * **Not chosen: hashing the script's CONTENT into the record.** It would catch one * more case — a script still present but emptied — and that case already has a * mechanism: RETRACTION. A gutted harness re-run reports `vacuous`, `vacuous` is * in the retraction set on purpose, and every record that script wrote is dropped. * So content-hashing buys the same outcome one run later, and charges for it in * false alarms: one script legitimately covers MANY surfaces (a single * `hooks.harness.mjs` exercising thirty hooks is this author's own repo), so a * reformat — or an added assertion, i.e. the harness getting STRONGER — would * invalidate all thirty records at once. A hash cannot tell "strengthened" from * "gutted", and thirty simultaneous "measured, but not this version" lines are * the shape a reader mutes. The surface hash does not have this problem because * it is one-to-one. * * ⚠️ WHAT PRESENCE DELIBERATELY DOES NOT CATCH: a script that still exists and no * longer exercises the surface — emptied, or repointed at something else. That is * retraction's job, it is transient (one run of the tier that wrote it closes it), * and it is already tested end-to-end in `cli-coverage-record.test.ts`. * * ⚠️ AND THIS IS NOT THE `skip` ASYMMETRY, WHICH STANDS UNCHANGED. A skip must not * RETRACT — that would delete a real measurement from the artifact because today's * machine lacks `claude` (see the table on {@link runsFromResults}). This rule * deletes nothing: it is a read-time judgement about a checkout, made fresh on * every scan. The two rules coexist; neither is the other simplified. */ export declare function indexRuns(artifact: CoverageArtifact | undefined, currentSha: (path: string) => string | null, scriptExists: (by: string) => boolean): Map; //# sourceMappingURL=coverage-artifact.d.ts.map