@descent-vtt/spec-harness 0.7.0 → 0.9.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 (56) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +54 -0
  4. package/README.md +5 -3
  5. package/dist/answers.d.ts +54 -0
  6. package/dist/answers.js +139 -0
  7. package/dist/answers.js.map +1 -0
  8. package/dist/branch.d.ts +2 -2
  9. package/dist/branch.js +3 -1
  10. package/dist/branch.js.map +1 -1
  11. package/dist/cli.js +28 -17
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands.js +7 -2
  14. package/dist/commands.js.map +1 -1
  15. package/dist/configure.d.ts +13 -1
  16. package/dist/configure.js +46 -3
  17. package/dist/configure.js.map +1 -1
  18. package/dist/context.d.ts +16 -4
  19. package/dist/context.js +27 -12
  20. package/dist/context.js.map +1 -1
  21. package/dist/document.d.ts +15 -3
  22. package/dist/document.js.map +1 -1
  23. package/dist/fs.js +8 -5
  24. package/dist/fs.js.map +1 -1
  25. package/dist/git.d.ts +12 -6
  26. package/dist/git.js +25 -11
  27. package/dist/git.js.map +1 -1
  28. package/dist/host.d.ts +4 -3
  29. package/dist/host.js +74 -13
  30. package/dist/host.js.map +1 -1
  31. package/dist/plugin.js +10 -17
  32. package/dist/plugin.js.map +1 -1
  33. package/dist/premises.js +16 -12
  34. package/dist/premises.js.map +1 -1
  35. package/dist/reader.js +27 -10
  36. package/dist/reader.js.map +1 -1
  37. package/dist/round.d.ts +1 -1
  38. package/dist/round.js +61 -55
  39. package/dist/round.js.map +1 -1
  40. package/dist/server.js +17 -6
  41. package/dist/server.js.map +1 -1
  42. package/dist/setup.js +20 -15
  43. package/dist/setup.js.map +1 -1
  44. package/dist/siblings.d.ts +2 -0
  45. package/dist/siblings.js +12 -4
  46. package/dist/siblings.js.map +1 -1
  47. package/dist/signers.d.ts +8 -5
  48. package/dist/signers.js +126 -51
  49. package/dist/signers.js.map +1 -1
  50. package/dist/versions.d.ts +16 -3
  51. package/dist/versions.js +12 -2
  52. package/dist/versions.js.map +1 -1
  53. package/dist/workspace.d.ts +16 -3
  54. package/dist/workspace.js +12 -2
  55. package/dist/workspace.js.map +1 -1
  56. package/package.json +1 -1
@@ -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 { readGuardRun, type Spec } from './answers.js';\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\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 = 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 // A directive spec-guard cannot read is listed apart from the results, as\n // errors; one in a premise section is a premise nothing checks.\n const { results, errors } = readGuardRun(answer.document);\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 | undefined }>(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.d.ts CHANGED
@@ -40,7 +40,7 @@ export interface RulingCheck {
40
40
  * last changed its row must be signed by a principal the base branch's
41
41
  * allowed-signers file lists (ADR-0006).
42
42
  */
43
- export declare function checkRulings(workspace: Workspace, brief: BriefRow, text: string, reader: DocumentReader, base: Base): Promise<RulingCheck>;
43
+ export declare function checkRulings(workspace: Workspace, brief: Pick<BriefRow, 'file'>, text: string, reader: DocumentReader, base: Base): Promise<RulingCheck>;
44
44
  export declare function briefText(workspace: Workspace, brief: BriefRow): Promise<string>;
45
45
  /** Decides paths against the brief, with the rulings verified against the base `baseFlag` names, or the configured one. */
46
46
  export declare function checkPaths(workspace: Workspace, brief: BriefRow | null, noBrief: string | undefined, given: readonly string[], cwd: string, reader: DocumentReader, baseFlag: string | undefined): Promise<Decision[]>;
package/dist/round.js CHANGED
@@ -9,12 +9,14 @@ import { existsSync, readFileSync, realpathSync } from 'node:fs';
9
9
  import { readdir, readFile, rm } from 'node:fs/promises';
10
10
  import { join, resolve } from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
+ import { readArchivePlan, readGuardRun } from './answers.js';
12
13
  import { audit, tally } from './audit.js';
14
+ import { SiblingOutputError } from './briefs.js';
13
15
  import { sameId } from './branch.js';
14
- import { enabledPlugin, holdsGuard, loadsPlugin, registersServer, SPEC_BRIEF_CONFIGS, } from './configure.js';
16
+ import { enabledPlugin, exportedFile, holdsGuard, loadsPlugin, registersServer, SPEC_BRIEF_CONFIGS, } from './configure.js';
15
17
  import { readRules, renderContext } from './context.js';
16
18
  import { readJsonObject, readText, repositoryPath, stateDirectory, writeAtomic } from './fs.js';
17
- import { blameLine, changes, mergeBase, remoteDefault, revision, show, verifyCommit } from './git.js';
19
+ import { blameLine, changes, isUncommitted, mergeBase, remoteDefault, revision, show, verifyCommit } from './git.js';
18
20
  import { decide } from './guard.js';
19
21
  import { diffManifest, ecosystemOf, readManifestNames } from './manifests.js';
20
22
  import { addRulingRow, nextId, readRulings, renderMemo, renderRow, } from './rulings.js';
@@ -30,9 +32,11 @@ export async function resolveBase(workspace, flag) {
30
32
  return { kind: 'unresolved', reason: 'no base is named and the remote has no default branch; pass --base <ref> or set "base"' };
31
33
  }
32
34
  const sha = await revision(ref, workspace.root);
33
- const head = await revision('HEAD', workspace.root);
34
- if (sha === null || head === null)
35
+ if (sha === null)
35
36
  return { kind: 'unresolved', reason: `"${ref}" names no commit` };
37
+ const head = await revision('HEAD', workspace.root);
38
+ if (head === null)
39
+ return { kind: 'unresolved', reason: 'HEAD names no commit: nothing is committed on this branch yet' };
36
40
  const common = await mergeBase(sha, head, workspace.root);
37
41
  if (common === null)
38
42
  return { kind: 'unresolved', reason: `"${ref}" and HEAD share no history` };
@@ -54,6 +58,8 @@ export async function checkRulings(workspace, brief, text, reader, base) {
54
58
  line: problem.line,
55
59
  }));
56
60
  const allows = rows.filter((row) => row.decision === 'allow');
61
+ // Nothing below verifies a row that allows nothing, so this saves a git
62
+ // show and is equivalent to its mutants.
57
63
  if (allows.length === 0)
58
64
  return { rows, verified: [], unverified: [], problems: findings };
59
65
  if (base.kind === 'unresolved') {
@@ -64,6 +70,8 @@ export async function checkRulings(workspace, brief, text, reader, base) {
64
70
  const reason = `${base.ref} has no ${workspace.config.rulings.allowedSigners}, so no signature can count`;
65
71
  return { rows, verified: [], unverified: allows.map((row) => ({ id: row.id, reason })), problems: findings };
66
72
  }
73
+ // Only verify-commit below reads this copy, so where it is kept, and under
74
+ // what name, is equivalent to its mutants.
67
75
  const directory = await stateDirectory(workspace.commonDir, 'signers');
68
76
  const file = join(directory, `${createHash('sha256').update(signers).digest('hex').slice(0, 16)}`);
69
77
  await writeAtomic(file, signers);
@@ -74,12 +82,12 @@ export async function checkRulings(workspace, brief, text, reader, base) {
74
82
  // HEAD the same number can be another line, one a signed commit wrote,
75
83
  // and an uncommitted row would borrow that commit's signature.
76
84
  const commit = await blameLine(null, brief.file, row.line, workspace.root);
77
- if (commit === null || /^0+$/.test(commit)) {
85
+ if (commit === null || isUncommitted(commit)) {
78
86
  unverified.push({ id: row.id, reason: 'its row is not committed' });
79
87
  continue;
80
88
  }
81
89
  const signature = await verifyCommit(commit, file, workspace.root);
82
- if (signature.good && signature.principal !== null)
90
+ if (signature.good)
83
91
  verified.push({ id: row.id, paths: row.paths, signer: signature.principal });
84
92
  else
85
93
  unverified.push({ id: row.id, reason: `commit ${commit.slice(0, 12)} last changed its row, and ${signature.detail || 'it is not signed'}` });
@@ -95,21 +103,18 @@ export async function briefText(workspace, brief) {
95
103
  }
96
104
  /** Decides paths against the brief, with the rulings verified against the base `baseFlag` names, or the configured one. */
97
105
  export async function checkPaths(workspace, brief, noBrief, given, cwd, reader, baseFlag) {
98
- let rulings = [];
99
106
  const resolved = given.map((path) => ({ given: path, path: repositoryPath(path, workspace.root, cwd) }));
100
- if (brief !== null) {
101
- // Rulings cost a blame and a signature check each, so they are read only
102
- // when a write reaches a protected file.
103
- const first = resolved.map(({ path, given: g }) => decide({ path, given: g, brief, noBrief, rulings: [], outOfScope: workspace.config.outOfScope }));
104
- if (first.some((decision) => decision.reason === 'protected')) {
105
- const base = await resolveBase(workspace, baseFlag);
106
- rulings = (await checkRulings(workspace, brief, await briefText(workspace, brief), reader, base)).verified;
107
- }
108
- else {
109
- return first;
110
- }
111
- }
112
- return resolved.map(({ path, given: g }) => decide({ path, given: g, brief, noBrief, rulings, outOfScope: workspace.config.outOfScope }));
107
+ const decideAll = (rulings) => resolved.map(({ path, given: g }) => decide({ path, given: g, brief, noBrief, rulings, outOfScope: workspace.config.outOfScope }));
108
+ const first = decideAll([]);
109
+ // Rulings cost a blame and a signature check each, so they are read only
110
+ // when a write reaches a protected file. Read anyway, they decide the same,
111
+ // since a ruling allows only a protected path, so mutants that always read
112
+ // them are equivalent; and with no brief nothing is protected, so the null
113
+ // check is there for the type.
114
+ if (brief === null || !first.some((decision) => decision.reason === 'protected'))
115
+ return first;
116
+ const base = await resolveBase(workspace, baseFlag);
117
+ return decideAll((await checkRulings(workspace, brief, await briefText(workspace, brief), reader, base)).verified);
113
118
  }
114
119
  /* ----------------------------------------------------------------- context */
115
120
  /**
@@ -131,6 +136,8 @@ function scopeBases(root, patterns) {
131
136
  const bases = new Set();
132
137
  for (const pattern of patterns) {
133
138
  try {
139
+ // Case is read only in matching, which this compile never does: its
140
+ // bases are the same either way, so `caseSensitive: false` is equivalent.
134
141
  for (const base of compileGlob(pattern, { dialect: 'path', caseSensitive: true, literal }).bases) {
135
142
  // A rooted alternative's base, `/docs` or `/`, is outside the
136
143
  // repository: it puts no path in the scope, and spec-guard refuses
@@ -158,9 +165,10 @@ async function rulesFor(workspace, brief) {
158
165
  return 'absent' in answer ? { unavailable: answer.absent } : readRules(answer);
159
166
  }
160
167
  catch (error) {
161
- // spec-guard printed no JSON: what it said instead is the reason, and
162
- // the rest of the packet stands without the rules.
163
- if (error instanceof SiblingError)
168
+ // spec-guard printed no JSON, or a document of another shape: what it
169
+ // said instead, or what was wrong, is the reason, and the rest of the
170
+ // packet stands without the rules.
171
+ if (error instanceof SiblingError || error instanceof SiblingOutputError)
164
172
  return { unavailable: error.message };
165
173
  throw error;
166
174
  }
@@ -179,7 +187,7 @@ async function citedDocuments(workspace, brief, text, reader) {
179
187
  // A destination that is not valid percent-encoding is read as written.
180
188
  }
181
189
  const path = resolveInside(dirname(brief.file), target);
182
- if (path === null || path === '' || path === brief.file || seen.has(path))
190
+ if (path === null || path === brief.file || seen.has(path))
183
191
  continue;
184
192
  seen.add(path);
185
193
  const document = /\.(?:md|markdown|mdx|txt)$/i.test(path);
@@ -188,6 +196,8 @@ async function citedDocuments(workspace, brief, text, reader) {
188
196
  if (!document && existsSync(join(workspace.root, path)))
189
197
  continue;
190
198
  const content = document ? await readText(join(workspace.root, path)) : null;
199
+ // A document that cannot be read is listed by its path alone, so its
200
+ // title and status are never read, and an empty object is equivalent.
191
201
  const meta = content === null ? { title: null, status: null } : reader.titleAndStatus(content);
192
202
  out.push({ path, ...meta, text: content });
193
203
  }
@@ -206,14 +216,9 @@ export async function buildContext(workspace, brief, briefs, reader, baseFlag) {
206
216
  briefText: text,
207
217
  dependencies: brief.dependsOn.map((id) =>
208
218
  // spec-brief reports a dependency as its front matter spells it, `7` for brief 007.
209
- briefs.find((candidate) => sameId(candidate.id, id)) ?? {
210
- ...brief,
211
- id,
212
- title: '(no such brief)',
213
- file: '',
214
- phase: 'live',
215
- status: 'unknown',
216
- }),
219
+ // The context reads a dependency's id, title, status and phase, and
220
+ // the brief copied here is live.
221
+ briefs.find((candidate) => sameId(candidate.id, id)) ?? { ...brief, id, title: '(no such brief)', status: 'unknown' }),
217
222
  cited,
218
223
  rules,
219
224
  rulings: rulings.verified,
@@ -240,8 +245,10 @@ async function dependencyChanges(workspace, base) {
240
245
  unread.push(change.path);
241
246
  continue;
242
247
  }
243
- const before = change.status === 'A' ? null : await show(base.mergeBase, change.from ?? change.path, workspace.root);
244
- const after = change.status === 'D' ? null : await show(base.head, change.path, workspace.root);
248
+ // git shows nothing where a file is not: an added manifest has no before,
249
+ // and a deleted one no after.
250
+ const before = await show(base.mergeBase, change.from ?? change.path, workspace.root);
251
+ const after = await show(base.head, change.path, workspace.root);
245
252
  const diff = diffManifest(change.path, ecosystem, before, after);
246
253
  if ('error' in diff)
247
254
  unread.push(change.path);
@@ -259,20 +266,6 @@ function realFile(path) {
259
266
  return null;
260
267
  }
261
268
  }
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
269
  /**
277
270
  * Whether a path spec-brief's `plugins` names, read from the root, is this
278
271
  * package's plugin: the file its `./spec-brief-plugin` export names, as the
@@ -284,15 +277,21 @@ export function pluginFile(root) {
284
277
  const installed = join(root, 'node_modules', '@descent-vtt', 'spec-harness');
285
278
  const candidates = [fileURLToPath(new URL('../dist/plugin.js', import.meta.url))];
286
279
  try {
280
+ // A manifest with no exports, or none for the plugin, throws in `join`
281
+ // or on the missing key inside this block, which the catch reads as no
282
+ // installed copy: `exports?.` and `target !== null` are equivalent to
283
+ // their mutants here.
287
284
  const manifest = JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8'));
288
- const target = exportTarget(manifest.exports?.['./spec-brief-plugin']);
285
+ const target = exportedFile(manifest.exports?.['./spec-brief-plugin']);
289
286
  if (target !== null)
290
287
  candidates.push(join(installed, target));
291
288
  }
292
289
  catch {
293
290
  // Not installed at the root, or its manifest cannot be read: this copy's file is the one to compare.
294
291
  }
295
- const files = new Set(candidates.map(realFile).filter((file) => file !== null));
292
+ // A candidate that names no file is `null` here, and so is a path that
293
+ // names none, which is never the plugin.
294
+ const files = new Set(candidates.map(realFile));
296
295
  return (path) => {
297
296
  const file = realFile(resolve(root, path));
298
297
  return file !== null && files.has(file);
@@ -304,6 +303,8 @@ export async function specBriefPlugin(root) {
304
303
  if (file === undefined)
305
304
  return { kind: 'unconfigured' };
306
305
  const config = await readJsonObject(join(root, file));
306
+ // The file was found a moment ago, so null is one removed in between, and
307
+ // reads as the unreadable one it has become.
307
308
  if (config === null || config === 'unreadable')
308
309
  return { kind: 'unreadable', file };
309
310
  return { kind: loadsPlugin(config, pluginFile(root)) ? 'loaded' : 'not-loaded', file };
@@ -344,12 +345,13 @@ async function archiveReasons(workspace, brief, base) {
344
345
  if (base.kind === 'resolved')
345
346
  args.push('--base', base.ref);
346
347
  const answer = await workspace.siblings.json('spec-brief', args);
348
+ // The audit listed the briefs through spec-brief before it asks this, so
349
+ // an absent one stopped it there; the check is for the type.
347
350
  if ('absent' in answer)
348
351
  return { unavailable: answer.absent };
349
352
  if (answer.code === 2)
350
353
  return { unavailable: 'spec-brief could not plan the archive' };
351
- const plan = answer.document.plan;
352
- return { blocking: plan?.blocking ?? [], warnings: plan?.warnings ?? [] };
354
+ return readArchivePlan(answer.document);
353
355
  }
354
356
  /** The brief's assertions as spec-guard ran them, and those it could not read, which it lists apart as `errors`. */
355
357
  async function briefAssertions(workspace, brief, text, reader) {
@@ -358,15 +360,15 @@ async function briefAssertions(workspace, brief, text, reader) {
358
360
  return { unavailable: answer.absent };
359
361
  if (answer.code === 2)
360
362
  return { unavailable: 'spec-guard could not run the brief\'s assertions' };
361
- const report = answer.document;
363
+ const report = readGuardRun(answer.document);
362
364
  const own = (spec) => spec === undefined || spec.file.replace(/\\/g, '/') === brief.file;
363
- const outcomes = (report.results ?? [])
365
+ const outcomes = report.results
364
366
  .filter((result) => own(result.spec))
365
367
  .map((result) => {
366
368
  const line = result.spec?.line ?? 0;
367
369
  return { ok: result.ok, description: result.description, message: result.message, line, section: reader.sectionAt(text, line), enclosing: reader.sectionsAt(text, line) };
368
370
  });
369
- const unreadable = (report.errors ?? []).filter((error) => own(error.spec)).map((error) => ({ message: error.message, line: error.spec?.line ?? 0, raw: error.raw ?? '' }));
371
+ const unreadable = report.errors.filter((error) => own(error.spec)).map((error) => ({ message: error.message, line: error.spec?.line ?? 0, raw: error.raw }));
370
372
  return { outcomes, unreadable };
371
373
  }
372
374
  export async function runAudit(workspace, brief, reader, baseFlag) {
@@ -386,6 +388,8 @@ export async function runAudit(workspace, brief, reader, baseFlag) {
386
388
  dependencies,
387
389
  archive,
388
390
  assertions: 'unavailable' in assertions ? assertions : assertions.outcomes,
391
+ // The audit reads no list as an empty one, and a run spec-guard could not
392
+ // make has none, so a mutant of this test reads the same.
389
393
  unreadableAssertions: 'unavailable' in assertions ? [] : assertions.unreadable,
390
394
  premiseSections: workspace.config.assertions.premises,
391
395
  unverifiedRulings: rulings.unverified,
@@ -444,6 +448,8 @@ export async function recordRuling(workspace, escalationId, decision, note, read
444
448
  const id = nextId('R', request.brief, taken);
445
449
  const row = renderRow(id, request.paths, decision, note);
446
450
  await writeAtomic(path, addRulingRow(text, workspace.config.rulings.section, row, where));
451
+ // Listed a moment ago; forced, a request another process retired in
452
+ // between is not an error, so the option is equivalent to its mutants.
447
453
  await rm(join(await escalationDirectory(workspace), `${request.id}.json`), { force: true });
448
454
  return { id, file: request.briefFile, row };
449
455
  }