/** * How a covered surface was decided to be covered. * * - `executed` — a recorded run exercised it, against THIS version of it. * - `colocated` — a test named after it sits beside it. Says the file exists. * * Two kinds, and unlike the three that were removed these are not two naming * conventions: one is about a process that ran, the other about a directory * listing. See the module header. */ export type CoverageEvidence = "executed" | "colocated" | "configured"; /** Is `a` stronger evidence than `b`? */ export declare function strongerEvidence(a: CoverageEvidence, b: CoverageEvidence): boolean; /** The minimum a surface must expose to be matched — structural, no import cycle. */ export interface CoverableSurface { /** Repo-relative path of the surface file (SKILL.md / agent .md / hook script). */ readonly path: string; /** Stable name: skill dir, agent basename, or hook script basename. */ readonly name: string; /** Substrings a test may reference to "cover" this surface (path / namespace). */ readonly tokens: readonly string[]; } /** * A discovered test file. Only its PATH matters now: colocation is decided by * placement, so the detector no longer reads a single test file's contents — * which also means a repo's coverage can no longer be changed by editing text * inside a test. */ export interface PreparedTest { readonly path: string; } /** * Is `testPath` the colocated test OF this surface — NAMED after it, SITTING * BESIDE it? * * 🔴 THIS USED TO BE WRITTEN TWICE, and the second copy said so in its own * comment: `test-coverage-files.ts` carried a function whose docstring was * *"Mirror of test-coverage.ts `isColocated`"*. A mirror is a promise that two * bodies stay equal, kept by whoever remembers — and this file already documents * three separate occasions where a change landed in one report builder and not * the other ({@link declaredSurfaceName}, {@link hookScriptRefs}). The rule now * has ONE body and three callers: the disk detector, the browser twin, and the * runner that turns an executed script into an execution record * (`coverage-artifact.ts`). There is nothing left to mirror wrong. * * That third caller is why this became shared rather than merely deduplicated. * Colocation and execution were answering the same question — *which surface is * this test about?* — with two different pieces of code, so a script vigiles had * just RUN could not be attributed by the very rule that was already willing to * credit it for existing. * * SEPARATORS ARE NORMALISED FIRST, then POSIX semantics apply. The disk detector * feeds paths from `globSync`, which yields `\` on Windows; the browser twin * feeds file-map keys, which are always `/`. Folding `\` to `/` makes one body * correct for both instead of asking each caller to bring its own path module — * which is exactly the seam the mirror lived in. * * ⚠️ THE COST, STATED: a POSIX file whose NAME literally contains a backslash is * now read as a directory boundary. That is not reachable for either input here — * a skill directory or a test filename with a `\` in it does not survive the glob * patterns that discover it in the first place — and it is the same trade * `hookScriptRefs` already makes one function up. */ export declare function isColocatedTest(surface: Pick, testPath: string): boolean; /** Wrap a discovered path. Kept as a function so both twins share one shape. */ export declare function prepareTest(path: string): PreparedTest; export { isEvalScript } from "./source-kinds.js"; /** * The root variables a hook command may be written against — the structural * slice of `PluginLayout` this needs, taken by SHAPE so the browser twin does * not have to import the layout module. */ export interface RootTokens { readonly pluginRootToken: string; readonly projectRootTokens?: readonly string[]; } /** * The hook-script paths a manifest's `hooks` block names, repo-relative. * * 🔴 THIS EXISTS BECAUSE ONE BUG HAD TO BE FIXED TWICE, and the second half was * missed. `settings.json` has two readers — the disk detector in * test-coverage.ts and the file-map twin in test-coverage-files.ts. While only * the PLUGIN token was stripped, `"$CLAUDE_PROJECT_DIR/.claude/hooks/a.sh"` — * Claude Code's DOCUMENTED spelling, used because hooks do not run with a stable * cwd — kept its literal prefix, failed the existence check, and was dropped * without a word. The disk reader was taught the project token; the twin was * not, and went on reporting ONE hook surface where disk reported FOUR, * under-reporting the untested count and therefore the score. * * Repairing the twin in place would have left the NEXT token to be added in two * places again. So the parsing, the token set, the prefix stripping and the * dependency rule live here once, and the callers inject only what genuinely * differs: where the manifest text comes from, and what "this file exists" means * (disk `existsSync` vs a key in the file map). Neither caller enumerates * tokens, so neither can forget one. */ export declare function hookScriptRefs(manifestText: string | undefined, layout: RootTokens, exists: (rel: string) => boolean): string[]; /** * The evidence one test file provides for one surface, or `null` for none. * `colocated` is passed in because placement is a path question the two twins * answer with their own (disk vs POSIX-string) path helpers. */ export declare function evidenceFor(_surface: CoverableSurface, _test: PreparedTest, colocated: boolean, configured?: boolean): CoverageEvidence | null; /** * The `{surface}` placeholder in a user's `include` — the ONE thing that makes * a centralized test layout expressible without weakening what coverage MEANS. * * The retired `declared` and `name-mentioned` tiers died because they could * credit a surface no test touched: a mention is a substring, and a substring * matched this file's own fixtures. `{surface}` cannot do that. The user writes * `tests/{surface}/evals/promptfooconfig*.yaml`, and the placeholder is replaced * with the surface's NAME before matching — so the binding between test and * surface is still the name, exactly as under colocation. Only the PLACE moves. * * What it costs, stated plainly because it is the argument colocation was chosen * on: `ls` beside the skill no longer answers "is this tested?" — you have to * know where the project keeps its tests. That is a real loss, and it is why * this is opt-in per repo rather than a second default. A project that has * already centralized its suites has paid that cost anyway. */ export declare const SURFACE_TOKEN = "{surface}"; /** Does this glob delegate its surface binding to the placeholder? */ export declare function hasSurfaceToken(glob: string): boolean; /** * The pattern to DISCOVER files with: the placeholder widened to `*` so one * glob pass finds every candidate. Narrowing back to the right surface happens * at match time — discovery must stay surface-agnostic or it would be one glob * pass per surface. */ export declare function discoveryGlob(glob: string): string; /** * Does this test file sit at a `{surface}` path configured FOR THIS SURFACE? * * The placeholder is replaced with the surface's own name, so * `tests/{surface}/evals/*.yaml` credits `tests/mysql-designer/evals/x.yaml` to * `mysql-designer` and to nothing else. A glob WITHOUT the placeholder returns * false here on purpose: a plain custom glob widens what counts as a test file, * which it always did, but it says nothing about WHICH surface the file is for * — and inferring that from a substring is exactly the retired `name-mentioned` * tier that credited surfaces nothing had touched. * * `minimatch` (already a direct dependency, pure JS) so the browser twin can * share this instead of growing a second matcher that disagrees. */ export declare function matchesSurfaceGlob(surface: Pick, testPath: string, globs: readonly string[]): boolean; /** Per-evidence tallies — the provenance summary the report prints. */ export interface EvidenceCounts { /** Decided by a recorded run against this version of the surface. */ readonly executed: number; readonly colocated: number; /** Decided by a `{surface}` testGlob — the name still binds, the place moved. */ readonly configured: number; } /** Tally a list of decisions by evidence kind. */ export declare function countEvidence(decisions: readonly { readonly evidence: CoverageEvidence; }[]): EvidenceCounts; /** * One line naming how the coverage was established. Printed wherever a coverage * count is printed: a number with no provenance is the thing this module exists * to stop shipping. * * 🔴 THE TWO CLAUSES ARE WORDED DIFFERENTLY ON PURPOSE. "Measured by a run" and * "there is a file with a matching name" are not two strengths of the same * statement, and a reader who cannot tell them apart in the output has the same * number they had before this tier existed. */ export declare function formatEvidence(counts: EvidenceCounts): string; /** * A root `SKILL.md`'s DECLARED `name:`, or `null` — the identity of a * single-skill-at-root repo. * * 🔴 SHARED BECAUSE IT DRIFTED. This lived only in the disk detector * (`test-coverage.ts`), and the browser twin hard-coded the string `"SKILL"` in * its place. For a single-skill repo declaring `name: foo`, the disk engine * therefore counted a top-level `foo.harness.mjs` as colocated coverage and the * GitHub/file-map engine — the SAME audit, same files — called the skill untested * and lowered its Tested score. That is the third time in this PR that a change * landed in one report builder and not the other, so the field is no longer * mirrored: both twins call this one function, and there is nothing left to * mirror wrong. * * The name matters only at the ROOT. For a nested skill the DIRECTORY is the * identity (`skills/foo/SKILL.md` → `foo`); the base of a single-skill target is * wherever the thing happens to be checked out — a temp dir, `~/src/wip-2`, a CI * workspace — and since colocation requires the test to be NAMED after the * surface, taking the identity from the path would ask the author to name their * test after their checkout directory. * * Read through the SHARED lenient frontmatter reader rather than a private * `load()` + regex — a small behaviour change, stated rather than smuggled: a * root SKILL.md whose YAML is malformed but whose `name:` line is salvageable now * keys coverage on that name instead of on the checkout directory. That is the * name `scanSkills` already prints for the same file, so the report and the * coverage tier stop disagreeing about what the skill is called. A file with no * declaration at all still returns `null` and the caller's fallback applies * (`basename` of the audited dir on disk, the repo name in the browser). */ export declare function declaredSurfaceName(content: string): string | null; /** * One output style as a coverage surface — shared by the disk detector and the * browser twin, so the two name and exempt a style the same way. * * Named by FILE (`status-block.md` → `status-block`), not by the name a * harness selects it by: the surface name is what a colocated test is spelled * after, and a declared name may hold spaces or colons no file name should. */ export declare function outputStyleSurface(path: string, content: string): { readonly kind: "output-style"; readonly path: string; readonly name: string; readonly tokens: readonly string[]; readonly ignored: boolean; }; /** * How many output styles a scan found, or that the harness has none. Shared by * the disk and browser scan engines, so the inventory reads the one discovery * pass both already ran. Exempt styles (`vigiles:ignore-test`) are counted: the * marker waives the test, not the style, and a style-only repository with an * exempt style is not an empty machine. */ export declare function outputStyleCount(layouts: readonly { readonly outputStyles?: unknown; }[], tier: { readonly covered: readonly { readonly kind: string; }[]; readonly untested: readonly { readonly kind: string; }[]; }, exempt: readonly { readonly kind: string; }[]): number | "not-supported"; /** The inventory counts both scan engines report the same way. */ export declare function inventoryCounts(files: Readonly>, isCommand: (path: string) => boolean, layouts: Parameters[0], coverage: { readonly harness: Parameters[1]; readonly exemptSurfaces: Parameters[2]; }): { readonly commands: number; readonly outputStyles: number | "not-supported"; }; //# sourceMappingURL=coverage-evidence.d.ts.map