/** * `prove-control` — does this test actually fail against the pre-fix code? * * Two recorded defects collapse into that one question * (docs/DEFECT_LEDGER_DESIGN.md section 3): * * - D-008: a "negative control" run as `git stash` removed the fix AND the * tests, so only pre-existing tests ran and the control proved nothing. * - D-009: a regression test asserted a design PREFERENCE rather than the * observed defect, passed against the old code, and smuggled a design * decision in under cover of a bug fix. * * The only honest way to ask is to revert the SOURCE alone, never the test * file, and require the test to go red. * * Everything a caller needs lives here rather than in the command, because of * the design rule this module exists to obey: THE SELF-TEST MUST CALL THE SAME * IMPLEMENTATION. D-023 happened because `prove-control`'s self-test carried * its own copy of the revert logic, also in text mode, and was therefore * structurally incapable of catching the bug in the code it existed to control. * One helper, both callers. */ /** * Read a blob at a ref as BYTES. * * D-023: the original read reverted content through a text-mode subprocess, so * bytes were decoded via the locale codec (cp1252 on Windows) and rewritten as * UTF-8. Any file starting with a UTF-8 BOM came back as `` decoded * to `i>>?` and stopped parsing -- roughly 75 of the first 400 tracked Ruby * files in the reference codebase carry one. The tool's whole assertion is * "the test fails without the fix"; a CORRUPTED revert also fails, for a reason * unrelated to the defect. It could therefore report "the control fires" while * proving only that it had broken the file. * * `encoding: 'buffer'` is the entire fix and it is not optional. Text mode is * correct for statuses and refs, never for the thing being written back. * * Returns null when the path does not exist at that ref -- which is a real * case, not an error: a fix that ADDS a source file reverts to "absent". */ export declare function readBlobAtRef(cwd: string, ref: string, repoRelPath: string): Buffer | null; /** Read a working-tree file as BYTES, or null when absent. See {@link readBlobAtRef}. */ export declare function readFileBytes(absPath: string): Buffer | null; /** * Write BYTES, creating parent directories. No encoding argument: passing one * would re-introduce exactly the decode/encode round trip D-023 is about. */ export declare function writeFileBytes(absPath: string, bytes: Buffer): void; /** Absolute path of the repository root containing `cwd`, or null. */ export declare function repoRoot(cwd: string): string | null; /** * Normalise any user-supplied path to a repo-root-relative POSIX path, which is * the only form `git show :` accepts. A path outside the repository * returns null so the caller can refuse it by name. */ export declare function toRepoRelative(root: string, cwd: string, p: string): string | null; /** True when `ref` resolves in this repository. */ export declare function refExists(cwd: string, ref: string): boolean; /** * Reject a caller-supplied ref that is not a plausible git rev before git runs. * The shell-free `execFileSync` already neutralises injection; this turns a * fat-fingered `--ref` into an explicit message instead of a git error. */ export declare function assertSafeRef(ref: string): string; export type TargetKind = 'source' | 'config' | 'workflow' | 'registry' | 'docs' | 'unknown'; /** * What kind of file is this? * * The stated scope limit (design section 3): this tool assumes the fix lives in * source and the test is separate. Where the fix is a config, workflow or * registry file it reports NOT-APPLICABLE rather than passing -- a tool that * cannot assess a case must not report success on it. Reverting a CI workflow * and running a unit test proves nothing, because the unit test cannot observe * the workflow. */ export declare function classifyTarget(repoRelPath: string): TargetKind; /** * Does this path look like a test? * * D-008 in one predicate. Reverting the test along with the fix is the exact * mistake this tool exists to prevent, so naming a test file as `--source` is * refused outright rather than silently obeyed. */ export declare function looksLikeTest(repoRelPath: string): boolean; export type Language = 'python' | 'typescript' | 'ruby' | 'java' | 'go' | 'dotnet' | 'other'; /** Which toolchain owns this file, for cache-invalidation purposes. */ export declare function languageOf(repoRelPath: string): Language; export interface CacheInvalidation { language: Language; /** What was removed or run, for the report. Empty means nothing was needed. */ actions: string[]; /** Stated plainly when a toolchain needs no invalidation, and why. */ notes: string[]; } /** * Is this path, or anything under it, tracked by git? * * Fail-closed: an unreadable answer counts as TRACKED, because the cost of a * wrong "no" is deleting somebody's source and the cost of a wrong "yes" is a * cache that survives -- and the mtime bump still covers that case. */ export declare function isTracked(root: string, absPath: string): boolean; /** * Invalidate whatever the toolchain cached about the files we are about to * rewrite. * * D-017 is the trap and it generalises. `prove-control`'s own self-test * reverted a Python source file and the test did not fail: CPython invalidates * bytecode on (mtime, size), and the fixture's `VALUE = 1` and `VALUE = 2` are * the same length written in the same second -- so the cached `.pyc` was reused * and THE REVERT HAD NO EFFECT. The control reported "did not fire", and the * honest reading was *the tool is broken*, not *the test is fine*. Had the * fixture differed by one byte it would have passed and hidden the trap for * every real Python target. * * Every language with build caching has this. They are not equally exposed and * the difference is worth stating rather than papering over: * * - python -- (mtime, size). Directly vulnerable; this is D-017 itself. * - ruby -- bootsnap keys on (mtime, size) too. Directly vulnerable. * - typescript -- `.tsbuildinfo` incremental state and bundler caches key on * mtime. Directly vulnerable. * - java -- rebuild decisions are mtime-based in both maven and gradle. * - go -- the BUILD cache is content-addressed and therefore immune, but * the TEST cache can serve a cached PASS, so it is cleared. * - dotnet -- obj/bin timestamps. * * Belt and braces: the caller also bumps mtime on every file it writes, so a * cache we do not know about still sees a changed timestamp. */ export declare function invalidateBuildCaches(root: string, repoRelPaths: string[]): CacheInvalidation[]; /** * Force a distinct mtime on a file we have just written. * * The D-017 fixture reverted `VALUE = 1` to `VALUE = 2`: same size, same * second, so a (mtime, size) cache saw no change. Deleting the known caches * above is the primary defence; this is the one that also covers a cache we * have never heard of. One second in the past is used rather than the future so * nothing downstream sees a timestamp newer than wall-clock. */ export declare function bumpMtime(absPath: string): void; export interface SavedFile { repoRelPath: string; absPath: string; /** Working-tree bytes before the revert; null when the file did not exist. */ original: Buffer | null; /** Bytes at the ref; null when the path did not exist at that ref. */ atRef: Buffer | null; /** True when the ref content differs from the working tree. */ changed: boolean; } /** * Read both sides of every target as BYTES without touching the tree. * Separated from the write so a caller can decide NOT-APPLICABLE before * anything is modified. */ export declare function planRevert(root: string, ref: string, repoRelPaths: string[]): SavedFile[]; /** * Apply the planned revert. A target absent at the ref is DELETED, which is the * correct revert for a fix that added a file. */ export declare function applyRevert(files: SavedFile[]): void; /** * Put the working tree back exactly as it was, byte for byte. * * Called from a `finally` AND from signal handlers: a `finally` does not run on * SIGINT, and an interrupted run must not leave the tree holding old code. * Idempotent, because both paths can fire. */ export declare function restoreOriginals(files: SavedFile[]): void; export interface TestCommand { argv: string[]; /** How this was arrived at, for the report. */ origin: 'override' | 'inferred'; } /** * Split a `--cmd` string into argv tokens, honouring quotes. * * A plain whitespace split cannot express a path containing a space, and test * directories with spaces in them are ordinary on Windows and macOS. Quotes are * understood here rather than by handing the string to a shell, because a shell * re-opens D-012: one rewrote a git `:` into a filesystem path and * returned EMPTY, which reads exactly like "the file does not exist". */ export declare function splitCommand(command: string): string[]; /** * Infer how to run one test file from its extension. Returns null when it * cannot be inferred -- which is reported as "cannot assess", never as a pass. * * Split on whitespace into an argv list, never handed to a shell: one shell * rewrote a git `:` into a filesystem path and returned EMPTY, which * reads exactly like "the file does not exist" (D-012). */ export declare function inferTestCommand(root: string, repoRelTestPath: string, override?: string): TestCommand | null; export interface TestRun { argv: string[]; exitCode: number | null; passed: boolean; /** Combined stdout+stderr, truncated for the report. */ output: string; /** * True when the command could not be started at all. A run that never * happened is NOT a failing test, and the two must never share a report line. */ spawnFailed: boolean; } export interface ResolvedExecutable { path: string; /** A .cmd/.bat shim cannot be started directly by Windows CreateProcess. */ viaComSpec: boolean; } /** * Find the real executable behind a bare command name. * * Found by running the tool against this repository's own history: on Windows * `npx`, `npm`, `go` and friends are `.cmd` shims, and `spawnSync('npx', args, * { shell: false })` fails with ENOENT. Since the inferred TypeScript command * IS `npx vitest run`, the tool was INCONCLUSIVE on every TypeScript target on * the platform this repository is developed on -- while reporting it as if the * test had failed. Both halves of that are fixed here and in `runTest`. * * Resolution is explicit rather than delegated to `shell: true`, because a * shell re-introduces the trap the design rule names: one shell rewrote a git * `:` into a filesystem path and returned EMPTY, which reads exactly * like "the file does not exist" (D-012). */ export declare function resolveExecutable(cmd: string, env?: NodeJS.ProcessEnv): ResolvedExecutable | null; /** * Run the test command. An argv list, never a shell string, per the design rule. * `PYTHONDONTWRITEBYTECODE` closes the D-017 hole for any bytecode written * DURING the run, on top of the deletion already performed. */ export declare function runTest(root: string, argv: string[], timeoutMs: number): TestRun; export type Verdict = 'proved' | 'not_proved' | 'not_applicable' | 'inconclusive'; export interface ProveControlOptions { test: string; sources: string[]; ref?: string; cmd?: string; cwd?: string; timeoutMs?: number; /** Skip the baseline run. Off by default -- see the note in proveControl. */ skipBaseline?: boolean; /** * Called immediately before the working tree is modified, with the exact * command that restores it. * * A `finally` covers a throw and the SIGINT/SIGTERM handlers cover an * interrupt, but nothing survives SIGKILL or a power cut -- and in that window * the tree holds PRE-FIX code with no record of why. The targets are all * tracked files, so git is the recovery; this makes sure the user is told how * before the window opens rather than left to work it out afterwards. */ onRevert?: (recoveryCommand: string) => void; } export interface ProveControlReport { verdict: Verdict; reason: string; ref: string; test: string; /** * `changed`/`existedAtRef` are null when the target was never compared -- * the scope-limit path returns before reading the ref. Reporting `false` * there would assert a comparison that did not happen, which is the * instrument-confidently-wrong class this whole tool is aimed at. */ targets: { path: string; kind: TargetKind; changed: boolean | null; existedAtRef: boolean | null; }[]; caches: CacheInvalidation[]; baseline?: TestRun; reverted?: TestRun; } export declare const EXIT_PROVED = 0; export declare const EXIT_NOT_PROVED = 1; export declare const EXIT_INCONCLUSIVE = 3; export declare const EXIT_NOT_APPLICABLE = 4; export declare function exitCodeFor(verdict: Verdict): number; /** * Revert the named sources to `ref`, run the named test, require it to FAIL. * * The baseline run is on by default and that is a deliberate strengthening of * the design's shape. D-023's failure mode was a revert that broke the file, so * the test went red for a reason unrelated to the defect and the tool reported * "the control fires". Byte fidelity is the primary fix; requiring the test to * be GREEN before the revert is the independent one, because it catches every * other way a red test can be uninformative -- a broken fixture, a missing * dependency, an unrelated pre-existing failure. */ export declare function proveControl(options: ProveControlOptions): Promise; //# sourceMappingURL=prove-control.d.ts.map