@descent-vtt/spec-harness 0.7.0 → 0.8.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.
package/dist/host.js CHANGED
@@ -3,17 +3,35 @@
3
3
  * sibling: Claude Code, asked which release it is.
4
4
  *
5
5
  * Found on `PATH` and started without a shell, as every program the harness
6
- * runs is (ADR-0002). On Windows a program is an `.exe` or a `.com`; a
7
- * `claude.cmd` shim, as npm installs one, can be started only by a shell, so
8
- * it is named rather than run, and its release cannot be told.
6
+ * runs is (ADR-0002). On Windows a program is an `.exe` or a `.com`. A
7
+ * `claude.cmd` shim, as npm installs Claude Code, can be started only by a
8
+ * shell - Node refuses a `.cmd` without one since the fix for CVE-2024-27980 -
9
+ * so it is read rather than run: the package it runs declares the release.
9
10
  */
10
11
  import { execFile } from 'node:child_process';
11
12
  import { existsSync, statSync } from 'node:fs';
12
- import { delimiter, join } from 'node:path';
13
+ import { delimiter, dirname, join } from 'node:path';
14
+ import { readText } from './fs.js';
15
+ import { declaredVersion } from './siblings.js';
13
16
  /** What Windows runs for a name with no extension, in order; the first two alone without a shell. */
14
17
  const WINDOWS_EXTENSIONS = ['.COM', '.EXE', '.BAT', '.CMD'];
18
+ /**
19
+ * A path a shim runs, from the shim's own directory. npm's cmd-shim writes
20
+ * `"%dp0%\node_modules\@anthropic-ai\claude-code\bin\claude.exe"`, after
21
+ * `SET dp0=%~dp0`; pnpm's writes `"%~dp0\..."`.
22
+ */
23
+ const SHIM_TARGET = /"(?:%~dp0|%dp0%)\\([^"\r\n]*)"/g;
24
+ /**
25
+ * Where npm and pnpm install Claude Code, as the segments of a path: only its
26
+ * own package declares its release, where a package that wraps it declares
27
+ * the wrapper's.
28
+ */
29
+ const CLAUDE_CODE_PACKAGE = ['node_modules', '@anthropic-ai', 'claude-code'];
15
30
  /** The file `name` names on `PATH` as the environment gives it, or `null`. */
16
31
  export function onPath(name, env, platform = process.platform) {
32
+ // With neither, any text names directories no test can have made; and an
33
+ // empty entry is passed over rather than read as the working directory,
34
+ // which a test cannot fill: both mutants are equivalent in the suite.
17
35
  const path = env['PATH'] ?? env['Path'] ?? '';
18
36
  const windows = platform === 'win32';
19
37
  const extensions = windows
@@ -30,21 +48,64 @@ export function onPath(name, env, platform = process.platform) {
30
48
  }
31
49
  return null;
32
50
  }
51
+ /** The directory of Claude Code's package that a shim runs, or `null` when it runs none. */
52
+ function shimPackage(file, text) {
53
+ for (const match of text.matchAll(SHIM_TARGET)) {
54
+ const parts = match[1].split('\\');
55
+ const at = parts.findIndex((_, start) => CLAUDE_CODE_PACKAGE.every((part, offset) => parts[start + offset] === part));
56
+ if (at >= 0)
57
+ return join(dirname(file), ...parts.slice(0, at + CLAUDE_CODE_PACKAGE.length));
58
+ }
59
+ return null;
60
+ }
61
+ /**
62
+ * The release a Windows script on `PATH` runs, as the package.json of the
63
+ * Claude Code package it names declares it; nothing is run. A script that
64
+ * names no such package, or one whose package.json gives no version, cannot
65
+ * tell.
66
+ */
67
+ async function shimVersion(file) {
68
+ // A shim that cannot be read names no package, as an empty one does, so
69
+ // what stands in for its text is equivalent whatever it is.
70
+ const directory = shimPackage(file, (await readText(file)) ?? '');
71
+ if (directory !== null) {
72
+ const manifest = join(directory, 'package.json');
73
+ const version = declaredVersion(manifest);
74
+ if (typeof version === 'string')
75
+ return { declared: version, file: manifest };
76
+ }
77
+ return {
78
+ 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`,
79
+ };
80
+ }
33
81
  /** `claude --version`, as the Claude Code on `PATH` answers it, or why it cannot. */
34
82
  export async function claudeVersion(env, platform = process.platform) {
35
83
  const file = onPath('claude', env, platform);
36
84
  if (file === null)
37
85
  return { missing: 'no claude is on PATH' };
38
- if (platform === 'win32' && !/\.(?:exe|com)$/i.test(file)) {
39
- return { missing: `claude on PATH is ${file}, a script that cannot be started without a shell, which spec-harness does not use` };
40
- }
86
+ if (platform === 'win32' && !/\.(?:exe|com)$/i.test(file))
87
+ return shimVersion(file);
88
+ // Of the options, only `shell: false` and the encoding change an answer a
89
+ // test can see: `windowsHide` hides a console window, and the timeout
90
+ // stops a claude that never answers, which a test would wait on for its
91
+ // length, so their mutants are equivalent in the suite.
41
92
  return new Promise((resolve) => {
42
- execFile(file, ['--version'], { encoding: 'utf8', windowsHide: true, timeout: 15_000, shell: false }, (error, stdout) => {
43
- if (error !== null)
44
- resolve({ missing: `${file} --version failed: ${error.message.split('\n')[0] ?? ''}` });
45
- else
46
- resolve({ output: stdout });
47
- });
93
+ const failed = (error) => resolve({ missing: `${file} --version failed: ${error.message.split('\n')[0]}` });
94
+ try {
95
+ execFile(file, ['--version'], { encoding: 'utf8', windowsHide: true, timeout: 15_000, shell: false }, (error, stdout) => {
96
+ if (error !== null)
97
+ failed(error);
98
+ else
99
+ resolve({ output: stdout });
100
+ });
101
+ }
102
+ catch (error) {
103
+ // Windows refuses a file that is no program before any callback, as
104
+ // spawn UNKNOWN, where Linux reports it to the callback: doctor says
105
+ // so rather than stop. The sweep runs on Linux, so this line is the
106
+ // Windows job's to hold.
107
+ failed(error);
108
+ }
48
109
  });
49
110
  }
50
111
  //# sourceMappingURL=host.js.map
package/dist/host.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAI5C,qGAAqG;AACrG,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAE5D,8EAA8E;AAC9E,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,GAAiD,EAAE,QAAQ,GAAoB,OAAO,CAAC,QAAQ;IAClI,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,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,EAAE,CAAC;QAC1D,OAAO,EAAE,OAAO,EAAE,qBAAqB,IAAI,oFAAoF,EAAE,CAAC;IACpI,CAAC;IACD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,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;YACtH,IAAI,KAAK,KAAK,IAAI;gBAAE,OAAO,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,sBAAsB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;;gBACvG,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACL,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 one, can be started only by a shell, so\n * it is named rather than run, and its release cannot be told.\n */\n\nimport { execFile } from 'node:child_process';\nimport { existsSync, statSync } from 'node:fs';\nimport { delimiter, join } from 'node:path';\n\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/** 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 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/** `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)) {\n return { missing: `claude on PATH is ${file}, a script that cannot be started without a shell, which spec-harness does not use` };\n }\n return new Promise((resolve) => {\n execFile(file, ['--version'], { encoding: 'utf8', windowsHide: true, timeout: 15_000, shell: false }, (error, stdout) => {\n if (error !== null) resolve({ missing: `${file} --version failed: ${error.message.split('\\n')[0] ?? ''}` });\n else resolve({ output: stdout });\n });\n });\n}\n"]}
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"]}
package/dist/premises.js CHANGED
@@ -54,22 +54,28 @@ export async function premisesCommand(options, io) {
54
54
  return EXIT_ERROR;
55
55
  }
56
56
  const document = answer.document;
57
- const results = (document.results ?? []).filter((result) => result.spec !== undefined);
57
+ // A missing list is an empty one. A list that holds a string instead, the
58
+ // mutant, sorts nothing to a brief either, since a string has no spec, so
59
+ // it is equivalent; so is the one for errors.
60
+ const results = document.results ?? [];
58
61
  // A directive spec-guard cannot read is listed apart from the results; one
59
62
  // in a premise section is a premise nothing checks.
60
- const errors = (document.errors ?? []).filter((error) => error.spec !== undefined);
61
- const inBrief = (spec, file) => spec?.file.replace(/\\/g, '/') === file;
63
+ const errors = document.errors ?? [];
64
+ // A directive is a brief's when spec-guard places it in the brief's file;
65
+ // one it places nowhere is no brief's.
66
+ const inBrief = (item, file) => item.spec?.file.replace(/\\/g, '/') === file;
62
67
  const findings = [];
63
68
  let premises = 0;
64
69
  for (const brief of live) {
65
- const own = results.filter((result) => inBrief(result.spec, brief.file));
66
- const unread = errors.filter((error) => inBrief(error.spec, brief.file));
70
+ const own = results.filter((result) => inBrief(result, brief.file));
71
+ const unread = errors.filter((error) => inBrief(error, brief.file));
72
+ // Only saves reading a brief with nothing to judge: going on finds nothing, so that mutant is equivalent.
67
73
  if (own.length === 0 && unread.length === 0)
68
74
  continue;
69
75
  const text = await briefText(workspace, brief);
70
76
  const premise = (line) => isPremise(reader.sectionsAt(text, line), workspace.config.assertions.premises);
71
77
  for (const result of own) {
72
- const line = result.spec?.line ?? 0;
78
+ const { line } = result.spec;
73
79
  if (!premise(line))
74
80
  continue;
75
81
  premises += 1;
@@ -78,7 +84,7 @@ export async function premisesCommand(options, io) {
78
84
  findings.push(premiseFinding(brief, { description: result.description, message: result.message, line }, brief.file === round));
79
85
  }
80
86
  for (const error of unread) {
81
- const line = error.spec?.line ?? 0;
87
+ const { line } = error.spec;
82
88
  if (premise(line))
83
89
  findings.push(unreadablePremiseFinding(brief, { message: error.message, line, raw: error.raw ?? '' }));
84
90
  }
@@ -86,6 +92,8 @@ export async function premisesCommand(options, io) {
86
92
  const stale = findings.filter((finding) => finding.severity === 'error').length;
87
93
  const retired = findings.filter((finding) => finding.severity === 'note').length;
88
94
  const unreadable = findings.filter((finding) => finding.severity === 'warning').length;
95
+ // Only the round on the active brief retires a premise, so with one retired
96
+ // there is an active brief, and dropping the second test is equivalent.
89
97
  const intended = retired === 0 || active.kind !== 'found' ? '' : `, and ${retired} retired by the round on brief ${active.brief.id}, as it intends`;
90
98
  const unchecked = unreadable === 0 ? '' : `; ${unreadable} premise(s) spec-guard cannot read, so not checked`;
91
99
  const summary = `${premises} premise(s) in ${live.length} live brief(s), ${stale} no longer hold${intended}${unchecked}`;
@@ -1 +1 @@
1
- {"version":3,"file":"premises.js","sourceRoot":"","sources":["../src/premises.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `spec-harness premises`: is every live brief still about something true?\n *\n * A brief written against a defect states the defect as a premise - an\n * assertion under a section such as \"The Defect, Measured\" that holds while\n * the defect does. When the premise stops holding before the round has run,\n * the brief is stale: someone fixed the defect another way, or the code it\n * describes is gone. A repository that keeps its premises executable can say\n * so on every build, and ask for the brief to be archived or rewritten before\n * an agent is sent to fix what is already fixed.\n *\n * Goals are the audit's (`audit`), not this: a goal fails until its round is\n * done, and a build that failed for that would fail for every brief in flight.\n * For the same reason the premise of the brief a round is working on - the\n * one the flag, SPEC_BRIEF or the branch names - is reported as the audit\n * reports it, retired as the round intends, and fails nothing: on the round's\n * branch, a premise that no longer holds is the work being done.\n *\n * A premise spec-guard cannot read is checked by nothing, and is a warning,\n * as it is in the audit: `--strict` fails the run on it.\n */\n\nimport { isPremise, premiseFinding, unreadablePremiseFinding } from './audit.js';\nimport { briefIdFromBranch } from './branch.js';\nimport { findActive } from './briefs.js';\nimport { CONFIG_FILE } from './config.js';\nimport { formatFindings } from './formats.js';\nimport { createReader } from './reader.js';\nimport { briefText } from './round.js';\nimport type { Finding } from './types.js';\nimport { EXIT_ERROR, EXIT_FAILED, EXIT_OK, json, openWorkspace, version, type CliIO, type Options } from './workspace.js';\n\ninterface GuardResult {\n readonly ok: boolean;\n readonly description: string;\n readonly message: string;\n readonly spec?: { readonly file: string; readonly line: number };\n}\n\ninterface GuardError {\n readonly message: string;\n readonly raw?: string;\n readonly spec?: { readonly file: string; readonly line: number };\n}\n\nexport async function premisesCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const reader = createReader();\n const briefs = await workspace.siblings.briefs();\n const live = briefs.filter((brief) => brief.phase === 'live');\n const fromBranch = workspace.branch === null ? null : briefIdFromBranch(workspace.config.branches, workspace.branch);\n const active = findActive(briefs, { flag: options.brief, environment: io.env['SPEC_BRIEF'], branch: fromBranch });\n const round = active.kind === 'found' ? active.brief.file : null;\n if (live.length === 0) {\n if (options.format === 'json') io.stdout.write(json('premises', { ok: true, checked: 0, findings: [] }));\n else if (options.format === 'pretty') io.stdout.write('no live brief\\n');\n else io.stdout.write(formatFindings(options.format, [], { file: CONFIG_FILE, version: version(), summary: 'no live brief' }));\n return EXIT_OK;\n }\n const answer = await workspace.siblings.json('spec-guard', [...live.map((brief) => brief.file), '--ignore-status', '--json']);\n if ('absent' in answer) {\n io.stderr.write(`spec-harness: premises cannot be checked: ${answer.absent}\\n`);\n return EXIT_ERROR;\n }\n if (answer.code === 2) {\n io.stderr.write('spec-harness: spec-guard could not run the briefs\\' assertions\\n');\n return EXIT_ERROR;\n }\n const document = answer.document as { results?: GuardResult[]; errors?: GuardError[] };\n const results = (document.results ?? []).filter((result) => result.spec !== undefined);\n // A directive spec-guard cannot read is listed apart from the results; one\n // in a premise section is a premise nothing checks.\n const errors = (document.errors ?? []).filter((error) => error.spec !== undefined);\n const inBrief = (spec: { readonly file: string } | undefined, file: string): boolean => spec?.file.replace(/\\\\/g, '/') === file;\n const findings: Finding[] = [];\n let premises = 0;\n for (const brief of live) {\n const own = results.filter((result) => inBrief(result.spec, brief.file));\n const unread = errors.filter((error) => inBrief(error.spec, brief.file));\n if (own.length === 0 && unread.length === 0) continue;\n const text = await briefText(workspace, brief);\n const premise = (line: number): boolean => isPremise(reader.sectionsAt(text, line), workspace.config.assertions.premises);\n for (const result of own) {\n const line = result.spec?.line ?? 0;\n if (!premise(line)) continue;\n premises += 1;\n if (result.ok) continue;\n findings.push(premiseFinding(brief, { description: result.description, message: result.message, line }, brief.file === round));\n }\n for (const error of unread) {\n const line = error.spec?.line ?? 0;\n if (premise(line)) findings.push(unreadablePremiseFinding(brief, { message: error.message, line, raw: error.raw ?? '' }));\n }\n }\n const stale = findings.filter((finding) => finding.severity === 'error').length;\n const retired = findings.filter((finding) => finding.severity === 'note').length;\n const unreadable = findings.filter((finding) => finding.severity === 'warning').length;\n const intended = retired === 0 || active.kind !== 'found' ? '' : `, and ${retired} retired by the round on brief ${active.brief.id}, as it intends`;\n const unchecked = unreadable === 0 ? '' : `; ${unreadable} premise(s) spec-guard cannot read, so not checked`;\n const summary = `${premises} premise(s) in ${live.length} live brief(s), ${stale} no longer hold${intended}${unchecked}`;\n // As the audit does: a warning fails the run under --strict.\n const failed = stale > 0 || (options.strict && unreadable > 0);\n if (options.format === 'json') {\n io.stdout.write(json('premises', { ok: !failed, briefs: live.length, premises, unreadable, findings }));\n } else if (options.format === 'pretty') {\n for (const finding of findings) io.stdout.write(`${finding.severity.padEnd(8)} ${finding.file}:${finding.line} ${finding.message}\\n ${finding.hint}\\n`);\n io.stdout.write(`${summary}\\n`);\n } else {\n io.stdout.write(formatFindings(options.format, findings, { file: CONFIG_FILE, version: version(), summary }));\n }\n return failed ? EXIT_FAILED : EXIT_OK;\n}\n"]}
1
+ {"version":3,"file":"premises.js","sourceRoot":"","sources":["../src/premises.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `spec-harness premises`: is every live brief still about something true?\n *\n * A brief written against a defect states the defect as a premise - an\n * assertion under a section such as \"The Defect, Measured\" that holds while\n * the defect does. When the premise stops holding before the round has run,\n * the brief is stale: someone fixed the defect another way, or the code it\n * describes is gone. A repository that keeps its premises executable can say\n * so on every build, and ask for the brief to be archived or rewritten before\n * an agent is sent to fix what is already fixed.\n *\n * Goals are the audit's (`audit`), not this: a goal fails until its round is\n * done, and a build that failed for that would fail for every brief in flight.\n * For the same reason the premise of the brief a round is working on - the\n * one the flag, SPEC_BRIEF or the branch names - is reported as the audit\n * reports it, retired as the round intends, and fails nothing: on the round's\n * branch, a premise that no longer holds is the work being done.\n *\n * A premise spec-guard cannot read is checked by nothing, and is a warning,\n * as it is in the audit: `--strict` fails the run on it.\n */\n\nimport { isPremise, premiseFinding, unreadablePremiseFinding } from './audit.js';\nimport { briefIdFromBranch } from './branch.js';\nimport { findActive } from './briefs.js';\nimport { CONFIG_FILE } from './config.js';\nimport { formatFindings } from './formats.js';\nimport { createReader } from './reader.js';\nimport { briefText } from './round.js';\nimport type { Finding } from './types.js';\nimport { EXIT_ERROR, EXIT_FAILED, EXIT_OK, json, openWorkspace, version, type CliIO, type Options } from './workspace.js';\n\n/** Where spec-guard places a directive. */\ninterface Spec {\n readonly file: string;\n readonly line: number;\n}\n\ninterface GuardResult {\n readonly ok: boolean;\n readonly description: string;\n readonly message: string;\n readonly spec?: Spec;\n}\n\ninterface GuardError {\n readonly message: string;\n readonly raw?: string;\n readonly spec?: Spec;\n}\n\nexport async function premisesCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const reader = createReader();\n const briefs = await workspace.siblings.briefs();\n const live = briefs.filter((brief) => brief.phase === 'live');\n const fromBranch = workspace.branch === null ? null : briefIdFromBranch(workspace.config.branches, workspace.branch);\n const active = findActive(briefs, { flag: options.brief, environment: io.env['SPEC_BRIEF'], branch: fromBranch });\n const round = active.kind === 'found' ? active.brief.file : null;\n if (live.length === 0) {\n if (options.format === 'json') io.stdout.write(json('premises', { ok: true, checked: 0, findings: [] }));\n else if (options.format === 'pretty') io.stdout.write('no live brief\\n');\n else io.stdout.write(formatFindings(options.format, [], { file: CONFIG_FILE, version: version(), summary: 'no live brief' }));\n return EXIT_OK;\n }\n const answer = await workspace.siblings.json('spec-guard', [...live.map((brief) => brief.file), '--ignore-status', '--json']);\n if ('absent' in answer) {\n io.stderr.write(`spec-harness: premises cannot be checked: ${answer.absent}\\n`);\n return EXIT_ERROR;\n }\n if (answer.code === 2) {\n io.stderr.write('spec-harness: spec-guard could not run the briefs\\' assertions\\n');\n return EXIT_ERROR;\n }\n const document = answer.document as { results?: GuardResult[]; errors?: GuardError[] };\n // A missing list is an empty one. A list that holds a string instead, the\n // mutant, sorts nothing to a brief either, since a string has no spec, so\n // it is equivalent; so is the one for errors.\n const results = document.results ?? [];\n // A directive spec-guard cannot read is listed apart from the results; one\n // in a premise section is a premise nothing checks.\n const errors = document.errors ?? [];\n // A directive is a brief's when spec-guard places it in the brief's file;\n // one it places nowhere is no brief's.\n const inBrief = <T extends { readonly spec?: Spec }>(item: T, file: string): item is T & { readonly spec: Spec } =>\n item.spec?.file.replace(/\\\\/g, '/') === file;\n const findings: Finding[] = [];\n let premises = 0;\n for (const brief of live) {\n const own = results.filter((result) => inBrief(result, brief.file));\n const unread = errors.filter((error) => inBrief(error, brief.file));\n // Only saves reading a brief with nothing to judge: going on finds nothing, so that mutant is equivalent.\n if (own.length === 0 && unread.length === 0) continue;\n const text = await briefText(workspace, brief);\n const premise = (line: number): boolean => isPremise(reader.sectionsAt(text, line), workspace.config.assertions.premises);\n for (const result of own) {\n const { line } = result.spec;\n if (!premise(line)) continue;\n premises += 1;\n if (result.ok) continue;\n findings.push(premiseFinding(brief, { description: result.description, message: result.message, line }, brief.file === round));\n }\n for (const error of unread) {\n const { line } = error.spec;\n if (premise(line)) findings.push(unreadablePremiseFinding(brief, { message: error.message, line, raw: error.raw ?? '' }));\n }\n }\n const stale = findings.filter((finding) => finding.severity === 'error').length;\n const retired = findings.filter((finding) => finding.severity === 'note').length;\n const unreadable = findings.filter((finding) => finding.severity === 'warning').length;\n // Only the round on the active brief retires a premise, so with one retired\n // there is an active brief, and dropping the second test is equivalent.\n const intended = retired === 0 || active.kind !== 'found' ? '' : `, and ${retired} retired by the round on brief ${active.brief.id}, as it intends`;\n const unchecked = unreadable === 0 ? '' : `; ${unreadable} premise(s) spec-guard cannot read, so not checked`;\n const summary = `${premises} premise(s) in ${live.length} live brief(s), ${stale} no longer hold${intended}${unchecked}`;\n // As the audit does: a warning fails the run under --strict.\n const failed = stale > 0 || (options.strict && unreadable > 0);\n if (options.format === 'json') {\n io.stdout.write(json('premises', { ok: !failed, briefs: live.length, premises, unreadable, findings }));\n } else if (options.format === 'pretty') {\n for (const finding of findings) io.stdout.write(`${finding.severity.padEnd(8)} ${finding.file}:${finding.line} ${finding.message}\\n ${finding.hint}\\n`);\n io.stdout.write(`${summary}\\n`);\n } else {\n io.stdout.write(formatFindings(options.format, findings, { file: CONFIG_FILE, version: version(), summary }));\n }\n return failed ? EXIT_FAILED : EXIT_OK;\n}\n"]}
package/dist/reader.js CHANGED
@@ -3,14 +3,7 @@
3
3
  * or a comment is ever taken for a section, a table, a link or a probe.
4
4
  */
5
5
  import { findEntry, readFrontMatter, scanMarkdown } from './vendor/spec-core/markdown/index.js';
6
- /**
7
- * The front matter keys a cited document's status is read under, the first
8
- * the document holds deciding: `status`, then the Traditional and the
9
- * Simplified Chinese for it, which a Chinese ADR writes with YAML's ASCII
10
- * colon. A full-width colon is no YAML separator, so `狀態:已接受` gives no
11
- * key, and no status, as spec-guard reads it.
12
- */
13
- const STATUS_KEYS = ['status', '狀態', '状态'];
6
+ import { splitLines } from './vendor/spec-core/text/index.js';
14
7
  /** Section names compare without case, emphasis, a leading number or a trailing colon. */
15
8
  export function sameSection(heading, name) {
16
9
  const normal = (text) => text
@@ -21,6 +14,23 @@ export function sameSection(heading, name) {
21
14
  .toLowerCase();
22
15
  return normal(heading) === normal(name);
23
16
  }
17
+ /**
18
+ * The lines of closed YAML front matter spec-core's reader passes over, since
19
+ * none is `key: value`, each with its reason. The reader reports a problem on
20
+ * a line it read too - a key declared twice - and one on the opening line for
21
+ * TOML front matter; neither is a line passed over, and only these are. A
22
+ * byte-order mark, which the reader leaves out, sits on the opening line,
23
+ * never one of these, so the text's own lines give each as written.
24
+ */
25
+ function unreadLines(text, front) {
26
+ if (front === null)
27
+ return [];
28
+ const read = new Set(front.entries.map((entry) => entry.line));
29
+ const lines = splitLines(text);
30
+ return front.problems
31
+ .filter((problem) => problem.line > 0 && !read.has(problem.line))
32
+ .map((problem) => ({ line: problem.line + 1, text: lines[problem.line], reason: problem.message }));
33
+ }
24
34
  function scanOf(text, cache) {
25
35
  return cache !== null && cache.text === text ? cache.scan : scanMarkdown(text);
26
36
  }
@@ -103,12 +113,19 @@ export function createReader() {
103
113
  const title = scanned.headings.find((heading) => heading.level === 1)?.text ?? null;
104
114
  const front = readFrontMatter(text);
105
115
  let status = null;
106
- const entry = STATUS_KEYS.map((key) => findEntry(front, key)).find((candidate) => candidate !== undefined);
116
+ // The status is read under `status` alone: a key in another language,
117
+ // such as `狀態`, is another key (ADR-0001).
118
+ const entry = findEntry(front, 'status');
107
119
  if (entry !== undefined && entry.value.kind === 'scalar')
108
120
  status = entry.value.scalar.text || null;
109
121
  // TOML front matter gives no status, closed or not, so closing it would
110
122
  // change nothing the context packet shows; only YAML's is worth saying.
111
- return { title, status, unclosedFrontMatter: scanned.unclosedFrontMatter?.kind === 'yaml' };
123
+ return {
124
+ title,
125
+ status,
126
+ unclosedFrontMatter: scanned.unclosedFrontMatter?.kind === 'yaml',
127
+ unreadableFrontMatter: unreadLines(text, front),
128
+ };
112
129
  },
113
130
  };
114
131
  }
@@ -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 } from './document.js';\nimport type { CodeBlock } from './probe.js';\nimport type { TableView } from './rulings.js';\nimport { findEntry, readFrontMatter, scanMarkdown, type MarkdownScan } from './vendor/spec-core/markdown/index.js';\n\n/**\n * The front matter keys a cited document's status is read under, the first\n * the document holds deciding: `status`, then the Traditional and the\n * Simplified Chinese for it, which a Chinese ADR writes with YAML's ASCII\n * colon. A full-width colon is no YAML separator, so `狀態:已接受` gives no\n * key, and no status, as spec-guard reads it.\n */\nconst STATUS_KEYS = ['status', '狀態', '状态'] as const;\n\n/** Section names compare without case, emphasis, a leading number or a trailing colon. */\nexport function sameSection(heading: string, name: string): boolean {\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\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 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): { title: string | null; status: string | null; unclosedFrontMatter: boolean } {\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 const entry = STATUS_KEYS.map((key) => findEntry(front, key)).find((candidate) => candidate !== undefined);\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 { title, status, unclosedFrontMatter: scanned.unclosedFrontMatter?.kind === 'yaml' };\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 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 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"]}
package/dist/round.js CHANGED
@@ -11,7 +11,7 @@ import { join, resolve } from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { audit, tally } from './audit.js';
13
13
  import { sameId } from './branch.js';
14
- import { enabledPlugin, holdsGuard, loadsPlugin, registersServer, SPEC_BRIEF_CONFIGS, } from './configure.js';
14
+ import { enabledPlugin, exportedFile, holdsGuard, loadsPlugin, registersServer, SPEC_BRIEF_CONFIGS, } from './configure.js';
15
15
  import { readRules, renderContext } from './context.js';
16
16
  import { readJsonObject, readText, repositoryPath, stateDirectory, writeAtomic } from './fs.js';
17
17
  import { blameLine, changes, mergeBase, remoteDefault, revision, show, verifyCommit } from './git.js';
@@ -131,6 +131,8 @@ function scopeBases(root, patterns) {
131
131
  const bases = new Set();
132
132
  for (const pattern of patterns) {
133
133
  try {
134
+ // Case is read only in matching, which this compile never does: its
135
+ // bases are the same either way, so `caseSensitive: false` is equivalent.
134
136
  for (const base of compileGlob(pattern, { dialect: 'path', caseSensitive: true, literal }).bases) {
135
137
  // A rooted alternative's base, `/docs` or `/`, is outside the
136
138
  // repository: it puts no path in the scope, and spec-guard refuses
@@ -259,20 +261,6 @@ function realFile(path) {
259
261
  return null;
260
262
  }
261
263
  }
262
- /** The file a package's `exports` entry names for an import, as spec-brief resolves its plugins. */
263
- function exportTarget(entry) {
264
- if (typeof entry === 'string')
265
- return entry;
266
- if (typeof entry !== 'object' || entry === null)
267
- return null;
268
- const conditions = entry;
269
- for (const condition of ['import', 'node', 'default']) {
270
- const target = exportTarget(conditions[condition]);
271
- if (target !== null)
272
- return target;
273
- }
274
- return null;
275
- }
276
264
  /**
277
265
  * Whether a path spec-brief's `plugins` names, read from the root, is this
278
266
  * package's plugin: the file its `./spec-brief-plugin` export names, as the
@@ -284,15 +272,21 @@ export function pluginFile(root) {
284
272
  const installed = join(root, 'node_modules', '@descent-vtt', 'spec-harness');
285
273
  const candidates = [fileURLToPath(new URL('../dist/plugin.js', import.meta.url))];
286
274
  try {
275
+ // A manifest with no exports, or none for the plugin, throws in `join`
276
+ // or on the missing key inside this block, which the catch reads as no
277
+ // installed copy: `exports?.` and `target !== null` are equivalent to
278
+ // their mutants here.
287
279
  const manifest = JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8'));
288
- const target = exportTarget(manifest.exports?.['./spec-brief-plugin']);
280
+ const target = exportedFile(manifest.exports?.['./spec-brief-plugin']);
289
281
  if (target !== null)
290
282
  candidates.push(join(installed, target));
291
283
  }
292
284
  catch {
293
285
  // Not installed at the root, or its manifest cannot be read: this copy's file is the one to compare.
294
286
  }
295
- const files = new Set(candidates.map(realFile).filter((file) => file !== null));
287
+ // A candidate that names no file is `null` here, and so is a path that
288
+ // names none, which is never the plugin.
289
+ const files = new Set(candidates.map(realFile));
296
290
  return (path) => {
297
291
  const file = realFile(resolve(root, path));
298
292
  return file !== null && files.has(file);
@@ -386,6 +380,8 @@ export async function runAudit(workspace, brief, reader, baseFlag) {
386
380
  dependencies,
387
381
  archive,
388
382
  assertions: 'unavailable' in assertions ? assertions : assertions.outcomes,
383
+ // The audit reads no list as an empty one, and a run spec-guard could not
384
+ // make has none, so a mutant of this test reads the same.
389
385
  unreadableAssertions: 'unavailable' in assertions ? [] : assertions.unreadable,
390
386
  premiseSections: workspace.config.assertions.premises,
391
387
  unverifiedRulings: rulings.unverified,