@descent-vtt/spec-harness 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +75 -0
  4. package/README.md +18 -3
  5. package/dist/answers.d.ts +6 -0
  6. package/dist/answers.js +12 -1
  7. package/dist/answers.js.map +1 -1
  8. package/dist/cli.d.ts +1 -1
  9. package/dist/cli.js +81 -43
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands.js +1 -0
  12. package/dist/commands.js.map +1 -1
  13. package/dist/configure.d.ts +58 -2
  14. package/dist/configure.js +116 -3
  15. package/dist/configure.js.map +1 -1
  16. package/dist/context.d.ts +16 -7
  17. package/dist/context.js +36 -11
  18. package/dist/context.js.map +1 -1
  19. package/dist/fs.js +1 -0
  20. package/dist/fs.js.map +1 -1
  21. package/dist/git.js +2 -1
  22. package/dist/git.js.map +1 -1
  23. package/dist/hooks.js.map +1 -1
  24. package/dist/host.js.map +1 -1
  25. package/dist/literal.d.ts +18 -0
  26. package/dist/literal.js +21 -0
  27. package/dist/literal.js.map +1 -0
  28. package/dist/probe.js +1 -1
  29. package/dist/probe.js.map +1 -1
  30. package/dist/reader.js +2 -0
  31. package/dist/reader.js.map +1 -1
  32. package/dist/round.d.ts +12 -1
  33. package/dist/round.js +23 -7
  34. package/dist/round.js.map +1 -1
  35. package/dist/sandbox.js.map +1 -1
  36. package/dist/server.js +2 -1
  37. package/dist/server.js.map +1 -1
  38. package/dist/setup.js +2 -1
  39. package/dist/setup.js.map +1 -1
  40. package/dist/siblings.js +2 -1
  41. package/dist/siblings.js.map +1 -1
  42. package/dist/signers.js +5 -0
  43. package/dist/signers.js.map +1 -1
  44. package/dist/usage.d.ts +83 -27
  45. package/dist/usage.js +61 -16
  46. package/dist/usage.js.map +1 -1
  47. package/dist/versions.js +3 -0
  48. package/dist/versions.js.map +1 -1
  49. package/dist/workspace.d.ts +9 -0
  50. package/dist/workspace.js +48 -32
  51. package/dist/workspace.js.map +1 -1
  52. package/package.json +3 -3
package/dist/fs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets the disk: the configuration, the state\n * directory outside the work tree, and paths as the filesystem spells them.\n */\n\nimport { existsSync, realpathSync, statSync } from 'node:fs';\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\n\nimport { ConfigError, CONFIG_FILE, DEFAULT_CONFIG, parseConfig, type HarnessConfig } from './config.js';\n\n/** Reads `.spec-harness.json` at the root, or the defaults when there is none. */\nexport async function loadConfig(root: string): Promise<{ config: HarnessConfig; file: string | null }> {\n const file = join(root, CONFIG_FILE);\n if (!existsSync(file)) return { config: DEFAULT_CONFIG, file: null };\n let raw: unknown;\n try {\n // A Buffer parses as its text, so the encoding is equivalent to its mutant.\n raw = JSON.parse(await readFile(file, 'utf8'));\n } catch (error) {\n // A directory under the file's name is not JSON that went wrong, and\n // Node's words for it, EISDIR, are not the harness's.\n if ((error as NodeJS.ErrnoException).code === 'EISDIR') throw new ConfigError(`${CONFIG_FILE} is a directory, not a configuration file`);\n throw new ConfigError(`${CONFIG_FILE} is not valid JSON: ${(error as Error).message}`);\n }\n return { config: parseConfig(raw), file };\n}\n\n/**\n * The real spelling of a path: links resolved and, on a filesystem that\n * ignores case, the case the directory entries have. A path that does not\n * exist yet is resolved through its nearest existing ancestor.\n *\n * This is what stops `SRC/db/schema.ts` from walking past a protection on\n * `src/db/schema.ts` on Windows or macOS, where both name one file.\n */\nexport function realSpelling(path: string): string {\n const missing: string[] = [];\n let current = resolve(path);\n for (;;) {\n try {\n // join leaves a real path as it is, and reads a segment's leading\n // separator as the one between it and the last.\n return join(realpathSync.native(current), ...missing.reverse());\n } catch {\n const parent = dirname(current);\n // Only a root that does not exist fails at the top, a drive Windows\n // does not have; everywhere else the loop ends in the try.\n if (parent === current) return resolve(path);\n missing.push(current.slice(parent.length));\n current = parent;\n }\n }\n}\n\n/**\n * A path as the repository names it - relative to the root, POSIX-separated,\n * real spelling - or `null` when it lies outside the root.\n */\nexport function repositoryPath(given: string, root: string, cwd: string): string | null {\n const absolute = isAbsolute(given) ? given : resolve(cwd, given);\n const real = realSpelling(absolute);\n const realRoot = realSpelling(root);\n const rel = relative(realRoot, real);\n // `..env` at the root is inside it; only a `..` segment climbs out.\n if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;\n return rel.split(sep).join('/');\n}\n\n/** The harness's own directory under the git common directory, created on demand (ADR-0003). */\nexport async function stateDirectory(commonDir: string, ...parts: string[]): Promise<string> {\n const directory = join(commonDir, 'spec-harness', ...parts);\n await mkdir(directory, { recursive: true });\n return directory;\n}\n\n/** Writes a file whole or not at all: a temporary file renamed over the target. */\nexport async function writeAtomic(file: string, content: string): Promise<void> {\n await mkdir(dirname(file), { recursive: true });\n const temporary = `${file}.${process.pid}.tmp`;\n // utf8 is writeFile's own default, so naming it is equivalent to its mutant.\n await writeFile(temporary, content, 'utf8');\n await rename(temporary, file);\n}\n\n/** A JSON object from a file: `null` when there is no file, `'unreadable'` when it holds anything else. */\nexport async function readJsonObject(file: string): Promise<Record<string, unknown> | null | 'unreadable'> {\n if (!existsSync(file)) return null;\n try {\n const value = JSON.parse(await readFile(file, 'utf8')) as unknown;\n return typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : 'unreadable';\n } catch {\n return 'unreadable';\n }\n}\n\n/** Whether a path names a directory that is there; one that cannot be read names none. */\nexport function isDirectory(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n // Each caller reads the answer with `!`, so the mutant that answers\n // nothing here is equivalent.\n return false;\n }\n}\n\nexport async function readText(file: string): Promise<string | null> {\n try {\n return await readFile(file, 'utf8');\n } catch {\n return null;\n }\n}\n"]}
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets the disk: the configuration, the state\n * directory outside the work tree, and paths as the filesystem spells them.\n */\n\nimport { existsSync, realpathSync, statSync } from 'node:fs';\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\n\nimport { ConfigError, CONFIG_FILE, DEFAULT_CONFIG, parseConfig, type HarnessConfig } from './config.js';\n\n/** Reads `.spec-harness.json` at the root, or the defaults when there is none. */\nexport async function loadConfig(root: string): Promise<{ config: HarnessConfig; file: string | null }> {\n const file = join(root, CONFIG_FILE);\n if (!existsSync(file)) return { config: DEFAULT_CONFIG, file: null };\n let raw: unknown;\n try {\n // A Buffer parses as its text, so the encoding is equivalent to its mutant.\n raw = JSON.parse(await readFile(file, 'utf8'));\n } catch (error) {\n // A directory under the file's name is not JSON that went wrong, and\n // Node's words for it, EISDIR, are not the harness's.\n if ((error as NodeJS.ErrnoException).code === 'EISDIR') throw new ConfigError(`${CONFIG_FILE} is a directory, not a configuration file`);\n throw new ConfigError(`${CONFIG_FILE} is not valid JSON: ${(error as Error).message}`);\n }\n return { config: parseConfig(raw), file };\n}\n\n/**\n * The real spelling of a path: links resolved and, on a filesystem that\n * ignores case, the case the directory entries have. A path that does not\n * exist yet is resolved through its nearest existing ancestor.\n *\n * This is what stops `SRC/db/schema.ts` from walking past a protection on\n * `src/db/schema.ts` on Windows or macOS, where both name one file.\n */\nexport function realSpelling(path: string): string {\n const missing: string[] = [];\n let current = resolve(path);\n for (;;) {\n try {\n // join leaves a real path as it is, and reads a segment's leading\n // separator as the one between it and the last.\n return join(realpathSync.native(current), ...missing.reverse());\n } catch {\n const parent = dirname(current);\n // Only a root that does not exist fails at the top, a drive Windows\n // does not have; everywhere else the loop ends in the try.\n if (parent === current) return resolve(path);\n missing.push(current.slice(parent.length));\n current = parent;\n }\n }\n}\n\n/**\n * A path as the repository names it - relative to the root, POSIX-separated,\n * real spelling - or `null` when it lies outside the root.\n */\nexport function repositoryPath(given: string, root: string, cwd: string): string | null {\n const absolute = isAbsolute(given) ? given : resolve(cwd, given);\n const real = realSpelling(absolute);\n const realRoot = realSpelling(root);\n const rel = relative(realRoot, real);\n // `..env` at the root is inside it; only a `..` segment climbs out.\n if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;\n return rel.split(sep).join('/');\n}\n\n/** The harness's own directory under the git common directory, created on demand (ADR-0003). */\nexport async function stateDirectory(commonDir: string, ...parts: string[]): Promise<string> {\n const directory = join(commonDir, 'spec-harness', ...parts);\n await mkdir(directory, { recursive: true });\n return directory;\n}\n\n/** Writes a file whole or not at all: a temporary file renamed over the target. */\nexport async function writeAtomic(file: string, content: string): Promise<void> {\n await mkdir(dirname(file), { recursive: true });\n const temporary = `${file}.${process.pid}.tmp`;\n // utf8 is writeFile's own default, so naming it is equivalent to its mutant.\n await writeFile(temporary, content, 'utf8');\n await rename(temporary, file);\n}\n\n/** A JSON object from a file: `null` when there is no file, `'unreadable'` when it holds anything else. */\nexport async function readJsonObject(file: string): Promise<Record<string, unknown> | null | 'unreadable'> {\n if (!existsSync(file)) return null;\n try {\n // A Buffer parses as its text, so the encoding is equivalent to its mutant.\n const value: unknown = JSON.parse(await readFile(file, 'utf8'));\n return typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : 'unreadable';\n } catch {\n return 'unreadable';\n }\n}\n\n/** Whether a path names a directory that is there; one that cannot be read names none. */\nexport function isDirectory(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n // Each caller reads the answer with `!`, so the mutant that answers\n // nothing here is equivalent.\n return false;\n }\n}\n\nexport async function readText(file: string): Promise<string | null> {\n try {\n return await readFile(file, 'utf8');\n } catch {\n return null;\n }\n}\n"]}
package/dist/git.js CHANGED
@@ -77,7 +77,8 @@ export function parseNameStatus(output) {
77
77
  i += 1;
78
78
  continue;
79
79
  }
80
- const status = code.charAt(0);
80
+ const letter = code.charAt(0);
81
+ const status = letter;
81
82
  if (status === 'R' || status === 'C') {
82
83
  out.push({ status, from: fields[i + 1] ?? '', path: fields[i + 2] ?? '' });
83
84
  i += 3;
package/dist/git.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets git.\n *\n * Reads, except for two things ADR-0003 allows and bounds: a temporary\n * worktree, added and always removed by the sandbox, and nothing else. Every\n * call is an argument vector, never a shell string, so a path or a ref cannot\n * become a command.\n */\n\nimport { execFile } from 'node:child_process';\n\nimport type { FileChange } from './types.js';\n\nexport interface Run {\n readonly code: number;\n readonly stdout: string;\n readonly stderr: string;\n}\n\n/** Runs git and answers whatever it answered; a missing git is a code of -1. */\nexport function git(args: readonly string[], cwd: string): Promise<Run> {\n return new Promise((resolve) => {\n // windowsHide only keeps a console window from flashing up on Windows,\n // and the sweep runs on Linux, where its mutant is equivalent.\n execFile('git', [...args], { cwd, maxBuffer: 256 * 1024 * 1024, encoding: 'utf8', windowsHide: true }, (error, stdout, stderr) => {\n // Every caller tells 0 from any other code, never one failure from\n // another, so which code a failure gets is equivalent to its mutants.\n const code = error === null ? 0 : typeof (error as { code?: unknown }).code === 'number' ? (error as { code: number }).code : -1;\n resolve({ code, stdout, stderr });\n });\n });\n}\n\nasync function value(args: readonly string[], cwd: string): Promise<string | null> {\n const run = await git(args, cwd);\n return run.code === 0 ? run.stdout.trim() : null;\n}\n\nexport class GitError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'GitError';\n }\n}\n\n/** The top of the work tree holding `cwd`, POSIX-separated, or `null` outside one. */\nexport async function workTreeRoot(cwd: string): Promise<string | null> {\n return value(['rev-parse', '--show-toplevel'], cwd);\n}\n\n/** The git directory every worktree of this repository shares. */\nexport async function commonDirectory(cwd: string): Promise<string | null> {\n return value(['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);\n}\n\n/**\n * Where git runs the hook `name` from, absolute: `core.hooksPath` read as git\n * reads it - relative to the work tree, absolute, or under `~` - and a linked\n * worktree's shared hooks. `null` when git cannot say, before 2.31.\n */\nexport async function hookPath(name: string, cwd: string): Promise<string | null> {\n return value(['rev-parse', '--path-format=absolute', '--git-path', `hooks/${name}`], cwd);\n}\n\n/** The checked-out branch's short name, or `null` on a detached head. */\nexport async function currentBranch(cwd: string): Promise<string | null> {\n return value(['symbolic-ref', '--quiet', '--short', 'HEAD'], cwd);\n}\n\n/** The remote's default branch as a local ref, such as `origin/main`, or `null`. */\nexport async function remoteDefault(cwd: string): Promise<string | null> {\n return value(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], cwd);\n}\n\n/** Every local branch, by its short name. */\nexport async function localBranches(cwd: string): Promise<string[]> {\n const listed = await value(['for-each-ref', '--format=%(refname:short)', 'refs/heads/'], cwd);\n // for-each-ref fails only where git itself does, outside a repository,\n // where init never runs: the null check is there for the type.\n return listed === null || listed === '' ? [] : listed.split('\\n');\n}\n\nexport async function revision(ref: string, cwd: string): Promise<string | null> {\n return value(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], cwd);\n}\n\nexport async function mergeBase(a: string, b: string, cwd: string): Promise<string | null> {\n return value(['merge-base', a, b], cwd);\n}\n\n/** Parses `git diff --name-status -z` output. */\nexport function parseNameStatus(output: string): FileChange[] {\n const fields = output.split('\\0');\n const out: FileChange[] = [];\n for (let i = 0; i < fields.length; ) {\n const code = fields[i] as string;\n if (code === '') {\n i += 1;\n continue;\n }\n const status = code.charAt(0) as FileChange['status'];\n if (status === 'R' || status === 'C') {\n out.push({ status, from: fields[i + 1] ?? '', path: fields[i + 2] ?? '' });\n i += 3;\n } else {\n out.push({ status, path: fields[i + 1] ?? '' });\n i += 2;\n }\n }\n return out;\n}\n\n/** The files changed from one revision to another, renames found. */\nexport async function changes(from: string, to: string, cwd: string): Promise<FileChange[]> {\n const run = await git(['diff', '--name-status', '-z', '--find-renames', '--no-color', from, to, '--'], cwd);\n if (run.code !== 0) throw new GitError(`git diff ${from} ${to} failed: ${run.stderr.trim()}`);\n return parseNameStatus(run.stdout);\n}\n\n/** The files a commit would change: what is staged. */\nexport async function stagedChanges(cwd: string): Promise<FileChange[]> {\n const run = await git(['diff', '--cached', '--name-status', '-z', '--find-renames', '--no-color', '--'], cwd);\n if (run.code !== 0) throw new GitError(`git diff --cached failed: ${run.stderr.trim()}`);\n return parseNameStatus(run.stdout);\n}\n\n/** A file's content at a revision, or `null` when it does not exist there. */\nexport async function show(ref: string, path: string, cwd: string): Promise<string | null> {\n const run = await git(['show', `${ref}:${path}`], cwd);\n return run.code === 0 ? run.stdout : null;\n}\n\n/** Whether a commit id from {@link blameLine} is the all-zero one of a line nobody committed. */\nexport function isUncommitted(sha: string): boolean {\n return /^0+$/.test(sha);\n}\n\n/**\n * The commit that last changed one line of a file, as of a revision, or of\n * the working tree when `ref` is `null` - where a line nobody committed yet\n * belongs to the all-zero commit.\n */\nexport async function blameLine(ref: string | null, path: string, line: number, cwd: string): Promise<string | null> {\n const run = await git(['blame', '--porcelain', '-L', `${line},${line}`, ...(ref === null ? [] : [ref]), '--', path], cwd);\n // A blame that fails prints nothing on stdout, which names no commit. Its\n // output opens with the commit, so the anchor is equivalent to its mutant.\n const sha = /^([0-9a-f]{40,64}) /.exec(run.stdout)?.[1];\n return sha ?? null;\n}\n\n/** A good signature names its allowed signer's principal; any other names none. */\nexport type Signature =\n | { readonly good: true; readonly principal: string; readonly detail: string }\n | { readonly good: false; readonly principal: null; readonly detail: string };\n\n/**\n * Whether a commit carries a good SSH signature by a principal in the given\n * allowed-signers file. The file is passed in, never read from the commit's\n * own tree: a round cannot vouch for itself (ADR-0006).\n */\nexport async function verifyCommit(sha: string, allowedSignersFile: string, cwd: string): Promise<Signature> {\n const run = await git(\n ['-c', `gpg.ssh.allowedSignersFile=${allowedSignersFile}`, '-c', 'gpg.format=ssh', 'verify-commit', '--raw', sha],\n cwd,\n );\n const text = `${run.stderr}\\n${run.stdout}`;\n const principal = /Good \"git\" signature for (\\S+)/.exec(text)?.[1];\n // git writes the check to stderr, so the trim changes the first line only\n // for output on stdout alone, which verify-commit does not print; and split\n // always answers a first part.\n const detail = text.trim().split('\\n')[0] as string;\n // git exits 0 only for a signature by a listed principal, which it names:\n // either condition alone is equivalent to both, and both trust neither.\n return run.code === 0 && principal !== undefined ? { good: true, principal, detail } : { good: false, principal: null, detail };\n}\n\nexport async function addWorktree(path: string, rev: string, cwd: string): Promise<void> {\n const run = await git(['worktree', 'add', '--detach', '--force', path, rev], cwd);\n if (run.code !== 0) throw new GitError(`git worktree add failed: ${run.stderr.trim()}`);\n}\n\n/**\n * Has git remove the worktree at `path`, and no other, with its record of it,\n * and answers whether git did. Git deletes what it can of the worktree's\n * directory and forgets the worktree even when it cannot delete it all. It\n * refuses a worktree whose directory has lost its `.git` file, and removes\n * its record of one whose directory is gone, found by the path it was added\n * at: `git worktree prune` would also forget every other worktree git has\n * lost track of, the person's among them.\n */\nexport async function removeWorktree(path: string, cwd: string): Promise<boolean> {\n // The sandbox asks again when this answers false, and git, asked again\n // about a worktree it has forgotten, changes nothing: the mutant that\n // always answers false is equivalent.\n return (await git(['worktree', 'remove', '--force', path], cwd)).code === 0;\n}\n"]}
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets git.\n *\n * Reads, except for two things ADR-0003 allows and bounds: a temporary\n * worktree, added and always removed by the sandbox, and nothing else. Every\n * call is an argument vector, never a shell string, so a path or a ref cannot\n * become a command.\n */\n\nimport { execFile } from 'node:child_process';\n\nimport type { FileChange } from './types.js';\n\nexport interface Run {\n readonly code: number;\n readonly stdout: string;\n readonly stderr: string;\n}\n\n/** Runs git and answers whatever it answered; a missing git is a code of -1. */\nexport function git(args: readonly string[], cwd: string): Promise<Run> {\n return new Promise((resolve) => {\n // windowsHide only keeps a console window from flashing up on Windows,\n // and the sweep runs on Linux, where its mutant is equivalent.\n execFile('git', [...args], { cwd, maxBuffer: 256 * 1024 * 1024, encoding: 'utf8', windowsHide: true }, (error, stdout, stderr) => {\n // Every caller tells 0 from any other code, never one failure from\n // another, so which code a failure gets is equivalent to its mutants.\n const code = error === null ? 0 : typeof (error as { code?: unknown }).code === 'number' ? (error as { code: number }).code : -1;\n resolve({ code, stdout, stderr });\n });\n });\n}\n\nasync function value(args: readonly string[], cwd: string): Promise<string | null> {\n const run = await git(args, cwd);\n return run.code === 0 ? run.stdout.trim() : null;\n}\n\nexport class GitError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'GitError';\n }\n}\n\n/** The top of the work tree holding `cwd`, POSIX-separated, or `null` outside one. */\nexport async function workTreeRoot(cwd: string): Promise<string | null> {\n return value(['rev-parse', '--show-toplevel'], cwd);\n}\n\n/** The git directory every worktree of this repository shares. */\nexport async function commonDirectory(cwd: string): Promise<string | null> {\n return value(['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);\n}\n\n/**\n * Where git runs the hook `name` from, absolute: `core.hooksPath` read as git\n * reads it - relative to the work tree, absolute, or under `~` - and a linked\n * worktree's shared hooks. `null` when git cannot say, before 2.31.\n */\nexport async function hookPath(name: string, cwd: string): Promise<string | null> {\n return value(['rev-parse', '--path-format=absolute', '--git-path', `hooks/${name}`], cwd);\n}\n\n/** The checked-out branch's short name, or `null` on a detached head. */\nexport async function currentBranch(cwd: string): Promise<string | null> {\n return value(['symbolic-ref', '--quiet', '--short', 'HEAD'], cwd);\n}\n\n/** The remote's default branch as a local ref, such as `origin/main`, or `null`. */\nexport async function remoteDefault(cwd: string): Promise<string | null> {\n return value(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], cwd);\n}\n\n/** Every local branch, by its short name. */\nexport async function localBranches(cwd: string): Promise<string[]> {\n const listed = await value(['for-each-ref', '--format=%(refname:short)', 'refs/heads/'], cwd);\n // for-each-ref fails only where git itself does, outside a repository,\n // where init never runs: the null check is there for the type.\n return listed === null || listed === '' ? [] : listed.split('\\n');\n}\n\nexport async function revision(ref: string, cwd: string): Promise<string | null> {\n return value(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], cwd);\n}\n\nexport async function mergeBase(a: string, b: string, cwd: string): Promise<string | null> {\n return value(['merge-base', a, b], cwd);\n}\n\n/** Parses `git diff --name-status -z` output. */\nexport function parseNameStatus(output: string): FileChange[] {\n const fields = output.split('\\0');\n const out: FileChange[] = [];\n for (let i = 0; i < fields.length; ) {\n const code = fields[i] as string;\n if (code === '') {\n i += 1;\n continue;\n }\n const letter = code.charAt(0);\n const status = letter as FileChange['status'];\n if (status === 'R' || status === 'C') {\n out.push({ status, from: fields[i + 1] ?? '', path: fields[i + 2] ?? '' });\n i += 3;\n } else {\n out.push({ status, path: fields[i + 1] ?? '' });\n i += 2;\n }\n }\n return out;\n}\n\n/** The files changed from one revision to another, renames found. */\nexport async function changes(from: string, to: string, cwd: string): Promise<FileChange[]> {\n const run = await git(['diff', '--name-status', '-z', '--find-renames', '--no-color', from, to, '--'], cwd);\n if (run.code !== 0) throw new GitError(`git diff ${from} ${to} failed: ${run.stderr.trim()}`);\n return parseNameStatus(run.stdout);\n}\n\n/** The files a commit would change: what is staged. */\nexport async function stagedChanges(cwd: string): Promise<FileChange[]> {\n const run = await git(['diff', '--cached', '--name-status', '-z', '--find-renames', '--no-color', '--'], cwd);\n if (run.code !== 0) throw new GitError(`git diff --cached failed: ${run.stderr.trim()}`);\n return parseNameStatus(run.stdout);\n}\n\n/** A file's content at a revision, or `null` when it does not exist there. */\nexport async function show(ref: string, path: string, cwd: string): Promise<string | null> {\n const run = await git(['show', `${ref}:${path}`], cwd);\n return run.code === 0 ? run.stdout : null;\n}\n\n/** Whether a commit id from {@link blameLine} is the all-zero one of a line nobody committed. */\nexport function isUncommitted(sha: string): boolean {\n return /^0+$/.test(sha);\n}\n\n/**\n * The commit that last changed one line of a file, as of a revision, or of\n * the working tree when `ref` is `null` - where a line nobody committed yet\n * belongs to the all-zero commit.\n */\nexport async function blameLine(ref: string | null, path: string, line: number, cwd: string): Promise<string | null> {\n const run = await git(['blame', '--porcelain', '-L', `${line},${line}`, ...(ref === null ? [] : [ref]), '--', path], cwd);\n // A blame that fails prints nothing on stdout, which names no commit. Its\n // output opens with the commit, so the anchor is equivalent to its mutant.\n const sha = /^([0-9a-f]{40,64}) /.exec(run.stdout)?.[1];\n return sha ?? null;\n}\n\n/** A good signature names its allowed signer's principal; any other names none. */\nexport type Signature =\n | { readonly good: true; readonly principal: string; readonly detail: string }\n | { readonly good: false; readonly principal: null; readonly detail: string };\n\n/**\n * Whether a commit carries a good SSH signature by a principal in the given\n * allowed-signers file. The file is passed in, never read from the commit's\n * own tree: a round cannot vouch for itself (ADR-0006).\n */\nexport async function verifyCommit(sha: string, allowedSignersFile: string, cwd: string): Promise<Signature> {\n const run = await git(\n ['-c', `gpg.ssh.allowedSignersFile=${allowedSignersFile}`, '-c', 'gpg.format=ssh', 'verify-commit', '--raw', sha],\n cwd,\n );\n const text = `${run.stderr}\\n${run.stdout}`;\n const principal = /Good \"git\" signature for (\\S+)/.exec(text)?.[1];\n // git writes the check to stderr, so the trim changes the first line only\n // for output on stdout alone, which verify-commit does not print; and split\n // always answers a first part.\n const detail = text.trim().split('\\n')[0]!;\n // git exits 0 only for a signature by a listed principal, which it names:\n // either condition alone is equivalent to both, and both trust neither.\n return run.code === 0 && principal !== undefined ? { good: true, principal, detail } : { good: false, principal: null, detail };\n}\n\nexport async function addWorktree(path: string, rev: string, cwd: string): Promise<void> {\n const run = await git(['worktree', 'add', '--detach', '--force', path, rev], cwd);\n if (run.code !== 0) throw new GitError(`git worktree add failed: ${run.stderr.trim()}`);\n}\n\n/**\n * Has git remove the worktree at `path`, and no other, with its record of it,\n * and answers whether git did. Git deletes what it can of the worktree's\n * directory and forgets the worktree even when it cannot delete it all. It\n * refuses a worktree whose directory has lost its `.git` file, and removes\n * its record of one whose directory is gone, found by the path it was added\n * at: `git worktree prune` would also forget every other worktree git has\n * lost track of, the person's among them.\n */\nexport async function removeWorktree(path: string, cwd: string): Promise<boolean> {\n // The sandbox asks again when this answers false, and git, asked again\n // about a worktree it has forgotten, changes nothing: the mutant that\n // always answers false is equivalent.\n return (await git(['worktree', 'remove', '--force', path], cwd)).code === 0;\n}\n"]}
package/dist/hooks.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The guard, spoken in the languages that ask it: an agent's pre- and\n * post-tool hooks, and git's pre-commit.\n *\n * Claude Code sends a hook one JSON object on stdin naming the tool and its\n * input. A file-editing tool names its file under `file_path`, `path` or\n * `notebook_path` depending on the tool and the release; all three are read,\n * because a guard that missed a renamed field would allow everything.\n *\n * What the answer may not do matters as much as what it does. Returning\n * `allow` from a pre-tool hook skips the person's own permission prompt, so\n * the guard never says `allow`: it says `deny`, or `ask`, or nothing. A\n * warning - a write outside the scope when the repository asked only to be\n * warned - is told to the model after the write, through the post-tool hook's\n * additional context, where it cannot unlock anything.\n */\n\nimport type { Decision } from './guard.js';\n\n/** The tools whose input names a file they write. */\nexport const WRITING_TOOLS: readonly string[] = ['Edit', 'Write', 'MultiEdit', 'NotebookEdit'];\n\nconst PATH_FIELDS = ['file_path', 'path', 'notebook_path'] as const;\n\nexport interface HookRequest {\n readonly event: string;\n readonly tool: string;\n /** The session's working directory, when the hook says. */\n readonly cwd: string | null;\n /** Files the tool writes, as the tool named them - absolute, usually. */\n readonly paths: readonly string[];\n}\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Reads a Claude Code hook's stdin, or says why it cannot. */\nexport function parseClaudeHook(raw: string): HookRequest | { readonly error: string } {\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch {\n return { error: 'the hook input is not JSON' };\n }\n if (!isObject(value)) return { error: 'the hook input is not a JSON object' };\n const event = value['hook_event_name'];\n const tool = value['tool_name'];\n if (typeof event !== 'string' || typeof tool !== 'string') return { error: 'the hook input names no event or tool' };\n const input = isObject(value['tool_input']) ? value['tool_input'] : {};\n const paths: string[] = [];\n if (WRITING_TOOLS.includes(tool)) {\n for (const field of PATH_FIELDS) {\n const path = input[field];\n if (typeof path === 'string' && path !== '' && !paths.includes(path)) paths.push(path);\n }\n }\n const cwd = value['cwd'];\n return { event, tool, cwd: typeof cwd === 'string' ? cwd : null, paths };\n}\n\nexport interface HookResponse {\n /** What to print on stdout: JSON, or nothing. */\n readonly stdout: string;\n readonly exitCode: number;\n}\n\nconst NONE: HookResponse = { stdout: '', exitCode: 0 };\n\nfunction reasonOf(decisions: readonly Decision[]): string {\n return decisions.map((decision) => `spec-harness: ${decision.message}. Next: ${decision.hint}.`).join('\\n');\n}\n\n/**\n * The answer to a Claude Code hook: a refusal or a question before a write,\n * a warning after one, or nothing.\n */\nexport function claudeResponse(event: string, decisions: readonly Decision[]): HookResponse {\n if (event === 'PreToolUse') {\n const denied = decisions.filter((decision) => decision.verdict === 'deny');\n const asked = decisions.filter((decision) => decision.verdict === 'ask');\n const decided = denied.length > 0 ? { verdict: 'deny', list: denied } : asked.length > 0 ? { verdict: 'ask', list: asked } : null;\n if (decided === null) return NONE;\n return {\n stdout: `${JSON.stringify({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: decided.verdict,\n permissionDecisionReason: reasonOf(decided.list),\n },\n })}\\n`,\n exitCode: 0,\n };\n }\n if (event === 'PostToolUse') {\n const warned = decisions.filter((decision) => decision.verdict === 'warn');\n if (warned.length === 0) return NONE;\n return {\n stdout: `${JSON.stringify({\n hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: reasonOf(warned) },\n })}\\n`,\n exitCode: 0,\n };\n }\n return NONE;\n}\n\n/**\n * The answer to git's pre-commit hook: every file the commit would change\n * that the round may not, and exit 1 if any is refused. A warning prints and\n * lets the commit through; a question has no one to ask in git, so it refuses.\n */\nexport function gitResponse(decisions: readonly Decision[]): { readonly text: string; readonly exitCode: number } {\n const lines: string[] = [];\n let refused = 0;\n for (const decision of decisions) {\n if (decision.verdict === 'allow') continue;\n const blocking = decision.verdict === 'deny' || decision.verdict === 'ask';\n if (blocking) refused += 1;\n lines.push(`${blocking ? 'refused' : 'warning'} ${decision.message}`, ` ${decision.hint}`);\n }\n if (refused > 0) lines.push(`spec-harness: ${refused} file(s) this round may not change; the commit was stopped`);\n return { text: lines.length === 0 ? '' : `${lines.join('\\n')}\\n`, exitCode: refused > 0 ? 1 : 0 };\n}\n"]}
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The guard, spoken in the languages that ask it: an agent's pre- and\n * post-tool hooks, and git's pre-commit.\n *\n * Claude Code sends a hook one JSON object on stdin naming the tool and its\n * input. A file-editing tool names its file under `file_path`, `path` or\n * `notebook_path` depending on the tool and the release; all three are read,\n * because a guard that missed a renamed field would allow everything.\n *\n * What the answer may not do matters as much as what it does. Returning\n * `allow` from a pre-tool hook skips the person's own permission prompt, so\n * the guard never says `allow`: it says `deny`, or `ask`, or nothing. A\n * warning - a write outside the scope when the repository asked only to be\n * warned - is told to the model after the write, through the post-tool hook's\n * additional context, where it cannot unlock anything.\n */\n\nimport type { Decision } from './guard.js';\n\n/** The tools whose input names a file they write. */\nexport const WRITING_TOOLS: readonly string[] = ['Edit', 'Write', 'MultiEdit', 'NotebookEdit'];\n\nconst PATH_FIELDS: readonly string[] = ['file_path', 'path', 'notebook_path'];\n\nexport interface HookRequest {\n readonly event: string;\n readonly tool: string;\n /** The session's working directory, when the hook says. */\n readonly cwd: string | null;\n /** Files the tool writes, as the tool named them - absolute, usually. */\n readonly paths: readonly string[];\n}\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Reads a Claude Code hook's stdin, or says why it cannot. */\nexport function parseClaudeHook(raw: string): HookRequest | { readonly error: string } {\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch {\n return { error: 'the hook input is not JSON' };\n }\n if (!isObject(value)) return { error: 'the hook input is not a JSON object' };\n const event = value['hook_event_name'];\n const tool = value['tool_name'];\n if (typeof event !== 'string' || typeof tool !== 'string') return { error: 'the hook input names no event or tool' };\n const input = isObject(value['tool_input']) ? value['tool_input'] : {};\n const paths: string[] = [];\n if (WRITING_TOOLS.includes(tool)) {\n for (const field of PATH_FIELDS) {\n const path = input[field];\n if (typeof path === 'string' && path !== '' && !paths.includes(path)) paths.push(path);\n }\n }\n const cwd = value['cwd'];\n return { event, tool, cwd: typeof cwd === 'string' ? cwd : null, paths };\n}\n\nexport interface HookResponse {\n /** What to print on stdout: JSON, or nothing. */\n readonly stdout: string;\n readonly exitCode: number;\n}\n\nconst NONE: HookResponse = { stdout: '', exitCode: 0 };\n\nfunction reasonOf(decisions: readonly Decision[]): string {\n return decisions.map((decision) => `spec-harness: ${decision.message}. Next: ${decision.hint}.`).join('\\n');\n}\n\n/**\n * The answer to a Claude Code hook: a refusal or a question before a write,\n * a warning after one, or nothing.\n */\nexport function claudeResponse(event: string, decisions: readonly Decision[]): HookResponse {\n if (event === 'PreToolUse') {\n const denied = decisions.filter((decision) => decision.verdict === 'deny');\n const asked = decisions.filter((decision) => decision.verdict === 'ask');\n const decided = denied.length > 0 ? { verdict: 'deny', list: denied } : asked.length > 0 ? { verdict: 'ask', list: asked } : null;\n if (decided === null) return NONE;\n return {\n stdout: `${JSON.stringify({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: decided.verdict,\n permissionDecisionReason: reasonOf(decided.list),\n },\n })}\\n`,\n exitCode: 0,\n };\n }\n if (event === 'PostToolUse') {\n const warned = decisions.filter((decision) => decision.verdict === 'warn');\n if (warned.length === 0) return NONE;\n return {\n stdout: `${JSON.stringify({\n hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: reasonOf(warned) },\n })}\\n`,\n exitCode: 0,\n };\n }\n return NONE;\n}\n\n/**\n * The answer to git's pre-commit hook: every file the commit would change\n * that the round may not, and exit 1 if any is refused. A warning prints and\n * lets the commit through; a question has no one to ask in git, so it refuses.\n */\nexport function gitResponse(decisions: readonly Decision[]): { readonly text: string; readonly exitCode: number } {\n const lines: string[] = [];\n let refused = 0;\n for (const decision of decisions) {\n if (decision.verdict === 'allow') continue;\n const blocking = decision.verdict === 'deny' || decision.verdict === 'ask';\n if (blocking) refused += 1;\n lines.push(`${blocking ? 'refused' : 'warning'} ${decision.message}`, ` ${decision.hint}`);\n }\n if (refused > 0) lines.push(`spec-harness: ${refused} file(s) this round may not change; the commit was stopped`);\n return { text: lines.length === 0 ? '' : `${lines.join('\\n')}\\n`, exitCode: refused > 0 ? 1 : 0 };\n}\n"]}
package/dist/host.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets a program that is neither git nor a\n * sibling: Claude Code, asked which release it is.\n *\n * Found on `PATH` and started without a shell, as every program the harness\n * runs is (ADR-0002). On Windows a program is an `.exe` or a `.com`. A\n * `claude.cmd` shim, as npm installs Claude Code, can be started only by a\n * shell - Node refuses a `.cmd` without one since the fix for CVE-2024-27980 -\n * so it is read rather than run: the package it runs declares the release.\n */\n\nimport { execFile } from 'node:child_process';\nimport { existsSync, statSync } from 'node:fs';\nimport { delimiter, dirname, join } from 'node:path';\n\nimport { readText } from './fs.js';\nimport { declaredVersion } from './siblings.js';\nimport type { ClaudeCodeAnswer } from './versions.js';\n\n/** What Windows runs for a name with no extension, in order; the first two alone without a shell. */\nconst WINDOWS_EXTENSIONS = ['.COM', '.EXE', '.BAT', '.CMD'];\n\n/**\n * A path a shim runs, from the shim's own directory. npm's cmd-shim writes\n * `\"%dp0%\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe\"`, after\n * `SET dp0=%~dp0`; pnpm's writes `\"%~dp0\\...\"`.\n */\nconst SHIM_TARGET = /\"(?:%~dp0|%dp0%)\\\\([^\"\\r\\n]*)\"/g;\n\n/**\n * Where npm and pnpm install Claude Code, as the segments of a path: only its\n * own package declares its release, where a package that wraps it declares\n * the wrapper's.\n */\nconst CLAUDE_CODE_PACKAGE = ['node_modules', '@anthropic-ai', 'claude-code'];\n\n/** The file `name` names on `PATH` as the environment gives it, or `null`. */\nexport function onPath(name: string, env: Readonly<Record<string, string | undefined>>, platform: NodeJS.Platform = process.platform): string | null {\n // With neither, any text names directories no test can have made; and an\n // empty entry is passed over rather than read as the working directory,\n // which a test cannot fill: both mutants are equivalent in the suite.\n const path = env['PATH'] ?? env['Path'] ?? '';\n const windows = platform === 'win32';\n const extensions = windows\n ? (env['PATHEXT'] ?? WINDOWS_EXTENSIONS.join(';')).split(';').filter((extension) => extension !== '')\n : [''];\n for (const directory of path.split(windows ? ';' : delimiter)) {\n if (directory === '') continue;\n for (const extension of extensions) {\n const file = join(directory, `${name}${extension.toLowerCase()}`);\n if (existsSync(file) && statSync(file).isFile()) return file;\n }\n }\n return null;\n}\n\n/** The directory of Claude Code's package that a shim runs, or `null` when it runs none. */\nfunction shimPackage(file: string, text: string): string | null {\n for (const match of text.matchAll(SHIM_TARGET)) {\n const parts = (match[1] as string).split('\\\\');\n const at = parts.findIndex((_, start) => CLAUDE_CODE_PACKAGE.every((part, offset) => parts[start + offset] === part));\n if (at >= 0) return join(dirname(file), ...parts.slice(0, at + CLAUDE_CODE_PACKAGE.length));\n }\n return null;\n}\n\n/**\n * The release a Windows script on `PATH` runs, as the package.json of the\n * Claude Code package it names declares it; nothing is run. A script that\n * names no such package, or one whose package.json gives no version, cannot\n * tell.\n */\nasync function shimVersion(file: string): Promise<ClaudeCodeAnswer> {\n // A shim that cannot be read names no package, as an empty one does, so\n // what stands in for its text is equivalent whatever it is.\n const directory = shimPackage(file, (await readText(file)) ?? '');\n if (directory !== null) {\n const manifest = join(directory, 'package.json');\n const version = declaredVersion(manifest);\n if (typeof version === 'string') return { declared: version, file: manifest };\n }\n return {\n missing: `claude on PATH is ${file}, a script that cannot be started without a shell, which spec-harness does not use, and it names no @anthropic-ai/claude-code package whose package.json gives a version`,\n };\n}\n\n/** `claude --version`, as the Claude Code on `PATH` answers it, or why it cannot. */\nexport async function claudeVersion(env: Readonly<Record<string, string | undefined>>, platform: NodeJS.Platform = process.platform): Promise<ClaudeCodeAnswer> {\n const file = onPath('claude', env, platform);\n if (file === null) return { missing: 'no claude is on PATH' };\n if (platform === 'win32' && !/\\.(?:exe|com)$/i.test(file)) return shimVersion(file);\n // Of the options, only `shell: false` and the encoding change an answer a\n // test can see: `windowsHide` hides a console window, and the timeout\n // stops a claude that never answers, which a test would wait on for its\n // length, so their mutants are equivalent in the suite.\n return new Promise((resolve) => {\n const failed = (error: Error): void => resolve({ missing: `${file} --version failed: ${error.message.split('\\n')[0] as string}` });\n try {\n execFile(file, ['--version'], { encoding: 'utf8', windowsHide: true, timeout: 15_000, shell: false }, (error, stdout) => {\n if (error !== null) failed(error);\n else resolve({ output: stdout });\n });\n } catch (error) {\n // Windows refuses a file that is no program before any callback, as\n // spawn UNKNOWN, where Linux reports it to the callback: doctor says\n // so rather than stop. The sweep runs on Linux, so this line is the\n // Windows job's to hold.\n failed(error as Error);\n }\n });\n}\n"]}
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD,qGAAqG;AACrG,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAE5D;;;;GAIG;AACH,MAAM,WAAW,GAAG,iCAAiC,CAAC;AAEtD;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,CAAC,cAAc,EAAE,eAAe,EAAE,aAAa,CAAC,CAAC;AAE7E,8EAA8E;AAC9E,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,GAAiD,EAAE,QAAQ,GAAoB,OAAO,CAAC,QAAQ;IAClI,yEAAyE;IACzE,wEAAwE;IACxE,sEAAsE;IACtE,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IAC9C,MAAM,OAAO,GAAG,QAAQ,KAAK,OAAO,CAAC;IACrC,MAAM,UAAU,GAAG,OAAO;QACxB,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,KAAK,EAAE,CAAC;QACrG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACT,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9D,IAAI,SAAS,KAAK,EAAE;YAAE,SAAS;QAC/B,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;YACnC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;YAClE,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE;gBAAE,OAAO,IAAI,CAAC;QAC/D,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,4FAA4F;AAC5F,SAAS,WAAW,CAAC,IAAY,EAAE,IAAY;IAC7C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAI,KAAK,CAAC,CAAC,CAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC/C,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC;QACtH,IAAI,EAAE,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9F,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,WAAW,CAAC,IAAY;IACrC,wEAAwE;IACxE,4DAA4D;IAC5D,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAClE,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;QACjD,MAAM,OAAO,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;QAC1C,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAChF,CAAC;IACD,OAAO;QACL,OAAO,EAAE,qBAAqB,IAAI,0KAA0K;KAC7M,CAAC;AACJ,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAiD,EAAE,QAAQ,GAAoB,OAAO,CAAC,QAAQ;IACjI,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC7C,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC;IAC9D,IAAI,QAAQ,KAAK,OAAO,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC;IACpF,0EAA0E;IAC1E,sEAAsE;IACtE,wEAAwE;IACxE,wDAAwD;IACxD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,MAAM,GAAG,CAAC,KAAY,EAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,sBAAsB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,EAAE,EAAE,CAAC,CAAC;QAC1H,IAAI,CAAC;YACH,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;gBACtH,IAAI,KAAK,KAAK,IAAI;oBAAE,MAAM,CAAC,KAAK,CAAC,CAAC;;oBAC7B,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;YACnC,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,oEAAoE;YACpE,qEAAqE;YACrE,oEAAoE;YACpE,yBAAyB;YACzB,MAAM,CAAC,KAAc,CAAC,CAAC;QACzB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * The edge where the harness meets a program that is neither git nor a\n * sibling: Claude Code, asked which release it is.\n *\n * Found on `PATH` and started without a shell, as every program the harness\n * runs is (ADR-0002). On Windows a program is an `.exe` or a `.com`. A\n * `claude.cmd` shim, as npm installs Claude Code, can be started only by a\n * shell - Node refuses a `.cmd` without one since the fix for CVE-2024-27980 -\n * so it is read rather than run: the package it runs declares the release.\n */\n\nimport { execFile } from 'node:child_process';\nimport { existsSync, statSync } from 'node:fs';\nimport { delimiter, dirname, join } from 'node:path';\n\nimport { readText } from './fs.js';\nimport { declaredVersion } from './siblings.js';\nimport type { ClaudeCodeAnswer } from './versions.js';\n\n/** What Windows runs for a name with no extension, in order; the first two alone without a shell. */\nconst WINDOWS_EXTENSIONS = ['.COM', '.EXE', '.BAT', '.CMD'];\n\n/**\n * A path a shim runs, from the shim's own directory. npm's cmd-shim writes\n * `\"%dp0%\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe\"`, after\n * `SET dp0=%~dp0`; pnpm's writes `\"%~dp0\\...\"`.\n */\nconst SHIM_TARGET = /\"(?:%~dp0|%dp0%)\\\\([^\"\\r\\n]*)\"/g;\n\n/**\n * Where npm and pnpm install Claude Code, as the segments of a path: only its\n * own package declares its release, where a package that wraps it declares\n * the wrapper's.\n */\nconst CLAUDE_CODE_PACKAGE = ['node_modules', '@anthropic-ai', 'claude-code'];\n\n/** The file `name` names on `PATH` as the environment gives it, or `null`. */\nexport function onPath(name: string, env: Readonly<Record<string, string | undefined>>, platform: NodeJS.Platform = process.platform): string | null {\n // With neither, any text names directories no test can have made; and an\n // empty entry is passed over rather than read as the working directory,\n // which a test cannot fill: both mutants are equivalent in the suite.\n const path = env['PATH'] ?? env['Path'] ?? '';\n const windows = platform === 'win32';\n const extensions = windows\n ? (env['PATHEXT'] ?? WINDOWS_EXTENSIONS.join(';')).split(';').filter((extension) => extension !== '')\n : [''];\n for (const directory of path.split(windows ? ';' : delimiter)) {\n if (directory === '') continue;\n for (const extension of extensions) {\n const file = join(directory, `${name}${extension.toLowerCase()}`);\n if (existsSync(file) && statSync(file).isFile()) return file;\n }\n }\n return null;\n}\n\n/** The directory of Claude Code's package that a shim runs, or `null` when it runs none. */\nfunction shimPackage(file: string, text: string): string | null {\n for (const match of text.matchAll(SHIM_TARGET)) {\n const parts = (match[1] as string).split('\\\\');\n const at = parts.findIndex((_, start) => CLAUDE_CODE_PACKAGE.every((part, offset) => parts[start + offset] === part));\n if (at >= 0) return join(dirname(file), ...parts.slice(0, at + CLAUDE_CODE_PACKAGE.length));\n }\n return null;\n}\n\n/**\n * The release a Windows script on `PATH` runs, as the package.json of the\n * Claude Code package it names declares it; nothing is run. A script that\n * names no such package, or one whose package.json gives no version, cannot\n * tell.\n */\nasync function shimVersion(file: string): Promise<ClaudeCodeAnswer> {\n // A shim that cannot be read names no package, as an empty one does, so\n // what stands in for its text is equivalent whatever it is.\n const directory = shimPackage(file, (await readText(file)) ?? '');\n if (directory !== null) {\n const manifest = join(directory, 'package.json');\n const version = declaredVersion(manifest);\n if (typeof version === 'string') return { declared: version, file: manifest };\n }\n return {\n missing: `claude on PATH is ${file}, a script that cannot be started without a shell, which spec-harness does not use, and it names no @anthropic-ai/claude-code package whose package.json gives a version`,\n };\n}\n\n/** `claude --version`, as the Claude Code on `PATH` answers it, or why it cannot. */\nexport async function claudeVersion(env: Readonly<Record<string, string | undefined>>, platform: NodeJS.Platform = process.platform): Promise<ClaudeCodeAnswer> {\n const file = onPath('claude', env, platform);\n if (file === null) return { missing: 'no claude is on PATH' };\n if (platform === 'win32' && !/\\.(?:exe|com)$/i.test(file)) return shimVersion(file);\n // Of the options, only `shell: false` and the encoding change an answer a\n // test can see: `windowsHide` hides a console window, and the timeout\n // stops a claude that never answers, which a test would wait on for its\n // length, so their mutants are equivalent in the suite.\n return new Promise((resolve) => {\n const failed = (error: Error): void => resolve({ missing: `${file} --version failed: ${error.message.split('\\n')[0]!}` });\n try {\n execFile(file, ['--version'], { encoding: 'utf8', windowsHide: true, timeout: 15_000, shell: false }, (error, stdout) => {\n if (error !== null) failed(error);\n else resolve({ output: stdout });\n });\n } catch (error) {\n // Windows refuses a file that is no program before any callback, as\n // spawn UNKNOWN, where Linux reports it to the callback: doctor says\n // so rather than stop. The sweep runs on Linux, so this line is the\n // Windows job's to hold.\n failed(error as Error);\n }\n });\n}\n"]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A table with the types a const assertion gives it, where a mutation sweep
3
+ * reads it.
4
+ *
5
+ * Stryker makes no mutant of anything that stands under a TypeScript `as`,
6
+ * `as const` with the rest: it passes the whole expression over as a type.
7
+ * The tables that decide what each command refuses were written
8
+ * `{ ... } as const`, and no sweep had mutated a word of them (ADR-0010,
9
+ * amended 2026-10-12). An argument is an expression like any other, and a
10
+ * `const` type parameter reads it as the assertion did, each property
11
+ * `readonly` and each value its own literal type.
12
+ *
13
+ * So a table whose literal types something reads is written
14
+ * `literal({ ... })`, and never `{ ... } as const`.
15
+ * `scripts/mutation-unmeasured.mjs` counts what stands under an `as`, and a
16
+ * test holds the sources to its count.
17
+ */
18
+ export declare function literal<const T>(value: T): T;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A table with the types a const assertion gives it, where a mutation sweep
3
+ * reads it.
4
+ *
5
+ * Stryker makes no mutant of anything that stands under a TypeScript `as`,
6
+ * `as const` with the rest: it passes the whole expression over as a type.
7
+ * The tables that decide what each command refuses were written
8
+ * `{ ... } as const`, and no sweep had mutated a word of them (ADR-0010,
9
+ * amended 2026-10-12). An argument is an expression like any other, and a
10
+ * `const` type parameter reads it as the assertion did, each property
11
+ * `readonly` and each value its own literal type.
12
+ *
13
+ * So a table whose literal types something reads is written
14
+ * `literal({ ... })`, and never `{ ... } as const`.
15
+ * `scripts/mutation-unmeasured.mjs` counts what stands under an `as`, and a
16
+ * test holds the sources to its count.
17
+ */
18
+ export function literal(value) {
19
+ return value;
20
+ }
21
+ //# sourceMappingURL=literal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"literal.js","sourceRoot":"","sources":["../src/literal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,OAAO,CAAU,KAAQ;IACvC,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["/**\n * A table with the types a const assertion gives it, where a mutation sweep\n * reads it.\n *\n * Stryker makes no mutant of anything that stands under a TypeScript `as`,\n * `as const` with the rest: it passes the whole expression over as a type.\n * The tables that decide what each command refuses were written\n * `{ ... } as const`, and no sweep had mutated a word of them (ADR-0010,\n * amended 2026-10-12). An argument is an expression like any other, and a\n * `const` type parameter reads it as the assertion did, each property\n * `readonly` and each value its own literal type.\n *\n * So a table whose literal types something reads is written\n * `literal({ ... })`, and never `{ ... } as const`.\n * `scripts/mutation-unmeasured.mjs` counts what stands under an `as`, and a\n * test holds the sources to its count.\n */\nexport function literal<const T>(value: T): T {\n return value;\n}\n"]}
package/dist/probe.js CHANGED
@@ -126,7 +126,7 @@ export function readProbes(blocks) {
126
126
  const runs = count('runs');
127
127
  const timeout = count('timeout');
128
128
  if (typeof runs === 'string' || typeof timeout === 'string') {
129
- problems.push({ line: block.line, message: (typeof runs === 'string' ? runs : timeout) });
129
+ problems.push({ line: block.line, message: typeof runs === 'string' ? runs : timeout });
130
130
  continue;
131
131
  }
132
132
  if (probes.some((probe) => probe.id === id)) {
package/dist/probe.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"probe.js","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Probes: a defect, measured before anyone is sent to fix it.\n *\n * A brief that claims a defect proves it with a probe - a test that fails at\n * the base commit, for the reason the brief states, every time it runs - and\n * the round is done when the same probe passes at the head. The probe lives\n * in the brief, as fenced blocks, so the evidence and the claim are one\n * record:\n *\n * ````markdown\n * ```probe\n * id: old-token-still-accepted\n * run: npm test -- tests/probes/rotate.test.ts\n * signature: expected 401, got 200\n * ```\n *\n * ```probe-file tests/probes/rotate.test.ts\n * ...the test...\n * ```\n * ````\n *\n * What \"for the reason the brief states\" means is declared, never inferred\n * from prose: a `signature` the failing output must contain, or a `test` name\n * the JUnit report must show failing (ADR-0007). Without one, a probe could be\n * red because it did not compile, and a red that proves nothing is refused.\n */\n\nimport { createHash } from 'node:crypto';\n\nimport type { JUnitRead } from './junit.js';\n\nexport interface ProbeSpec {\n readonly id: string;\n /** The command, as a line a shell would run. */\n readonly run: string;\n /** A command to prepare a fresh checkout, such as `npm ci`. */\n readonly setup: string | null;\n /** Text the failing output, or the failing test's message, must contain. */\n readonly signature: string | null;\n /** A JUnit report the command writes, relative to the checkout. */\n readonly junit: string | null;\n /** The failing test's name, or part of it, in the JUnit report. */\n readonly test: string | null;\n readonly runs: number | null;\n readonly timeout: number | null;\n /** 1-based line of the probe block in the brief. */\n readonly line: number;\n}\n\nexport interface ProbeFile {\n readonly path: string;\n readonly content: string;\n readonly line: number;\n}\n\n/** A fenced block as the scanner found it. */\nexport interface CodeBlock {\n readonly info: string;\n readonly content: string;\n /** 1-based line of the opening fence. */\n readonly line: number;\n}\n\nexport interface ProbeSet {\n readonly probes: readonly ProbeSpec[];\n readonly files: readonly ProbeFile[];\n readonly problems: readonly { readonly line: number; readonly message: string }[];\n /** SHA-256 over every probe block and probe file, in order: what the evidence was measured with. */\n readonly hash: string;\n}\n\nconst KEYS = ['id', 'run', 'setup', 'signature', 'junit', 'test', 'runs', 'timeout'];\n\nfunction unquote(value: string): string {\n const trimmed = value.trim();\n if (trimmed.length >= 2 && (trimmed.startsWith('\"') || trimmed.startsWith(\"'\")) && trimmed.endsWith(trimmed.charAt(0))) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/**\n * A probe line without its trailing `# comment`. A `#` counts only after\n * whitespace and outside quotes, so `run: npm test -- -t \"issue #12\"` keeps\n * its argument; a quote counts only where a word starts, so the apostrophe in\n * `it's` opens nothing.\n */\nfunction withoutComment(raw: string): string {\n let quote: string | null = null;\n for (let i = 0; i < raw.length; i += 1) {\n const ch = raw.charAt(i);\n const after = i === 0 ? ' ' : raw.charAt(i - 1);\n if (quote !== null) {\n if (ch === quote) quote = null;\n } else if ((ch === '\"' || ch === \"'\") && /[\\s:=]/.test(after)) {\n quote = ch;\n } else if (ch === '#' && /\\s/.test(after)) {\n return raw.slice(0, i);\n }\n }\n return raw;\n}\n\n/** Reads the probes and probe files among a brief's fenced blocks. */\nexport function readProbes(blocks: readonly CodeBlock[]): ProbeSet {\n const probes: ProbeSpec[] = [];\n const files: ProbeFile[] = [];\n const problems: { line: number; message: string }[] = [];\n const hash = createHash('sha256');\n for (const block of blocks) {\n const [word, ...rest] = block.info.trim().split(/\\s+/);\n if (word === 'probe-file') {\n const path = rest.join(' ').trim();\n // A backslash is a separator where the probe is written to disk on Windows.\n if (path === '' || /^[\\\\/]/.test(path) || path.split(/[\\\\/]/).includes('..') || /^[A-Za-z]:/.test(path)) {\n problems.push({ line: block.line, message: `a probe-file names \"${path}\"; it must be a path inside the repository` });\n continue;\n }\n files.push({ path, content: block.content, line: block.line });\n hash.update(`probe-file ${path}\\n${block.content}\\n`);\n continue;\n }\n if (word !== 'probe') continue;\n hash.update(`probe\\n${block.content}\\n`);\n const fields = new Map<string, string>();\n let bad = false;\n for (const raw of block.content.split(/\\r?\\n/)) {\n const line = withoutComment(raw).trim();\n if (line === '' || line.startsWith('#')) continue;\n const colon = line.indexOf(':');\n const key = colon < 0 ? '' : line.slice(0, colon).trim();\n if (!KEYS.includes(key)) {\n problems.push({ line: block.line, message: `a probe says \"${line}\"; its keys are ${KEYS.join(', ')}` });\n bad = true;\n continue;\n }\n fields.set(key, unquote(line.slice(colon + 1)));\n }\n if (bad) continue;\n const id = fields.get('id') ?? '';\n const run = fields.get('run') ?? '';\n if (id === '' || run === '') {\n problems.push({ line: block.line, message: 'a probe needs an id and a run command' });\n continue;\n }\n const signature = fields.get('signature') || null;\n const test = fields.get('test') || null;\n const junit = fields.get('junit') || null;\n if (signature === null && test === null) {\n problems.push({\n line: block.line,\n message: `probe ${id} does not say how it fails; give a signature, or a JUnit test name, so that a failure to compile is not taken for the defect`,\n });\n continue;\n }\n if (test !== null && junit === null) {\n problems.push({ line: block.line, message: `probe ${id} names a test but no junit report to find it in` });\n continue;\n }\n const count = (key: string): number | null | string => {\n const value = fields.get(key);\n if (value === undefined) return null;\n const n = Number(value);\n return Number.isInteger(n) && n >= 1 ? n : `probe ${id}: \"${key}\" must be a whole number of at least 1`;\n };\n const runs = count('runs');\n const timeout = count('timeout');\n if (typeof runs === 'string' || typeof timeout === 'string') {\n problems.push({ line: block.line, message: (typeof runs === 'string' ? runs : timeout) as string });\n continue;\n }\n if (probes.some((probe) => probe.id === id)) {\n problems.push({ line: block.line, message: `probe ${id} is declared twice` });\n continue;\n }\n probes.push({ id, run, setup: fields.get('setup') || null, signature, junit, test, runs, timeout, line: block.line });\n }\n return { probes, files, problems, hash: `sha256-${hash.digest('hex')}` };\n}\n\n/** What one run of a probe did. `exitCode` is `null` when it was stopped at its timeout. */\nexport interface ProbeRun {\n readonly exitCode: number | null;\n readonly output: string;\n /** The report when the probe names one: read, unreadable, or `null` when the file was not written. */\n readonly junit: JUnitRead | null;\n}\n\nexport type RunOutcome = 'red' | 'green' | 'wrong-failure' | 'timeout' | 'no-report';\n\nexport interface Classified {\n readonly outcome: RunOutcome;\n /** The evidence: the line or the message that matched, or what was missing. */\n readonly detail: string;\n /**\n * The end of what the command printed, as {@link endOfOutput} gives it, on\n * a run that proves nothing: it failed for another reason, was stopped at\n * its timeout, or left no report that can be read. `detail` says only that\n * the reason was not the probe's; the reason itself is the command's to\n * give, and this is where it gave it. A red or a green run has none.\n */\n readonly output?: string;\n}\n\n/**\n * npm's `yes` setting as an environment spells it. npm reads the name in any\n * case: `NPM_CONFIG_YES` and `Npm_Config_Yes` did what `npm_config_yes` does\n * under npm 10.9.9, 11.16.0, 11.20.0 and 12.2.0 (ADR-0007).\n */\nconst NPM_YES = /^npm_config_yes$/i;\n\n/**\n * The environment a probe's commands run in: the person's own, with `CI` set\n * as a pipeline sets it, and with npm told to fetch nothing for a command\n * unless the person's environment already says what npm may fetch.\n *\n * A probe's command runs where nothing is installed until its `setup`\n * installs it, with no terminal to be asked on. There `npx <name>` fetches\n * the registry's package of that name and runs it, unasked, and with\n * `npm_config_yes=false` it stops and names the package instead (ADR-0007).\n * A person who set the variable, in any case, has said what they want, and\n * it is left as they set it. Set and empty says nothing: npm fetched with it\n * as with no variable, so the setting is made. The empty one is taken out\n * first and not left beside it: on Windows a name is one variable whatever\n * its case, and of two spellings a command is handed one, the upper case\n * before the lower, which would be the empty one.\n */\nexport function probeEnvironment(own: Readonly<Record<string, string | undefined>>): Record<string, string | undefined> {\n const spelled = Object.keys(own).filter((name) => NPM_YES.test(name));\n if (spelled.some((name) => Boolean(own[name]))) return { ...own, CI: '1' };\n const rest = Object.fromEntries(Object.entries(own).filter(([name]) => !spelled.includes(name)));\n return { ...rest, CI: '1', npm_config_yes: 'false' };\n}\n\n/** How many characters of the end of a command's output a person is shown. */\nconst SHOWN = 2000;\n\n/**\n * The end of what a command printed, as a person is shown it: the last 2,000\n * characters, less what is there for a terminal to obey rather than for a\n * person to read. Written out as it came, an escape sequence in it moves the\n * cursor, clears lines already written or renames the window.\n *\n * - A carriage return ends a line as a line feed does, alone or in front of\n * one: alone it would have the next line written over the last.\n * - A colour is dropped. A command told to colour what it prints, as\n * `npm_config_color=always` tells npm, does so with nobody at a terminal\n * to see it, and a colour says nothing in a report.\n * - Every other control character but the line feed and the tab is written\n * as its escape, as JSON writes it, so that it shows and does nothing.\n *\n * Space at the end is dropped before the 2,000 are counted, so that blank\n * lines do not take the place of the reason above them. Blank lines at the\n * start of what is left are dropped after: `npm run` opens with one.\n */\nexport function endOfOutput(output: string): string {\n return output\n .replace(/\\r\\n?/g, '\\n')\n .replace(/\\u001b\\[[\\d;]*m/g, '')\n .replace(/[^\\P{Cc}\\n\\t]/gu, (control) => `\\\\u${control.charCodeAt(0).toString(16).padStart(4, '0')}`)\n .trimEnd()\n .slice(-SHOWN)\n .replace(/^\\n+/, '');\n}\n\nfunction firstLineWith(text: string, needle: string): string {\n const line = text.split(/\\r?\\n/).find((candidate) => candidate.includes(needle));\n return (line ?? needle).trim().slice(0, 200);\n}\n\n/**\n * What one run showed, against what the probe declares, with the end of the\n * command's output on a run that is neither red nor green.\n */\nexport function classify(spec: ProbeSpec, run: ProbeRun): Classified {\n const judged = judge(spec, run);\n return judged.outcome === 'red' || judged.outcome === 'green' ? judged : { ...judged, output: endOfOutput(run.output) };\n}\n\nfunction judge(spec: ProbeSpec, run: ProbeRun): Classified {\n if (run.exitCode === null) return { outcome: 'timeout', detail: `stopped after ${spec.timeout ?? 'the configured'} seconds` };\n if (spec.junit !== null) {\n if (run.junit === null) {\n return run.exitCode === 0\n ? { outcome: 'no-report', detail: `the command passed and wrote no ${spec.junit}` }\n : { outcome: 'wrong-failure', detail: `the command failed before writing ${spec.junit}` };\n }\n if (!run.junit.ok) return { outcome: 'no-report', detail: `${spec.junit} cannot be read: ${run.junit.error}` };\n const failing = run.junit.cases.filter((c) => c.outcome === 'failed' || c.outcome === 'errored');\n const matching = failing.filter(\n (c) =>\n (spec.test === null || `${c.classname} ${c.name}`.includes(spec.test)) &&\n (spec.signature === null || c.message.includes(spec.signature) || c.name.includes(spec.signature)),\n );\n const first = matching[0];\n if (first !== undefined) {\n return { outcome: 'red', detail: `${first.name} failed${spec.signature === null ? '' : `: ${firstLineWith(first.message, spec.signature)}`}` };\n }\n if (failing.length === 0 && run.exitCode === 0) return { outcome: 'green', detail: 'every test passed' };\n const other = failing[0];\n return {\n outcome: 'wrong-failure',\n detail: other === undefined ? `the command exited ${run.exitCode} with no failing test` : `${other.name} failed, which is not what the probe declares`,\n };\n }\n if (run.exitCode === 0) return { outcome: 'green', detail: 'the command passed' };\n const signature = spec.signature as string;\n return run.output.includes(signature)\n ? { outcome: 'red', detail: firstLineWith(run.output, signature) }\n : { outcome: 'wrong-failure', detail: `the command exited ${run.exitCode} and its output does not contain \"${signature}\"` };\n}\n\nexport type Verdict =\n /** Red at every run where red was expected: the defect is measured. */\n | 'measured'\n /** Green at every run where green was expected: the defect is gone. */\n | 'fixed'\n /** Green where red was expected: the probe finds no defect. */\n | 'vacuous'\n /** Red where green was expected: the round did not fix it. */\n | 'still-failing'\n /** Runs disagreed with each other. */\n | 'flaky'\n /** It failed for another reason, timed out, or wrote no report: it proves nothing. */\n | 'invalid';\n\n/** The verdict over every run of one probe, at the base (`red` expected) or the head (`green`). */\nexport function verdictOf(expected: 'red' | 'green', runs: readonly Classified[]): Verdict {\n const outcomes = new Set(runs.map((run) => run.outcome));\n if (outcomes.size > 1) return 'flaky';\n const only = runs[0]?.outcome;\n if (only === 'red') return expected === 'red' ? 'measured' : 'still-failing';\n if (only === 'green') return expected === 'green' ? 'fixed' : 'vacuous';\n return 'invalid';\n}\n\nexport interface ProbeResult {\n readonly probe: ProbeSpec;\n readonly at: string;\n readonly commit: string;\n readonly expected: 'red' | 'green';\n readonly runs: readonly Classified[];\n readonly verdict: Verdict;\n}\n\nfunction cell(text: string): string {\n return text.replace(/\\|/g, '\\\\|').replace(/\\r?\\n/g, ' ').trim();\n}\n\n/** The evidence a brief records: a table a reader can check, and what it was measured with. */\nexport function renderEvidence(results: readonly ProbeResult[], hash: string, date: string): string {\n const lines = ['| Probe | At | Runs | Verdict | Evidence |', '| --- | --- | ---: | --- | --- |'];\n for (const result of results) {\n const agreeing = result.runs.filter((run) => run.outcome === result.runs[0]?.outcome).length;\n lines.push(\n `| ${cell(result.probe.id)} | ${result.at} \\`${result.commit.slice(0, 12)}\\` | ${agreeing}/${result.runs.length} | ${result.verdict} | ${cell(result.runs[0]?.detail ?? '')} |`,\n );\n }\n lines.push('', `Measured ${date} by spec-harness with probes \\`${hash}\\`.`);\n return lines.join('\\n');\n}\n\n/**\n * What a person is told, beside the table, of each result with a run that\n * proves nothing: which run, what it showed, and the end of what its command\n * printed, where the reason is. The first such run of a result speaks for the\n * rest, as the first run's evidence fills the table's cell. A result whose\n * runs are all red or green has nothing to add to its verdict.\n *\n * It is for the standard error: the table is what a brief records and what a\n * script reads, and a command's output is no part of either.\n */\nexport function renderUnexplained(results: readonly ProbeResult[]): string {\n return results\n .map((result) => {\n const at = result.runs.findIndex((run) => run.output !== undefined);\n const run = result.runs[at];\n if (run?.output === undefined) return '';\n const printed = run.output === '' ? 'it printed nothing' : `its output ended:\\n${run.output}`;\n return `spec-harness: probe ${result.probe.id} is ${result.verdict} at ${result.at}: run ${at + 1} of ${result.runs.length}: ${run.detail}; ${printed}\\n`;\n })\n .join('');\n}\n"]}
1
+ {"version":3,"file":"probe.js","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Probes: a defect, measured before anyone is sent to fix it.\n *\n * A brief that claims a defect proves it with a probe - a test that fails at\n * the base commit, for the reason the brief states, every time it runs - and\n * the round is done when the same probe passes at the head. The probe lives\n * in the brief, as fenced blocks, so the evidence and the claim are one\n * record:\n *\n * ````markdown\n * ```probe\n * id: old-token-still-accepted\n * run: npm test -- tests/probes/rotate.test.ts\n * signature: expected 401, got 200\n * ```\n *\n * ```probe-file tests/probes/rotate.test.ts\n * ...the test...\n * ```\n * ````\n *\n * What \"for the reason the brief states\" means is declared, never inferred\n * from prose: a `signature` the failing output must contain, or a `test` name\n * the JUnit report must show failing (ADR-0007). Without one, a probe could be\n * red because it did not compile, and a red that proves nothing is refused.\n */\n\nimport { createHash } from 'node:crypto';\n\nimport type { JUnitRead } from './junit.js';\n\nexport interface ProbeSpec {\n readonly id: string;\n /** The command, as a line a shell would run. */\n readonly run: string;\n /** A command to prepare a fresh checkout, such as `npm ci`. */\n readonly setup: string | null;\n /** Text the failing output, or the failing test's message, must contain. */\n readonly signature: string | null;\n /** A JUnit report the command writes, relative to the checkout. */\n readonly junit: string | null;\n /** The failing test's name, or part of it, in the JUnit report. */\n readonly test: string | null;\n readonly runs: number | null;\n readonly timeout: number | null;\n /** 1-based line of the probe block in the brief. */\n readonly line: number;\n}\n\nexport interface ProbeFile {\n readonly path: string;\n readonly content: string;\n readonly line: number;\n}\n\n/** A fenced block as the scanner found it. */\nexport interface CodeBlock {\n readonly info: string;\n readonly content: string;\n /** 1-based line of the opening fence. */\n readonly line: number;\n}\n\nexport interface ProbeSet {\n readonly probes: readonly ProbeSpec[];\n readonly files: readonly ProbeFile[];\n readonly problems: readonly { readonly line: number; readonly message: string }[];\n /** SHA-256 over every probe block and probe file, in order: what the evidence was measured with. */\n readonly hash: string;\n}\n\nconst KEYS = ['id', 'run', 'setup', 'signature', 'junit', 'test', 'runs', 'timeout'];\n\nfunction unquote(value: string): string {\n const trimmed = value.trim();\n if (trimmed.length >= 2 && (trimmed.startsWith('\"') || trimmed.startsWith(\"'\")) && trimmed.endsWith(trimmed.charAt(0))) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/**\n * A probe line without its trailing `# comment`. A `#` counts only after\n * whitespace and outside quotes, so `run: npm test -- -t \"issue #12\"` keeps\n * its argument; a quote counts only where a word starts, so the apostrophe in\n * `it's` opens nothing.\n */\nfunction withoutComment(raw: string): string {\n let quote: string | null = null;\n for (let i = 0; i < raw.length; i += 1) {\n const ch = raw.charAt(i);\n const after = i === 0 ? ' ' : raw.charAt(i - 1);\n if (quote !== null) {\n if (ch === quote) quote = null;\n } else if ((ch === '\"' || ch === \"'\") && /[\\s:=]/.test(after)) {\n quote = ch;\n } else if (ch === '#' && /\\s/.test(after)) {\n return raw.slice(0, i);\n }\n }\n return raw;\n}\n\n/** Reads the probes and probe files among a brief's fenced blocks. */\nexport function readProbes(blocks: readonly CodeBlock[]): ProbeSet {\n const probes: ProbeSpec[] = [];\n const files: ProbeFile[] = [];\n const problems: { line: number; message: string }[] = [];\n const hash = createHash('sha256');\n for (const block of blocks) {\n const [word, ...rest] = block.info.trim().split(/\\s+/);\n if (word === 'probe-file') {\n const path = rest.join(' ').trim();\n // A backslash is a separator where the probe is written to disk on Windows.\n if (path === '' || /^[\\\\/]/.test(path) || path.split(/[\\\\/]/).includes('..') || /^[A-Za-z]:/.test(path)) {\n problems.push({ line: block.line, message: `a probe-file names \"${path}\"; it must be a path inside the repository` });\n continue;\n }\n files.push({ path, content: block.content, line: block.line });\n hash.update(`probe-file ${path}\\n${block.content}\\n`);\n continue;\n }\n if (word !== 'probe') continue;\n hash.update(`probe\\n${block.content}\\n`);\n const fields = new Map<string, string>();\n let bad = false;\n for (const raw of block.content.split(/\\r?\\n/)) {\n const line = withoutComment(raw).trim();\n if (line === '' || line.startsWith('#')) continue;\n const colon = line.indexOf(':');\n const key = colon < 0 ? '' : line.slice(0, colon).trim();\n if (!KEYS.includes(key)) {\n problems.push({ line: block.line, message: `a probe says \"${line}\"; its keys are ${KEYS.join(', ')}` });\n bad = true;\n continue;\n }\n fields.set(key, unquote(line.slice(colon + 1)));\n }\n if (bad) continue;\n const id = fields.get('id') ?? '';\n const run = fields.get('run') ?? '';\n if (id === '' || run === '') {\n problems.push({ line: block.line, message: 'a probe needs an id and a run command' });\n continue;\n }\n const signature = fields.get('signature') || null;\n const test = fields.get('test') || null;\n const junit = fields.get('junit') || null;\n if (signature === null && test === null) {\n problems.push({\n line: block.line,\n message: `probe ${id} does not say how it fails; give a signature, or a JUnit test name, so that a failure to compile is not taken for the defect`,\n });\n continue;\n }\n if (test !== null && junit === null) {\n problems.push({ line: block.line, message: `probe ${id} names a test but no junit report to find it in` });\n continue;\n }\n const count = (key: string): number | null | string => {\n const value = fields.get(key);\n if (value === undefined) return null;\n const n = Number(value);\n return Number.isInteger(n) && n >= 1 ? n : `probe ${id}: \"${key}\" must be a whole number of at least 1`;\n };\n const runs = count('runs');\n const timeout = count('timeout');\n if (typeof runs === 'string' || typeof timeout === 'string') {\n problems.push({ line: block.line, message: typeof runs === 'string' ? runs : (timeout as string) });\n continue;\n }\n if (probes.some((probe) => probe.id === id)) {\n problems.push({ line: block.line, message: `probe ${id} is declared twice` });\n continue;\n }\n probes.push({ id, run, setup: fields.get('setup') || null, signature, junit, test, runs, timeout, line: block.line });\n }\n return { probes, files, problems, hash: `sha256-${hash.digest('hex')}` };\n}\n\n/** What one run of a probe did. `exitCode` is `null` when it was stopped at its timeout. */\nexport interface ProbeRun {\n readonly exitCode: number | null;\n readonly output: string;\n /** The report when the probe names one: read, unreadable, or `null` when the file was not written. */\n readonly junit: JUnitRead | null;\n}\n\nexport type RunOutcome = 'red' | 'green' | 'wrong-failure' | 'timeout' | 'no-report';\n\nexport interface Classified {\n readonly outcome: RunOutcome;\n /** The evidence: the line or the message that matched, or what was missing. */\n readonly detail: string;\n /**\n * The end of what the command printed, as {@link endOfOutput} gives it, on\n * a run that proves nothing: it failed for another reason, was stopped at\n * its timeout, or left no report that can be read. `detail` says only that\n * the reason was not the probe's; the reason itself is the command's to\n * give, and this is where it gave it. A red or a green run has none.\n */\n readonly output?: string;\n}\n\n/**\n * npm's `yes` setting as an environment spells it. npm reads the name in any\n * case: `NPM_CONFIG_YES` and `Npm_Config_Yes` did what `npm_config_yes` does\n * under npm 10.9.9, 11.16.0, 11.20.0 and 12.2.0 (ADR-0007).\n */\nconst NPM_YES = /^npm_config_yes$/i;\n\n/**\n * The environment a probe's commands run in: the person's own, with `CI` set\n * as a pipeline sets it, and with npm told to fetch nothing for a command\n * unless the person's environment already says what npm may fetch.\n *\n * A probe's command runs where nothing is installed until its `setup`\n * installs it, with no terminal to be asked on. There `npx <name>` fetches\n * the registry's package of that name and runs it, unasked, and with\n * `npm_config_yes=false` it stops and names the package instead (ADR-0007).\n * A person who set the variable, in any case, has said what they want, and\n * it is left as they set it. Set and empty says nothing: npm fetched with it\n * as with no variable, so the setting is made. The empty one is taken out\n * first and not left beside it: on Windows a name is one variable whatever\n * its case, and of two spellings a command is handed one, the upper case\n * before the lower, which would be the empty one.\n */\nexport function probeEnvironment(own: Readonly<Record<string, string | undefined>>): Record<string, string | undefined> {\n const spelled = Object.keys(own).filter((name) => NPM_YES.test(name));\n if (spelled.some((name) => Boolean(own[name]))) return { ...own, CI: '1' };\n const rest = Object.fromEntries(Object.entries(own).filter(([name]) => !spelled.includes(name)));\n return { ...rest, CI: '1', npm_config_yes: 'false' };\n}\n\n/** How many characters of the end of a command's output a person is shown. */\nconst SHOWN = 2000;\n\n/**\n * The end of what a command printed, as a person is shown it: the last 2,000\n * characters, less what is there for a terminal to obey rather than for a\n * person to read. Written out as it came, an escape sequence in it moves the\n * cursor, clears lines already written or renames the window.\n *\n * - A carriage return ends a line as a line feed does, alone or in front of\n * one: alone it would have the next line written over the last.\n * - A colour is dropped. A command told to colour what it prints, as\n * `npm_config_color=always` tells npm, does so with nobody at a terminal\n * to see it, and a colour says nothing in a report.\n * - Every other control character but the line feed and the tab is written\n * as its escape, as JSON writes it, so that it shows and does nothing.\n *\n * Space at the end is dropped before the 2,000 are counted, so that blank\n * lines do not take the place of the reason above them. Blank lines at the\n * start of what is left are dropped after: `npm run` opens with one.\n */\nexport function endOfOutput(output: string): string {\n return output\n .replace(/\\r\\n?/g, '\\n')\n .replace(/\\u001b\\[[\\d;]*m/g, '')\n .replace(/[^\\P{Cc}\\n\\t]/gu, (control) => `\\\\u${control.charCodeAt(0).toString(16).padStart(4, '0')}`)\n .trimEnd()\n .slice(-SHOWN)\n .replace(/^\\n+/, '');\n}\n\nfunction firstLineWith(text: string, needle: string): string {\n const line = text.split(/\\r?\\n/).find((candidate) => candidate.includes(needle));\n return (line ?? needle).trim().slice(0, 200);\n}\n\n/**\n * What one run showed, against what the probe declares, with the end of the\n * command's output on a run that is neither red nor green.\n */\nexport function classify(spec: ProbeSpec, run: ProbeRun): Classified {\n const judged = judge(spec, run);\n return judged.outcome === 'red' || judged.outcome === 'green' ? judged : { ...judged, output: endOfOutput(run.output) };\n}\n\nfunction judge(spec: ProbeSpec, run: ProbeRun): Classified {\n if (run.exitCode === null) return { outcome: 'timeout', detail: `stopped after ${spec.timeout ?? 'the configured'} seconds` };\n if (spec.junit !== null) {\n if (run.junit === null) {\n return run.exitCode === 0\n ? { outcome: 'no-report', detail: `the command passed and wrote no ${spec.junit}` }\n : { outcome: 'wrong-failure', detail: `the command failed before writing ${spec.junit}` };\n }\n if (!run.junit.ok) return { outcome: 'no-report', detail: `${spec.junit} cannot be read: ${run.junit.error}` };\n const failing = run.junit.cases.filter((c) => c.outcome === 'failed' || c.outcome === 'errored');\n const matching = failing.filter(\n (c) =>\n (spec.test === null || `${c.classname} ${c.name}`.includes(spec.test)) &&\n (spec.signature === null || c.message.includes(spec.signature) || c.name.includes(spec.signature)),\n );\n const first = matching[0];\n if (first !== undefined) {\n return { outcome: 'red', detail: `${first.name} failed${spec.signature === null ? '' : `: ${firstLineWith(first.message, spec.signature)}`}` };\n }\n if (failing.length === 0 && run.exitCode === 0) return { outcome: 'green', detail: 'every test passed' };\n const other = failing[0];\n return {\n outcome: 'wrong-failure',\n detail: other === undefined ? `the command exited ${run.exitCode} with no failing test` : `${other.name} failed, which is not what the probe declares`,\n };\n }\n if (run.exitCode === 0) return { outcome: 'green', detail: 'the command passed' };\n const signature = spec.signature as string;\n return run.output.includes(signature)\n ? { outcome: 'red', detail: firstLineWith(run.output, signature) }\n : { outcome: 'wrong-failure', detail: `the command exited ${run.exitCode} and its output does not contain \"${signature}\"` };\n}\n\nexport type Verdict =\n /** Red at every run where red was expected: the defect is measured. */\n | 'measured'\n /** Green at every run where green was expected: the defect is gone. */\n | 'fixed'\n /** Green where red was expected: the probe finds no defect. */\n | 'vacuous'\n /** Red where green was expected: the round did not fix it. */\n | 'still-failing'\n /** Runs disagreed with each other. */\n | 'flaky'\n /** It failed for another reason, timed out, or wrote no report: it proves nothing. */\n | 'invalid';\n\n/** The verdict over every run of one probe, at the base (`red` expected) or the head (`green`). */\nexport function verdictOf(expected: 'red' | 'green', runs: readonly Classified[]): Verdict {\n const outcomes = new Set(runs.map((run) => run.outcome));\n if (outcomes.size > 1) return 'flaky';\n const only = runs[0]?.outcome;\n if (only === 'red') return expected === 'red' ? 'measured' : 'still-failing';\n if (only === 'green') return expected === 'green' ? 'fixed' : 'vacuous';\n return 'invalid';\n}\n\nexport interface ProbeResult {\n readonly probe: ProbeSpec;\n readonly at: string;\n readonly commit: string;\n readonly expected: 'red' | 'green';\n readonly runs: readonly Classified[];\n readonly verdict: Verdict;\n}\n\nfunction cell(text: string): string {\n return text.replace(/\\|/g, '\\\\|').replace(/\\r?\\n/g, ' ').trim();\n}\n\n/** The evidence a brief records: a table a reader can check, and what it was measured with. */\nexport function renderEvidence(results: readonly ProbeResult[], hash: string, date: string): string {\n const lines = ['| Probe | At | Runs | Verdict | Evidence |', '| --- | --- | ---: | --- | --- |'];\n for (const result of results) {\n const agreeing = result.runs.filter((run) => run.outcome === result.runs[0]?.outcome).length;\n lines.push(\n `| ${cell(result.probe.id)} | ${result.at} \\`${result.commit.slice(0, 12)}\\` | ${agreeing}/${result.runs.length} | ${result.verdict} | ${cell(result.runs[0]?.detail ?? '')} |`,\n );\n }\n lines.push('', `Measured ${date} by spec-harness with probes \\`${hash}\\`.`);\n return lines.join('\\n');\n}\n\n/**\n * What a person is told, beside the table, of each result with a run that\n * proves nothing: which run, what it showed, and the end of what its command\n * printed, where the reason is. The first such run of a result speaks for the\n * rest, as the first run's evidence fills the table's cell. A result whose\n * runs are all red or green has nothing to add to its verdict.\n *\n * It is for the standard error: the table is what a brief records and what a\n * script reads, and a command's output is no part of either.\n */\nexport function renderUnexplained(results: readonly ProbeResult[]): string {\n return results\n .map((result) => {\n const at = result.runs.findIndex((run) => run.output !== undefined);\n const run = result.runs[at];\n if (run?.output === undefined) return '';\n const printed = run.output === '' ? 'it printed nothing' : `its output ended:\\n${run.output}`;\n return `spec-harness: probe ${result.probe.id} is ${result.verdict} at ${result.at}: run ${at + 1} of ${result.runs.length}: ${run.detail}; ${printed}\\n`;\n })\n .join('');\n}\n"]}
package/dist/reader.js CHANGED
@@ -49,6 +49,8 @@ export function createReader() {
49
49
  return {
50
50
  codeBlocks(text) {
51
51
  const scanned = scan(text);
52
+ // No space is a match too, so there is one at the start of every line:
53
+ // the match is never null, and the anchor is equivalent to its mutant.
52
54
  const spaces = (line) => /^ */.exec(line)[0].length;
53
55
  return scanned.blocks
54
56
  .filter((block) => block.kind === 'fenced')
@@ -1 +1 @@
1
- {"version":3,"file":"reader.js","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The document reader, on spec-core's Markdown scanner: nothing inside code\n * or a comment is ever taken for a section, a table, a link or a probe.\n */\n\nimport type { Citation, DocumentReader, SectionTables, UnreadableFrontMatterLine } from './document.js';\nimport type { CodeBlock } from './probe.js';\nimport type { TableView } from './rulings.js';\nimport { findEntry, readFrontMatter, scanMarkdown, type FrontMatter, type MarkdownScan } from './vendor/spec-core/markdown/index.js';\nimport { splitLines } from './vendor/spec-core/text/index.js';\n\n/** Section names compare without case, emphasis, a leading number or a trailing colon. */\nexport function sameSection(heading: string, name: string): boolean {\n // Both names are folded the same way and then compared, so the mutant that\n // folds them to upper case is equivalent.\n const normal = (text: string): string =>\n text\n .replace(/[*_`]/g, '')\n .replace(/^\\s*\\d+[.)]?\\s+/, '')\n .replace(/[:\\s]+$/, '')\n .trim()\n .toLowerCase();\n return normal(heading) === normal(name);\n}\n\n/**\n * The lines of closed YAML front matter spec-core's reader passes over, since\n * none is `key: value`, each with its reason. The reader reports a problem on\n * a line it read too - a key declared twice - and one on the opening line for\n * TOML front matter; neither is a line passed over, and only these are. A\n * byte-order mark, which the reader leaves out, sits on the opening line,\n * never one of these, so the text's own lines give each as written.\n */\nfunction unreadLines(text: string, front: FrontMatter | null): UnreadableFrontMatterLine[] {\n if (front === null) return [];\n const read = new Set(front.entries.map((entry) => entry.line));\n const lines = splitLines(text);\n return front.problems\n .filter((problem) => problem.line > 0 && !read.has(problem.line))\n .map((problem) => ({ line: problem.line + 1, text: lines[problem.line] as string, reason: problem.message }));\n}\n\nfunction scanOf(text: string, cache: { text: string; scan: MarkdownScan } | null): MarkdownScan {\n return cache !== null && cache.text === text ? cache.scan : scanMarkdown(text);\n}\n\nexport function createReader(): DocumentReader {\n // Commands read the same brief several times in a row; one scan serves them.\n // It spares a scan and changes no answer, so the mutant that keeps nothing\n // is equivalent.\n let last: { text: string; scan: MarkdownScan } | null = null;\n const scan = (text: string): MarkdownScan => {\n const result = scanOf(text, last);\n last = { text, scan: result };\n return result;\n };\n\n return {\n codeBlocks(text: string): CodeBlock[] {\n const scanned = scan(text);\n const spaces = (line: string): number => (/^ */.exec(line) as RegExpExecArray)[0].length;\n return scanned.blocks\n .filter((block) => block.kind === 'fenced')\n .map((block) => {\n const first = block.line + 1;\n const last = block.closed ? block.endLine - 1 : block.endLine;\n // A fence in a block quote or a list item: its content is the code\n // without the quote markers and without the fence's own indent, as\n // CommonMark reads it, so a probe file is written as it was meant.\n const indent = spaces(scanned.lines[block.line - 1]?.content ?? '');\n const lines: string[] = [];\n for (let line = first; line <= last; line += 1) {\n const content = scanned.lines[line - 1]?.content ?? '';\n lines.push(content.slice(Math.min(indent, spaces(content))));\n }\n return { info: block.info, content: lines.join('\\n'), line: block.line };\n });\n },\n\n sectionAt(text: string, line: number): string | null {\n let found: string | null = null;\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level >= 2) found = heading.text;\n }\n return found;\n },\n\n sectionsAt(text: string, line: number): string[] {\n const open: { level: number; text: string }[] = [];\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level < 2) continue;\n while (open.length > 0 && (open[open.length - 1] as { level: number }).level >= heading.level) open.pop();\n open.push({ level: heading.level, text: heading.text });\n }\n return open.map((section) => section.text);\n },\n\n sectionTables(text: string, section: string): SectionTables {\n const scanned = scan(text);\n const headings = scanned.headings;\n const index = headings.findIndex((heading) => heading.level >= 2 && sameSection(heading.text, section));\n if (index < 0) return { tables: [], headingLine: null, tableEndLine: null };\n const heading = headings[index] as (typeof headings)[number];\n const next = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level);\n const endLine = next === undefined ? scanned.index.lineCount + 1 : next.line;\n const tables = scanned.tables.filter((table) => table.line > heading.endLine && table.line < endLine);\n const views: TableView[] = tables.map((table) => ({\n headers: table.headers.map((cell) => cell.text),\n rows: table.rows.map((row) => ({ cells: row.cells.map((cell) => cell.text), line: row.line })),\n }));\n const lastTable = tables[tables.length - 1];\n return { tables: views, headingLine: heading.endLine, tableEndLine: lastTable === undefined ? null : lastTable.endLine };\n },\n\n citations(text: string): Citation[] {\n return scan(text)\n .links.filter((link) => !link.image && link.form !== 'autolink' && link.form !== 'definition')\n .map((link) => ({ target: link.target, line: link.line }));\n },\n\n titleAndStatus(text: string): {\n title: string | null;\n status: string | null;\n unclosedFrontMatter: boolean;\n unreadableFrontMatter: UnreadableFrontMatterLine[];\n } {\n const scanned = scan(text);\n const title = scanned.headings.find((heading) => heading.level === 1)?.text ?? null;\n const front = readFrontMatter(text);\n let status: string | null = null;\n // The status is read under `status` alone: a key in another language,\n // such as `狀態`, is another key (ADR-0001).\n const entry = findEntry(front, 'status');\n if (entry !== undefined && entry.value.kind === 'scalar') status = entry.value.scalar.text || null;\n // TOML front matter gives no status, closed or not, so closing it would\n // change nothing the context packet shows; only YAML's is worth saying.\n return {\n title,\n status,\n unclosedFrontMatter: scanned.unclosedFrontMatter?.kind === 'yaml',\n unreadableFrontMatter: unreadLines(text, front),\n };\n },\n };\n}\n"]}
1
+ {"version":3,"file":"reader.js","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The document reader, on spec-core's Markdown scanner: nothing inside code\n * or a comment is ever taken for a section, a table, a link or a probe.\n */\n\nimport type { Citation, DocumentReader, SectionTables, UnreadableFrontMatterLine } from './document.js';\nimport type { CodeBlock } from './probe.js';\nimport type { TableView } from './rulings.js';\nimport { findEntry, readFrontMatter, scanMarkdown, type FrontMatter, type MarkdownScan } from './vendor/spec-core/markdown/index.js';\nimport { splitLines } from './vendor/spec-core/text/index.js';\n\n/** Section names compare without case, emphasis, a leading number or a trailing colon. */\nexport function sameSection(heading: string, name: string): boolean {\n // Both names are folded the same way and then compared, so the mutant that\n // folds them to upper case is equivalent.\n const normal = (text: string): string =>\n text\n .replace(/[*_`]/g, '')\n .replace(/^\\s*\\d+[.)]?\\s+/, '')\n .replace(/[:\\s]+$/, '')\n .trim()\n .toLowerCase();\n return normal(heading) === normal(name);\n}\n\n/**\n * The lines of closed YAML front matter spec-core's reader passes over, since\n * none is `key: value`, each with its reason. The reader reports a problem on\n * a line it read too - a key declared twice - and one on the opening line for\n * TOML front matter; neither is a line passed over, and only these are. A\n * byte-order mark, which the reader leaves out, sits on the opening line,\n * never one of these, so the text's own lines give each as written.\n */\nfunction unreadLines(text: string, front: FrontMatter | null): UnreadableFrontMatterLine[] {\n if (front === null) return [];\n const read = new Set(front.entries.map((entry) => entry.line));\n const lines = splitLines(text);\n return front.problems\n .filter((problem) => problem.line > 0 && !read.has(problem.line))\n .map((problem) => ({ line: problem.line + 1, text: lines[problem.line] as string, reason: problem.message }));\n}\n\nfunction scanOf(text: string, cache: { text: string; scan: MarkdownScan } | null): MarkdownScan {\n return cache !== null && cache.text === text ? cache.scan : scanMarkdown(text);\n}\n\nexport function createReader(): DocumentReader {\n // Commands read the same brief several times in a row; one scan serves them.\n // It spares a scan and changes no answer, so the mutant that keeps nothing\n // is equivalent.\n let last: { text: string; scan: MarkdownScan } | null = null;\n const scan = (text: string): MarkdownScan => {\n const result = scanOf(text, last);\n last = { text, scan: result };\n return result;\n };\n\n return {\n codeBlocks(text: string): CodeBlock[] {\n const scanned = scan(text);\n // No space is a match too, so there is one at the start of every line:\n // the match is never null, and the anchor is equivalent to its mutant.\n const spaces = (line: string): number => /^ */.exec(line)![0].length;\n return scanned.blocks\n .filter((block) => block.kind === 'fenced')\n .map((block) => {\n const first = block.line + 1;\n const last = block.closed ? block.endLine - 1 : block.endLine;\n // A fence in a block quote or a list item: its content is the code\n // without the quote markers and without the fence's own indent, as\n // CommonMark reads it, so a probe file is written as it was meant.\n const indent = spaces(scanned.lines[block.line - 1]?.content ?? '');\n const lines: string[] = [];\n for (let line = first; line <= last; line += 1) {\n const content = scanned.lines[line - 1]?.content ?? '';\n lines.push(content.slice(Math.min(indent, spaces(content))));\n }\n return { info: block.info, content: lines.join('\\n'), line: block.line };\n });\n },\n\n sectionAt(text: string, line: number): string | null {\n let found: string | null = null;\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level >= 2) found = heading.text;\n }\n return found;\n },\n\n sectionsAt(text: string, line: number): string[] {\n const open: { level: number; text: string }[] = [];\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level < 2) continue;\n while (open.length > 0 && open[open.length - 1]!.level >= heading.level) open.pop();\n open.push({ level: heading.level, text: heading.text });\n }\n return open.map((section) => section.text);\n },\n\n sectionTables(text: string, section: string): SectionTables {\n const scanned = scan(text);\n const headings = scanned.headings;\n const index = headings.findIndex((heading) => heading.level >= 2 && sameSection(heading.text, section));\n if (index < 0) return { tables: [], headingLine: null, tableEndLine: null };\n const heading = headings[index] as (typeof headings)[number];\n const next = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level);\n const endLine = next === undefined ? scanned.index.lineCount + 1 : next.line;\n const tables = scanned.tables.filter((table) => table.line > heading.endLine && table.line < endLine);\n const views: TableView[] = tables.map((table) => ({\n headers: table.headers.map((cell) => cell.text),\n rows: table.rows.map((row) => ({ cells: row.cells.map((cell) => cell.text), line: row.line })),\n }));\n const lastTable = tables[tables.length - 1];\n return { tables: views, headingLine: heading.endLine, tableEndLine: lastTable === undefined ? null : lastTable.endLine };\n },\n\n citations(text: string): Citation[] {\n return scan(text)\n .links.filter((link) => !link.image && link.form !== 'autolink' && link.form !== 'definition')\n .map((link) => ({ target: link.target, line: link.line }));\n },\n\n titleAndStatus(text: string): {\n title: string | null;\n status: string | null;\n unclosedFrontMatter: boolean;\n unreadableFrontMatter: UnreadableFrontMatterLine[];\n } {\n const scanned = scan(text);\n const title = scanned.headings.find((heading) => heading.level === 1)?.text ?? null;\n const front = readFrontMatter(text);\n let status: string | null = null;\n // The status is read under `status` alone: a key in another language,\n // such as `狀態`, is another key (ADR-0001).\n const entry = findEntry(front, 'status');\n if (entry !== undefined && entry.value.kind === 'scalar') status = entry.value.scalar.text || null;\n // TOML front matter gives no status, closed or not, so closing it would\n // change nothing the context packet shows; only YAML's is worth saying.\n return {\n title,\n status,\n unclosedFrontMatter: scanned.unclosedFrontMatter?.kind === 'yaml',\n unreadableFrontMatter: unreadLines(text, front),\n };\n },\n };\n}\n"]}
package/dist/round.d.ts CHANGED
@@ -69,7 +69,18 @@ export declare function claudeSettingsFiles(root: string, env: CliIO['env'], pla
69
69
  }[];
70
70
  /** The settings files Claude Code reads here, as read; one that is missing or cannot be read as JSON holds nothing. */
71
71
  export declare function claudeSettings(root: string, env: CliIO['env']): Promise<ClaudeSettings[]>;
72
- /** Whether the plugin, init's entries, both or neither wire Claude Code to the harness here. */
72
+ /**
73
+ * Whether the plugin, init's entries, both or neither wire Claude Code to the
74
+ * harness here, and what the hook does with each line the settings hold for
75
+ * it.
76
+ *
77
+ * A line is the hook's by the hook's own reading of it, whatever else stands
78
+ * on it, and read as a session started at `root` hands it over: the project
79
+ * is `root`, an empty `--root` is the directory the hook is started in, and
80
+ * a relative one is read from there. The plugin's own hooks are not read:
81
+ * they are files of a release, in a directory Claude Code keeps, and no
82
+ * release wrote more than the hook on their line (ADR-0012).
83
+ */
73
84
  export declare function claudeCodeWiring(root: string, env: CliIO['env']): Promise<ClaudeCodeWiring>;
74
85
  export interface AuditResult {
75
86
  readonly brief: BriefRow;
package/dist/round.js CHANGED
@@ -13,9 +13,9 @@ import { readArchivePlan, readGuardRun } from './answers.js';
13
13
  import { audit, tally } from './audit.js';
14
14
  import { SiblingOutputError } from './briefs.js';
15
15
  import { sameId } from './branch.js';
16
- import { enabledPlugin, exportedFile, holdsGuard, loadsPlugin, registersServer, SPEC_BRIEF_CONFIGS, } from './configure.js';
16
+ import { enabledPlugin, exportedFile, loadsPlugin, registersServer, reportHooks, SPEC_BRIEF_CONFIGS, } from './configure.js';
17
17
  import { readRules, renderContext } from './context.js';
18
- import { readJsonObject, readText, repositoryPath, stateDirectory, writeAtomic } from './fs.js';
18
+ import { isDirectory, readJsonObject, readText, repositoryPath, stateDirectory, writeAtomic } from './fs.js';
19
19
  import { blameLine, changes, isUncommitted, mergeBase, remoteDefault, revision, show, verifyCommit } from './git.js';
20
20
  import { decide } from './guard.js';
21
21
  import { diffManifest, ecosystemOf, readManifestNames } from './manifests.js';
@@ -23,7 +23,8 @@ import { addRulingRow, nextId, readRulings, renderMemo, renderRow, } from './rul
23
23
  import { compileGlob } from './vendor/spec-core/pattern/index.js';
24
24
  import { dirname, isRelativeReference, resolveInside, splitReference } from './vendor/spec-core/path/index.js';
25
25
  import { SiblingError } from './siblings.js';
26
- import { UsageError } from './workspace.js';
26
+ import { claudeHookLine } from './usage.js';
27
+ import { parseOptions, UsageError } from './workspace.js';
27
28
  /** What the round is measured from: the flag, the configuration, then the remote's default branch. */
28
29
  export async function resolveBase(workspace, flag) {
29
30
  const source = flag !== undefined ? 'flag' : workspace.config.base !== null ? 'config' : 'remote';
@@ -285,7 +286,7 @@ export function pluginFile(root) {
285
286
  // A manifest with no exports, or none for the plugin, throws in `join`
286
287
  // or on the missing key inside this block, which the catch reads as no
287
288
  // installed copy: `exports?.` and `target !== null` are equivalent to
288
- // their mutants here.
289
+ // their mutants here. So is the encoding: a Buffer parses as its text.
289
290
  const manifest = JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8'));
290
291
  const target = exportedFile(manifest.exports?.['./spec-brief-plugin']);
291
292
  if (target !== null)
@@ -336,12 +337,25 @@ export function claudeSettingsFiles(root, env, platform = process.platform) {
336
337
  export async function claudeSettings(root, env) {
337
338
  return Promise.all(claudeSettingsFiles(root, env).map(async ({ file, path }) => ({ file, settings: await readJsonObject(path) })));
338
339
  }
339
- /** Whether the plugin, init's entries, both or neither wire Claude Code to the harness here. */
340
+ /**
341
+ * Whether the plugin, init's entries, both or neither wire Claude Code to the
342
+ * harness here, and what the hook does with each line the settings hold for
343
+ * it.
344
+ *
345
+ * A line is the hook's by the hook's own reading of it, whatever else stands
346
+ * on it, and read as a session started at `root` hands it over: the project
347
+ * is `root`, an empty `--root` is the directory the hook is started in, and
348
+ * a relative one is read from there. The plugin's own hooks are not read:
349
+ * they are files of a release, in a directory Claude Code keeps, and no
350
+ * release wrote more than the hook on their line (ADR-0012).
351
+ */
340
352
  export async function claudeCodeWiring(root, env) {
341
353
  const sources = await claudeSettings(root, env);
354
+ const lines = reportHooks(sources, root, (words) => claudeHookLine(words, parseOptions, root, (path) => isDirectory(resolve(root, path))));
342
355
  return {
343
356
  plugin: enabledPlugin(sources),
344
- hooks: sources.filter(({ settings }) => holdsGuard(settings)).map(({ file }) => file),
357
+ hooks: sources.map(({ file }) => file).filter((file) => lines.some((line) => line.file === file)),
358
+ lines,
345
359
  server: registersServer(await readJsonObject(join(root, '.mcp.json'))),
346
360
  };
347
361
  }
@@ -419,7 +433,9 @@ export async function listEscalations(workspace) {
419
433
  const out = [];
420
434
  for (const name of (await readdir(directory)).filter((n) => n.endsWith('.json')).sort()) {
421
435
  try {
422
- out.push(JSON.parse(await readFile(join(directory, name), 'utf8')));
436
+ // A Buffer parses as its text, so the encoding is equivalent to its mutant.
437
+ const request = JSON.parse(await readFile(join(directory, name), 'utf8'));
438
+ out.push(request);
423
439
  }
424
440
  catch {
425
441
  // A request that cannot be read is skipped; listing it would list nothing true.