@descent-vtt/spec-harness 0.5.0 → 0.7.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 (84) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +148 -0
  4. package/README.md +273 -24
  5. package/dist/audit.d.ts +71 -1
  6. package/dist/audit.js +140 -17
  7. package/dist/audit.js.map +1 -1
  8. package/dist/branch.d.ts +13 -0
  9. package/dist/branch.js +19 -0
  10. package/dist/branch.js.map +1 -1
  11. package/dist/cli.d.ts +1 -1
  12. package/dist/cli.js +51 -13
  13. package/dist/cli.js.map +1 -1
  14. package/dist/commands.js +13 -4
  15. package/dist/commands.js.map +1 -1
  16. package/dist/config.js +3 -1
  17. package/dist/config.js.map +1 -1
  18. package/dist/configure.d.ts +50 -3
  19. package/dist/configure.js +78 -5
  20. package/dist/configure.js.map +1 -1
  21. package/dist/context.d.ts +21 -7
  22. package/dist/context.js +104 -31
  23. package/dist/context.js.map +1 -1
  24. package/dist/document.d.ts +2 -1
  25. package/dist/document.js.map +1 -1
  26. package/dist/formats.d.ts +31 -0
  27. package/dist/formats.js +127 -0
  28. package/dist/formats.js.map +1 -0
  29. package/dist/git.d.ts +6 -0
  30. package/dist/git.js +8 -0
  31. package/dist/git.js.map +1 -1
  32. package/dist/guard.d.ts +8 -0
  33. package/dist/guard.js +19 -1
  34. package/dist/guard.js.map +1 -1
  35. package/dist/host.d.ts +14 -0
  36. package/dist/host.js +50 -0
  37. package/dist/host.js.map +1 -0
  38. package/dist/index.d.ts +5 -3
  39. package/dist/index.js +2 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/manifests.d.ts +8 -1
  42. package/dist/manifests.js +16 -6
  43. package/dist/manifests.js.map +1 -1
  44. package/dist/premises.d.ts +3 -0
  45. package/dist/premises.js +43 -13
  46. package/dist/premises.js.map +1 -1
  47. package/dist/reader.js +10 -2
  48. package/dist/reader.js.map +1 -1
  49. package/dist/round.d.ts +8 -0
  50. package/dist/round.js +73 -22
  51. package/dist/round.js.map +1 -1
  52. package/dist/server.js +11 -3
  53. package/dist/server.js.map +1 -1
  54. package/dist/setup.d.ts +10 -0
  55. package/dist/setup.js +68 -32
  56. package/dist/setup.js.map +1 -1
  57. package/dist/signers.d.ts +43 -0
  58. package/dist/signers.js +105 -0
  59. package/dist/signers.js.map +1 -0
  60. package/dist/types.d.ts +7 -0
  61. package/dist/types.js.map +1 -1
  62. package/dist/vendor/spec-core/markdown/frontmatter.d.ts +11 -1
  63. package/dist/vendor/spec-core/markdown/frontmatter.js +20 -3
  64. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
  65. package/dist/vendor/spec-core/pattern/glob.d.ts +88 -2
  66. package/dist/vendor/spec-core/pattern/glob.js +234 -32
  67. package/dist/vendor/spec-core/pattern/glob.js.map +1 -1
  68. package/dist/vendor/spec-core/pattern/index.d.ts +2 -2
  69. package/dist/vendor/spec-core/pattern/index.js +1 -1
  70. package/dist/vendor/spec-core/pattern/index.js.map +1 -1
  71. package/dist/vendor/spec-core/text/index.d.ts +1 -0
  72. package/dist/vendor/spec-core/text/index.js +1 -0
  73. package/dist/vendor/spec-core/text/index.js.map +1 -1
  74. package/dist/vendor/spec-core/text/width.d.ts +30 -0
  75. package/dist/vendor/spec-core/text/width.js +113 -0
  76. package/dist/vendor/spec-core/text/width.js.map +1 -0
  77. package/dist/versions.d.ts +27 -1
  78. package/dist/versions.js +23 -1
  79. package/dist/versions.js.map +1 -1
  80. package/dist/workspace.d.ts +6 -1
  81. package/dist/workspace.js +17 -6
  82. package/dist/workspace.js.map +1 -1
  83. package/package.json +1 -1
  84. package/skills/close-round/SKILL.md +4 -1
package/dist/premises.js CHANGED
@@ -15,13 +15,18 @@
15
15
  * one the flag, SPEC_BRIEF or the branch names - is reported as the audit
16
16
  * reports it, retired as the round intends, and fails nothing: on the round's
17
17
  * branch, a premise that no longer holds is the work being done.
18
+ *
19
+ * A premise spec-guard cannot read is checked by nothing, and is a warning,
20
+ * as it is in the audit: `--strict` fails the run on it.
18
21
  */
19
- import { isPremise, premiseFinding } from './audit.js';
22
+ import { isPremise, premiseFinding, unreadablePremiseFinding } from './audit.js';
20
23
  import { briefIdFromBranch } from './branch.js';
21
24
  import { findActive } from './briefs.js';
25
+ import { CONFIG_FILE } from './config.js';
26
+ import { formatFindings } from './formats.js';
22
27
  import { createReader } from './reader.js';
23
28
  import { briefText } from './round.js';
24
- import { EXIT_ERROR, EXIT_FAILED, EXIT_OK, json, openWorkspace } from './workspace.js';
29
+ import { EXIT_ERROR, EXIT_FAILED, EXIT_OK, json, openWorkspace, version } from './workspace.js';
25
30
  export async function premisesCommand(options, io) {
26
31
  const workspace = await openWorkspace(options, io);
27
32
  const reader = createReader();
@@ -31,7 +36,12 @@ export async function premisesCommand(options, io) {
31
36
  const active = findActive(briefs, { flag: options.brief, environment: io.env['SPEC_BRIEF'], branch: fromBranch });
32
37
  const round = active.kind === 'found' ? active.brief.file : null;
33
38
  if (live.length === 0) {
34
- io.stdout.write(options.format === 'json' ? json('premises', { ok: true, checked: 0, findings: [] }) : 'no live brief\n');
39
+ if (options.format === 'json')
40
+ io.stdout.write(json('premises', { ok: true, checked: 0, findings: [] }));
41
+ else if (options.format === 'pretty')
42
+ io.stdout.write('no live brief\n');
43
+ else
44
+ io.stdout.write(formatFindings(options.format, [], { file: CONFIG_FILE, version: version(), summary: 'no live brief' }));
35
45
  return EXIT_OK;
36
46
  }
37
47
  const answer = await workspace.siblings.json('spec-guard', [...live.map((brief) => brief.file), '--ignore-status', '--json']);
@@ -43,35 +53,55 @@ export async function premisesCommand(options, io) {
43
53
  io.stderr.write('spec-harness: spec-guard could not run the briefs\' assertions\n');
44
54
  return EXIT_ERROR;
45
55
  }
46
- const results = (answer.document.results ?? []).filter((result) => result.spec !== undefined);
56
+ const document = answer.document;
57
+ const results = (document.results ?? []).filter((result) => result.spec !== undefined);
58
+ // A directive spec-guard cannot read is listed apart from the results; one
59
+ // 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;
47
62
  const findings = [];
48
63
  let premises = 0;
49
64
  for (const brief of live) {
50
- const own = results.filter((result) => result.spec?.file.replace(/\\/g, '/') === brief.file);
51
- if (own.length === 0)
65
+ const own = results.filter((result) => inBrief(result.spec, brief.file));
66
+ const unread = errors.filter((error) => inBrief(error.spec, brief.file));
67
+ if (own.length === 0 && unread.length === 0)
52
68
  continue;
53
69
  const text = await briefText(workspace, brief);
70
+ const premise = (line) => isPremise(reader.sectionsAt(text, line), workspace.config.assertions.premises);
54
71
  for (const result of own) {
55
72
  const line = result.spec?.line ?? 0;
56
- if (!isPremise(reader.sectionsAt(text, line), workspace.config.assertions.premises))
73
+ if (!premise(line))
57
74
  continue;
58
75
  premises += 1;
59
76
  if (result.ok)
60
77
  continue;
61
78
  findings.push(premiseFinding(brief, { description: result.description, message: result.message, line }, brief.file === round));
62
79
  }
80
+ for (const error of unread) {
81
+ const line = error.spec?.line ?? 0;
82
+ if (premise(line))
83
+ findings.push(unreadablePremiseFinding(brief, { message: error.message, line, raw: error.raw ?? '' }));
84
+ }
63
85
  }
64
86
  const stale = findings.filter((finding) => finding.severity === 'error').length;
65
- const retired = findings.length - stale;
87
+ const retired = findings.filter((finding) => finding.severity === 'note').length;
88
+ const unreadable = findings.filter((finding) => finding.severity === 'warning').length;
89
+ const intended = retired === 0 || active.kind !== 'found' ? '' : `, and ${retired} retired by the round on brief ${active.brief.id}, as it intends`;
90
+ const unchecked = unreadable === 0 ? '' : `; ${unreadable} premise(s) spec-guard cannot read, so not checked`;
91
+ const summary = `${premises} premise(s) in ${live.length} live brief(s), ${stale} no longer hold${intended}${unchecked}`;
92
+ // As the audit does: a warning fails the run under --strict.
93
+ const failed = stale > 0 || (options.strict && unreadable > 0);
66
94
  if (options.format === 'json') {
67
- io.stdout.write(json('premises', { ok: stale === 0, briefs: live.length, premises, findings }));
95
+ io.stdout.write(json('premises', { ok: !failed, briefs: live.length, premises, unreadable, findings }));
68
96
  }
69
- else {
97
+ else if (options.format === 'pretty') {
70
98
  for (const finding of findings)
71
99
  io.stdout.write(`${finding.severity.padEnd(8)} ${finding.file}:${finding.line} ${finding.message}\n ${finding.hint}\n`);
72
- const intended = retired === 0 || active.kind !== 'found' ? '' : `, and ${retired} retired by the round on brief ${active.brief.id}, as it intends`;
73
- io.stdout.write(`${premises} premise(s) in ${live.length} live brief(s), ${stale} no longer hold${intended}\n`);
100
+ io.stdout.write(`${summary}\n`);
101
+ }
102
+ else {
103
+ io.stdout.write(formatFindings(options.format, findings, { file: CONFIG_FILE, version: version(), summary }));
74
104
  }
75
- return stale > 0 ? EXIT_FAILED : EXIT_OK;
105
+ return failed ? EXIT_FAILED : EXIT_OK;
76
106
  }
77
107
  //# sourceMappingURL=premises.js.map
@@ -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\nimport { isPremise, premiseFinding } from './audit.js';\nimport { briefIdFromBranch } from './branch.js';\nimport { findActive } from './briefs.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, 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\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 io.stdout.write(options.format === 'json' ? json('premises', { ok: true, checked: 0, findings: [] }) : 'no live brief\\n');\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 results = ((answer.document as { results?: GuardResult[] }).results ?? []).filter((result) => result.spec !== undefined);\n const findings: Finding[] = [];\n let premises = 0;\n for (const brief of live) {\n const own = results.filter((result) => result.spec?.file.replace(/\\\\/g, '/') === brief.file);\n if (own.length === 0) continue;\n const text = await briefText(workspace, brief);\n for (const result of own) {\n const line = result.spec?.line ?? 0;\n if (!isPremise(reader.sectionsAt(text, line), workspace.config.assertions.premises)) 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 }\n const stale = findings.filter((finding) => finding.severity === 'error').length;\n const retired = findings.length - stale;\n if (options.format === 'json') {\n io.stdout.write(json('premises', { ok: stale === 0, briefs: live.length, premises, findings }));\n } else {\n for (const finding of findings) io.stdout.write(`${finding.severity.padEnd(8)} ${finding.file}:${finding.line} ${finding.message}\\n ${finding.hint}\\n`);\n const intended = retired === 0 || active.kind !== 'found' ? '' : `, and ${retired} retired by the round on brief ${active.brief.id}, as it intends`;\n io.stdout.write(`${premises} premise(s) in ${live.length} live brief(s), ${stale} no longer hold${intended}\\n`);\n }\n return stale > 0 ? 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\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"]}
package/dist/reader.js CHANGED
@@ -2,7 +2,15 @@
2
2
  * The document reader, on spec-core's Markdown scanner: nothing inside code
3
3
  * or a comment is ever taken for a section, a table, a link or a probe.
4
4
  */
5
- import { readFrontMatter, scanMarkdown } from './vendor/spec-core/markdown/index.js';
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
14
  /** Section names compare without case, emphasis, a leading number or a trailing colon. */
7
15
  export function sameSection(heading, name) {
8
16
  const normal = (text) => text
@@ -95,7 +103,7 @@ export function createReader() {
95
103
  const title = scanned.headings.find((heading) => heading.level === 1)?.text ?? null;
96
104
  const front = readFrontMatter(text);
97
105
  let status = null;
98
- const entry = front?.entries.find((candidate) => candidate.name === 'status');
106
+ const entry = STATUS_KEYS.map((key) => findEntry(front, key)).find((candidate) => candidate !== undefined);
99
107
  if (entry !== undefined && entry.value.kind === 'scalar')
100
108
  status = entry.value.scalar.text || null;
101
109
  // TOML front matter gives no status, closed or not, so closing it would
@@ -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 { readFrontMatter, scanMarkdown, type MarkdownScan } from './vendor/spec-core/markdown/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\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 = front?.entries.find((candidate) => candidate.name === '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 { 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 } 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"]}
package/dist/round.d.ts CHANGED
@@ -45,6 +45,14 @@ export declare function briefText(workspace: Workspace, brief: BriefRow): Promis
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[]>;
47
47
  export declare function buildContext(workspace: Workspace, brief: BriefRow, briefs: readonly BriefRow[], reader: DocumentReader, baseFlag: string | undefined): Promise<ContextPacket>;
48
+ /**
49
+ * Whether a path spec-brief's `plugins` names, read from the root, is this
50
+ * package's plugin: the file its `./spec-brief-plugin` export names, as the
51
+ * project installed the package, or as this copy of it ships. spec-brief
52
+ * loads a plugin by a path as well as by its name, and a path to the plugin
53
+ * read as another plugin would say the archive cannot accept a ruling it can.
54
+ */
55
+ export declare function pluginFile(root: string): (path: string) => boolean;
48
56
  /** Whether spec-brief loads this package's plugin, as its configuration at the root says. */
49
57
  export declare function specBriefPlugin(root: string): Promise<PluginState>;
50
58
  /**
package/dist/round.js CHANGED
@@ -5,10 +5,11 @@
5
5
  * `audit.ts`, `guard.ts` and `rulings.ts`.
6
6
  */
7
7
  import { createHash } from 'node:crypto';
8
- import { existsSync } from 'node:fs';
8
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
9
9
  import { readdir, readFile, rm } from 'node:fs/promises';
10
- import { join } from 'node:path';
11
- import { audit } from './audit.js';
10
+ import { join, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { audit, tally } from './audit.js';
12
13
  import { sameId } from './branch.js';
13
14
  import { enabledPlugin, holdsGuard, loadsPlugin, registersServer, SPEC_BRIEF_CONFIGS, } from './configure.js';
14
15
  import { readRules, renderContext } from './context.js';
@@ -130,8 +131,15 @@ function scopeBases(root, patterns) {
130
131
  const bases = new Set();
131
132
  for (const pattern of patterns) {
132
133
  try {
133
- for (const base of compileGlob(pattern, { dialect: 'path', caseSensitive: true, literal }).bases)
134
+ for (const base of compileGlob(pattern, { dialect: 'path', caseSensitive: true, literal }).bases) {
135
+ // A rooted alternative's base, `/docs` or `/`, is outside the
136
+ // repository: it puts no path in the scope, and spec-guard refuses
137
+ // the whole question for it, with the rules for every other base.
138
+ // The packet names the pattern.
139
+ if (base.startsWith('/'))
140
+ continue;
134
141
  bases.add(base === '' ? '.' : base);
142
+ }
135
143
  }
136
144
  catch {
137
145
  // The packet names a pattern the guard cannot read, with spec-core's
@@ -220,7 +228,7 @@ async function dependencyChanges(workspace, base) {
220
228
  // leave its manifests out of every audit measured from one.
221
229
  const names = readManifestNames(workspace.config.dependencies.manifests);
222
230
  if (base.kind === 'unresolved')
223
- return { changes: [], unread: [], unreadNames: names.unread };
231
+ return { changes: [], unread: [], unreadNames: names.unread, rootedNames: names.rooted };
224
232
  const isManifest = names.match;
225
233
  const out = [];
226
234
  const unread = [];
@@ -240,7 +248,55 @@ async function dependencyChanges(workspace, base) {
240
248
  else
241
249
  out.push(...diff.changes);
242
250
  }
243
- return { changes: out, unread, unreadNames: names.unread };
251
+ return { changes: out, unread, unreadNames: names.unread, rootedNames: names.rooted };
252
+ }
253
+ /** A file's real path, links and the filesystem's case resolved, or `null` when there is no such file. */
254
+ function realFile(path) {
255
+ try {
256
+ return realpathSync.native(path);
257
+ }
258
+ catch {
259
+ return null;
260
+ }
261
+ }
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
+ /**
277
+ * Whether a path spec-brief's `plugins` names, read from the root, is this
278
+ * package's plugin: the file its `./spec-brief-plugin` export names, as the
279
+ * project installed the package, or as this copy of it ships. spec-brief
280
+ * loads a plugin by a path as well as by its name, and a path to the plugin
281
+ * read as another plugin would say the archive cannot accept a ruling it can.
282
+ */
283
+ export function pluginFile(root) {
284
+ const installed = join(root, 'node_modules', '@descent-vtt', 'spec-harness');
285
+ const candidates = [fileURLToPath(new URL('../dist/plugin.js', import.meta.url))];
286
+ try {
287
+ const manifest = JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8'));
288
+ const target = exportTarget(manifest.exports?.['./spec-brief-plugin']);
289
+ if (target !== null)
290
+ candidates.push(join(installed, target));
291
+ }
292
+ catch {
293
+ // Not installed at the root, or its manifest cannot be read: this copy's file is the one to compare.
294
+ }
295
+ const files = new Set(candidates.map(realFile).filter((file) => file !== null));
296
+ return (path) => {
297
+ const file = realFile(resolve(root, path));
298
+ return file !== null && files.has(file);
299
+ };
244
300
  }
245
301
  /** Whether spec-brief loads this package's plugin, as its configuration at the root says. */
246
302
  export async function specBriefPlugin(root) {
@@ -250,7 +306,7 @@ export async function specBriefPlugin(root) {
250
306
  const config = await readJsonObject(join(root, file));
251
307
  if (config === null || config === 'unreadable')
252
308
  return { kind: 'unreadable', file };
253
- return { kind: loadsPlugin(config) ? 'loaded' : 'not-loaded', file };
309
+ return { kind: loadsPlugin(config, pluginFile(root)) ? 'loaded' : 'not-loaded', file };
254
310
  }
255
311
  /**
256
312
  * Claude Code's settings files that can turn a plugin on or hold a hook,
@@ -295,6 +351,7 @@ async function archiveReasons(workspace, brief, base) {
295
351
  const plan = answer.document.plan;
296
352
  return { blocking: plan?.blocking ?? [], warnings: plan?.warnings ?? [] };
297
353
  }
354
+ /** The brief's assertions as spec-guard ran them, and those it could not read, which it lists apart as `errors`. */
298
355
  async function briefAssertions(workspace, brief, text, reader) {
299
356
  const answer = await workspace.siblings.json('spec-guard', [brief.file, '--ignore-status', '--json']);
300
357
  if ('absent' in answer)
@@ -302,12 +359,15 @@ async function briefAssertions(workspace, brief, text, reader) {
302
359
  if (answer.code === 2)
303
360
  return { unavailable: 'spec-guard could not run the brief\'s assertions' };
304
361
  const report = answer.document;
305
- return (report.results ?? [])
306
- .filter((result) => result.spec === undefined || result.spec.file.replace(/\\/g, '/') === brief.file)
362
+ const own = (spec) => spec === undefined || spec.file.replace(/\\/g, '/') === brief.file;
363
+ const outcomes = (report.results ?? [])
364
+ .filter((result) => own(result.spec))
307
365
  .map((result) => {
308
366
  const line = result.spec?.line ?? 0;
309
367
  return { ok: result.ok, description: result.description, message: result.message, line, section: reader.sectionAt(text, line), enclosing: reader.sectionsAt(text, line) };
310
368
  });
369
+ const unreadable = (report.errors ?? []).filter((error) => own(error.spec)).map((error) => ({ message: error.message, line: error.spec?.line ?? 0, raw: error.raw ?? '' }));
370
+ return { outcomes, unreadable };
311
371
  }
312
372
  export async function runAudit(workspace, brief, reader, baseFlag) {
313
373
  const base = await resolveBase(workspace, baseFlag);
@@ -325,24 +385,15 @@ export async function runAudit(workspace, brief, reader, baseFlag) {
325
385
  unmeasured,
326
386
  dependencies,
327
387
  archive,
328
- assertions,
388
+ assertions: 'unavailable' in assertions ? assertions : assertions.outcomes,
389
+ unreadableAssertions: 'unavailable' in assertions ? [] : assertions.unreadable,
329
390
  premiseSections: workspace.config.assertions.premises,
330
391
  unverifiedRulings: rulings.unverified,
331
392
  verifiedRulings: rulings.verified,
332
393
  pluginLoaded: plugin.kind === 'loaded',
333
394
  });
334
- return {
335
- brief,
336
- base,
337
- dependencies: dependencies.changes,
338
- report: { findings: [...rulings.problems, ...report.findings], counts: tally([...rulings.problems, ...report.findings]) },
339
- };
340
- }
341
- function tally(findings) {
342
- const counts = { error: 0, warning: 0, note: 0 };
343
- for (const finding of findings)
344
- counts[finding.severity] += 1;
345
- return counts;
395
+ const findings = [...rulings.problems, ...report.findings];
396
+ return { brief, base, dependencies: dependencies.changes, report: { findings, counts: tally(findings), measured: report.measured } };
346
397
  }
347
398
  /* -------------------------------------------------------------- escalation */
348
399
  async function escalationDirectory(workspace) {