@descent-vtt/spec-harness 0.0.0 → 0.1.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 (161) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/.claude-plugin/plugin.json +17 -0
  3. package/.mcp.json +12 -0
  4. package/CHANGELOG.md +46 -0
  5. package/README.md +127 -9
  6. package/bin/spec-harness.js +19 -0
  7. package/dist/audit.d.ts +69 -0
  8. package/dist/audit.js +93 -0
  9. package/dist/audit.js.map +1 -0
  10. package/dist/branch.d.ts +26 -0
  11. package/dist/branch.js +100 -0
  12. package/dist/branch.js.map +1 -0
  13. package/dist/briefs.d.ts +49 -0
  14. package/dist/briefs.js +111 -0
  15. package/dist/briefs.js.map +1 -0
  16. package/dist/cli.d.ts +14 -0
  17. package/dist/cli.js +245 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/commands.d.ts +18 -0
  20. package/dist/commands.js +224 -0
  21. package/dist/commands.js.map +1 -0
  22. package/dist/config.d.ts +57 -0
  23. package/dist/config.js +167 -0
  24. package/dist/config.js.map +1 -0
  25. package/dist/context.d.ts +56 -0
  26. package/dist/context.js +125 -0
  27. package/dist/context.js.map +1 -0
  28. package/dist/document.d.ts +33 -0
  29. package/dist/document.js +9 -0
  30. package/dist/document.js.map +1 -0
  31. package/dist/fs.d.ts +29 -0
  32. package/dist/fs.js +85 -0
  33. package/dist/fs.js.map +1 -0
  34. package/dist/git.d.ts +57 -0
  35. package/dist/git.js +129 -0
  36. package/dist/git.js.map +1 -0
  37. package/dist/guard.d.ts +43 -0
  38. package/dist/guard.js +120 -0
  39. package/dist/guard.js.map +1 -0
  40. package/dist/hooks.d.ts +50 -0
  41. package/dist/hooks.js +109 -0
  42. package/dist/hooks.js.map +1 -0
  43. package/dist/index.d.ts +31 -0
  44. package/dist/index.js +20 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/junit.d.ts +30 -0
  47. package/dist/junit.js +166 -0
  48. package/dist/junit.js.map +1 -0
  49. package/dist/manifests.d.ts +52 -0
  50. package/dist/manifests.js +346 -0
  51. package/dist/manifests.js.map +1 -0
  52. package/dist/plugin.d.ts +45 -0
  53. package/dist/plugin.js +79 -0
  54. package/dist/plugin.js.map +1 -0
  55. package/dist/premises.d.ts +16 -0
  56. package/dist/premises.js +71 -0
  57. package/dist/premises.js.map +1 -0
  58. package/dist/probe.d.ts +108 -0
  59. package/dist/probe.js +203 -0
  60. package/dist/probe.js.map +1 -0
  61. package/dist/reader.d.ts +8 -0
  62. package/dist/reader.js +105 -0
  63. package/dist/reader.js.map +1 -0
  64. package/dist/round.d.ts +68 -0
  65. package/dist/round.js +339 -0
  66. package/dist/round.js.map +1 -0
  67. package/dist/rulings.d.ts +92 -0
  68. package/dist/rulings.js +160 -0
  69. package/dist/rulings.js.map +1 -0
  70. package/dist/sandbox.d.ts +28 -0
  71. package/dist/sandbox.js +110 -0
  72. package/dist/sandbox.js.map +1 -0
  73. package/dist/server.d.ts +13 -0
  74. package/dist/server.js +212 -0
  75. package/dist/server.js.map +1 -0
  76. package/dist/setup.d.ts +32 -0
  77. package/dist/setup.js +218 -0
  78. package/dist/setup.js.map +1 -0
  79. package/dist/siblings.d.ts +66 -0
  80. package/dist/siblings.js +134 -0
  81. package/dist/siblings.js.map +1 -0
  82. package/dist/types.d.ts +38 -0
  83. package/dist/types.js +3 -0
  84. package/dist/types.js.map +1 -0
  85. package/dist/vendor/spec-core/jsonrpc/index.d.ts +2 -0
  86. package/dist/vendor/spec-core/jsonrpc/index.js +2 -0
  87. package/dist/vendor/spec-core/jsonrpc/index.js.map +1 -0
  88. package/dist/vendor/spec-core/jsonrpc/mcp.d.ts +157 -0
  89. package/dist/vendor/spec-core/jsonrpc/mcp.js +353 -0
  90. package/dist/vendor/spec-core/jsonrpc/mcp.js.map +1 -0
  91. package/dist/vendor/spec-core/markdown/frontmatter.d.ts +103 -0
  92. package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
  93. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
  94. package/dist/vendor/spec-core/markdown/headings.d.ts +45 -0
  95. package/dist/vendor/spec-core/markdown/headings.js +199 -0
  96. package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
  97. package/dist/vendor/spec-core/markdown/index.d.ts +5 -0
  98. package/dist/vendor/spec-core/markdown/index.js +4 -0
  99. package/dist/vendor/spec-core/markdown/index.js.map +1 -0
  100. package/dist/vendor/spec-core/markdown/layout.d.ts +39 -0
  101. package/dist/vendor/spec-core/markdown/layout.js +28 -0
  102. package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
  103. package/dist/vendor/spec-core/markdown/links.d.ts +22 -0
  104. package/dist/vendor/spec-core/markdown/links.js +364 -0
  105. package/dist/vendor/spec-core/markdown/links.js.map +1 -0
  106. package/dist/vendor/spec-core/markdown/lists.d.ts +22 -0
  107. package/dist/vendor/spec-core/markdown/lists.js +127 -0
  108. package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
  109. package/dist/vendor/spec-core/markdown/scan.d.ts +36 -0
  110. package/dist/vendor/spec-core/markdown/scan.js +439 -0
  111. package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
  112. package/dist/vendor/spec-core/markdown/syntax.d.ts +75 -0
  113. package/dist/vendor/spec-core/markdown/syntax.js +181 -0
  114. package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
  115. package/dist/vendor/spec-core/markdown/tables.d.ts +18 -0
  116. package/dist/vendor/spec-core/markdown/tables.js +86 -0
  117. package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
  118. package/dist/vendor/spec-core/markdown/types.d.ts +287 -0
  119. package/dist/vendor/spec-core/markdown/types.js +9 -0
  120. package/dist/vendor/spec-core/markdown/types.js.map +1 -0
  121. package/dist/vendor/spec-core/path/index.d.ts +1 -0
  122. package/dist/vendor/spec-core/path/index.js +2 -0
  123. package/dist/vendor/spec-core/path/index.js.map +1 -0
  124. package/dist/vendor/spec-core/path/posix.d.ts +83 -0
  125. package/dist/vendor/spec-core/path/posix.js +150 -0
  126. package/dist/vendor/spec-core/path/posix.js.map +1 -0
  127. package/dist/vendor/spec-core/pattern/automaton.d.ts +129 -0
  128. package/dist/vendor/spec-core/pattern/automaton.js +340 -0
  129. package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
  130. package/dist/vendor/spec-core/pattern/charset.d.ts +58 -0
  131. package/dist/vendor/spec-core/pattern/charset.js +116 -0
  132. package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
  133. package/dist/vendor/spec-core/pattern/glob.d.ts +121 -0
  134. package/dist/vendor/spec-core/pattern/glob.js +481 -0
  135. package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
  136. package/dist/vendor/spec-core/pattern/index.d.ts +6 -0
  137. package/dist/vendor/spec-core/pattern/index.js +4 -0
  138. package/dist/vendor/spec-core/pattern/index.js.map +1 -0
  139. package/dist/vendor/spec-core/pattern/regex.d.ts +67 -0
  140. package/dist/vendor/spec-core/pattern/regex.js +813 -0
  141. package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
  142. package/dist/vendor/spec-core/text/index.d.ts +2 -0
  143. package/dist/vendor/spec-core/text/index.js +2 -0
  144. package/dist/vendor/spec-core/text/index.js.map +1 -0
  145. package/dist/vendor/spec-core/text/lines.d.ts +72 -0
  146. package/dist/vendor/spec-core/text/lines.js +156 -0
  147. package/dist/vendor/spec-core/text/lines.js.map +1 -0
  148. package/dist/versions.d.ts +42 -0
  149. package/dist/versions.js +57 -0
  150. package/dist/versions.js.map +1 -0
  151. package/dist/workspace.d.ts +77 -0
  152. package/dist/workspace.js +138 -0
  153. package/dist/workspace.js.map +1 -0
  154. package/hooks/hooks.json +28 -0
  155. package/package.json +86 -6
  156. package/skills/close-round/SKILL.md +38 -0
  157. package/skills/draft-brief/SKILL.md +64 -0
  158. package/skills/run-round/SKILL.md +46 -0
  159. package/skills/split-goal/SKILL.md +42 -0
  160. package/src/vendor/spec-core/LICENSE +21 -0
  161. package/src/vendor/spec-core/README.md +8 -0
@@ -0,0 +1,224 @@
1
+ /**
2
+ * The commands that read a round: context, audit, escalate, rule, rulings
3
+ * and probe. Each resolves the brief, asks `round.ts` for the facts and
4
+ * prints them; exit codes follow the family contract.
5
+ */
6
+ import { existsSync } from 'node:fs';
7
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
8
+ import { dirname, join } from 'node:path';
9
+ import { briefIdFromBranch } from './branch.js';
10
+ import { findActive } from './briefs.js';
11
+ import { readJUnit } from './junit.js';
12
+ import { classify, readProbes, renderEvidence, verdictOf } from './probe.js';
13
+ import { createReader } from './reader.js';
14
+ import { buildContext, checkRulings, briefText, listEscalations, raiseEscalation, recordRuling, resolveBase, runAudit, } from './round.js';
15
+ import { renderMemo } from './rulings.js';
16
+ import { runCommand, withWorktree } from './sandbox.js';
17
+ import { describeActive, EXIT_ERROR, EXIT_FAILED, EXIT_OK, json, UsageError, openWorkspace, } from './workspace.js';
18
+ const reader = createReader();
19
+ /** The brief a command acts on: a positional id, else the active one. A command that needs one and has none is a usage error. */
20
+ export async function targetBrief(workspace, options, io, positional) {
21
+ const briefs = await workspace.siblings.briefs();
22
+ const fromBranch = workspace.branch === null ? null : briefIdFromBranch(workspace.config.branches, workspace.branch);
23
+ const active = findActive(briefs, { flag: positional ?? options.brief, environment: io.env['SPEC_BRIEF'], branch: fromBranch });
24
+ const { brief, note, problem } = describeActive(active);
25
+ if (problem !== null)
26
+ throw new UsageError(problem);
27
+ if (brief === null)
28
+ throw new UsageError(note ?? 'no brief is named');
29
+ return { brief, briefs };
30
+ }
31
+ const MARK = { error: 'error ', warning: 'warning', note: 'note ' };
32
+ function printFindings(io, findings) {
33
+ for (const finding of findings) {
34
+ const where = finding.file === undefined ? '' : `${finding.file}${finding.line === undefined ? '' : `:${finding.line}`} `;
35
+ io.stdout.write(`${MARK[finding.severity]} ${where}${finding.message} ${finding.rule}\n ${finding.hint}\n`);
36
+ }
37
+ }
38
+ /* ----------------------------------------------------------------- context */
39
+ export async function contextCommand(options, io) {
40
+ const workspace = await openWorkspace(options, io);
41
+ const { brief, briefs } = await targetBrief(workspace, options, io, options.positionals[0]);
42
+ const packet = await buildContext(workspace, brief, briefs, reader);
43
+ if (options.format === 'json') {
44
+ io.stdout.write(json('context', { brief: brief.id, markdown: packet.markdown, included: packet.included, omitted: packet.omitted, unresolved: packet.unresolved }));
45
+ }
46
+ else {
47
+ io.stdout.write(packet.markdown);
48
+ }
49
+ return EXIT_OK;
50
+ }
51
+ /* ------------------------------------------------------------------- audit */
52
+ export async function auditCommand(options, io) {
53
+ const workspace = await openWorkspace(options, io);
54
+ const { brief } = await targetBrief(workspace, options, io, options.positionals[0]);
55
+ const result = await runAudit(workspace, brief, reader, options.base);
56
+ const { counts, findings } = result.report;
57
+ if (options.format === 'json') {
58
+ io.stdout.write(json('audit', {
59
+ ok: counts.error === 0 && (!options.strict || counts.warning === 0),
60
+ brief: brief.id,
61
+ base: result.base.kind === 'resolved' ? { ref: result.base.ref, mergeBase: result.base.mergeBase, head: result.base.head } : null,
62
+ counts,
63
+ findings,
64
+ dependencies: result.dependencies,
65
+ }));
66
+ }
67
+ else {
68
+ io.stdout.write(`audit of brief ${brief.id}${result.base.kind === 'resolved' ? ` from ${result.base.ref} (${result.base.mergeBase.slice(0, 12)})` : ''}\n\n`);
69
+ printFindings(io, findings);
70
+ io.stdout.write(`\n${counts.error} error(s), ${counts.warning} warning(s), ${counts.note} note(s)\n`);
71
+ }
72
+ return counts.error > 0 || (options.strict && counts.warning > 0) ? EXIT_FAILED : EXIT_OK;
73
+ }
74
+ /* -------------------------------------------------------------- escalation */
75
+ function parseOption(raw) {
76
+ const colon = raw.indexOf(':');
77
+ return colon < 0 ? { label: raw.trim(), consequence: '' } : { label: raw.slice(0, colon).trim(), consequence: raw.slice(colon + 1).trim() };
78
+ }
79
+ export async function escalateCommand(options, io) {
80
+ const workspace = await openWorkspace(options, io);
81
+ if (options.list) {
82
+ const waiting = await listEscalations(workspace);
83
+ if (options.format === 'json')
84
+ io.stdout.write(json('escalate', { waiting }));
85
+ else if (waiting.length === 0)
86
+ io.stdout.write('no escalation is waiting\n');
87
+ else
88
+ for (const request of waiting)
89
+ io.stdout.write(`${request.id} brief ${request.brief} ${request.paths.join(', ')} ${request.reason.split('\n')[0]}\n`);
90
+ return EXIT_OK;
91
+ }
92
+ if (options.show !== undefined) {
93
+ const request = (await listEscalations(workspace)).find((candidate) => candidate.id === options.show);
94
+ if (request === undefined)
95
+ throw new UsageError(`no escalation "${options.show}" is waiting`);
96
+ io.stdout.write(options.format === 'json' ? json('escalate', { request }) : renderMemo(request));
97
+ return EXIT_OK;
98
+ }
99
+ if (options.paths.length === 0 || options.reason === undefined) {
100
+ throw new UsageError('escalate needs --path <file> (repeatable) and --reason <why>; or --list, or --show <id>');
101
+ }
102
+ const { brief } = await targetBrief(workspace, options, io, undefined);
103
+ const { request, memo } = await raiseEscalation(workspace, brief, {
104
+ paths: options.paths,
105
+ reason: options.reason,
106
+ options: options.options.map(parseOption),
107
+ recommendation: options.recommend ?? null,
108
+ });
109
+ io.stdout.write(options.format === 'json' ? json('escalate', { request, memo }) : memo);
110
+ // Exit 1: the round is waiting on a person, which a caller must not read as done.
111
+ return EXIT_FAILED;
112
+ }
113
+ export async function ruleCommand(options, io) {
114
+ const id = options.positionals[0];
115
+ if (id === undefined)
116
+ throw new UsageError('rule needs the escalation id, such as E-012-1');
117
+ if (options.allow === options.deny)
118
+ throw new UsageError('rule needs exactly one of --allow or --deny');
119
+ if (options.note === undefined || options.note.trim() === '')
120
+ throw new UsageError('rule needs --note: what exactly is allowed, or why not');
121
+ const workspace = await openWorkspace(options, io);
122
+ const recorded = await recordRuling(workspace, id, options.allow ? 'allow' : 'deny', options.note, reader);
123
+ const command = `git commit -S -m "ruling ${recorded.id}: ${options.allow ? 'allow' : 'deny'}" -- ${recorded.file}`;
124
+ if (options.format === 'json') {
125
+ io.stdout.write(json('rule', { ruling: recorded.id, file: recorded.file, row: recorded.row, commit: command }));
126
+ }
127
+ else {
128
+ io.stdout.write(`${recorded.file} now holds ruling ${recorded.id}:\n\n ${recorded.row}\n\nIt counts once you commit it signed, with a key the base branch's allowed signers list:\n\n ${command}\n`);
129
+ }
130
+ return EXIT_OK;
131
+ }
132
+ export async function rulingsCommand(options, io) {
133
+ const workspace = await openWorkspace(options, io);
134
+ const { brief } = await targetBrief(workspace, options, io, options.positionals[0]);
135
+ const base = await resolveBase(workspace, options.base);
136
+ const check = await checkRulings(workspace, brief, await briefText(workspace, brief), reader, base);
137
+ if (options.format === 'json') {
138
+ io.stdout.write(json('rulings', { brief: brief.id, rows: check.rows, verified: check.verified, unverified: check.unverified, problems: check.problems }));
139
+ }
140
+ else {
141
+ if (check.rows.length === 0)
142
+ io.stdout.write(`brief ${brief.id} holds no ruling\n`);
143
+ for (const row of check.rows) {
144
+ const verified = check.verified.find((ruling) => ruling.id === row.id);
145
+ const unverified = check.unverified.find((ruling) => ruling.id === row.id);
146
+ const state = row.decision === 'deny' ? 'refused' : verified !== undefined ? `signed by ${verified.signer}` : `not verified: ${unverified?.reason ?? ''}`;
147
+ io.stdout.write(`${row.id} ${row.decision} ${row.paths.join(', ')} ${state}\n`);
148
+ }
149
+ printFindings(io, check.problems);
150
+ }
151
+ return check.unverified.length > 0 || check.problems.length > 0 ? EXIT_FAILED : EXIT_OK;
152
+ }
153
+ /* ------------------------------------------------------------------- probe */
154
+ async function runProbe(directory, probe, runs, timeout) {
155
+ const classified = [];
156
+ for (let i = 0; i < runs; i += 1) {
157
+ const report = probe.junit === null ? null : join(directory, probe.junit);
158
+ if (report !== null)
159
+ await rm(report, { force: true });
160
+ const run = await runCommand(probe.run, directory, timeout);
161
+ const junit = report === null || !existsSync(report) ? null : readJUnit(await readFile(report, 'utf8'));
162
+ classified.push(classify(probe, { exitCode: run.exitCode, output: run.output, junit }));
163
+ }
164
+ return classified;
165
+ }
166
+ export async function probeCommand(options, io) {
167
+ const workspace = await openWorkspace(options, io);
168
+ const { brief } = await targetBrief(workspace, options, io, options.positionals[0]);
169
+ const text = await briefText(workspace, brief);
170
+ const set = readProbes(reader.codeBlocks(text));
171
+ if (set.problems.length > 0) {
172
+ for (const problem of set.problems)
173
+ io.stderr.write(`spec-harness: ${brief.file}:${problem.line}: ${problem.message}\n`);
174
+ return EXIT_ERROR;
175
+ }
176
+ const probes = set.probes.filter((probe) => options.id === undefined || probe.id === options.id);
177
+ if (probes.length === 0) {
178
+ io.stdout.write(`brief ${brief.id} declares no probe${options.id === undefined ? '' : ` named ${options.id}`}\n`);
179
+ return options.id === undefined ? EXIT_OK : EXIT_ERROR;
180
+ }
181
+ const base = await resolveBase(workspace, options.base);
182
+ if (base.kind === 'unresolved')
183
+ throw new UsageError(base.reason);
184
+ const at = options.at ?? (base.mergeBase === base.head ? 'base' : 'both');
185
+ const results = [];
186
+ const measure = async (label, commit, expected) => {
187
+ await withWorktree(workspace.root, commit, async (directory) => {
188
+ for (const file of set.files) {
189
+ await mkdir(dirname(join(directory, file.path)), { recursive: true });
190
+ await writeFile(join(directory, file.path), file.content.endsWith('\n') ? file.content : `${file.content}\n`);
191
+ }
192
+ const setups = [...new Set(probes.map((probe) => probe.setup).filter((setup) => setup !== null))];
193
+ for (const setup of setups) {
194
+ const run = await runCommand(setup, directory, workspace.config.probes.timeout);
195
+ if (run.exitCode !== 0)
196
+ throw new UsageError(`the probe setup "${setup}" failed at ${label}:\n${run.output.slice(-2000)}`);
197
+ }
198
+ for (const probe of probes) {
199
+ const runs = await runProbe(directory, probe, probe.runs ?? workspace.config.probes.runs, probe.timeout ?? workspace.config.probes.timeout);
200
+ results.push({ probe, at: label, commit, expected, runs, verdict: verdictOf(expected, runs) });
201
+ }
202
+ });
203
+ };
204
+ if (at === 'base' || at === 'both')
205
+ await measure('base', base.mergeBase, 'red');
206
+ if (at === 'head' || at === 'both')
207
+ await measure('head', base.head, 'green');
208
+ const evidence = renderEvidence(results, set.hash, new Date().toISOString().slice(0, 10));
209
+ const failed = results.filter((result) => result.verdict !== (result.expected === 'red' ? 'measured' : 'fixed'));
210
+ if (options.format === 'json') {
211
+ io.stdout.write(json('probe', {
212
+ ok: failed.length === 0,
213
+ brief: brief.id,
214
+ hash: set.hash,
215
+ results: results.map((result) => ({ id: result.probe.id, at: result.at, commit: result.commit, expected: result.expected, verdict: result.verdict, runs: result.runs })),
216
+ evidence,
217
+ }));
218
+ }
219
+ else {
220
+ io.stdout.write(`${evidence}\n`);
221
+ }
222
+ return failed.length === 0 ? EXIT_OK : EXIT_FAILED;
223
+ }
224
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The commands that read a round: context, audit, escalate, rule, rulings\n * and probe. Each resolves the brief, asks `round.ts` for the facts and\n * prints them; exit codes follow the family contract.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\n\nimport { briefIdFromBranch } from './branch.js';\nimport { findActive } from './briefs.js';\nimport { readJUnit } from './junit.js';\nimport { classify, readProbes, renderEvidence, verdictOf, type ProbeResult, type ProbeSpec } from './probe.js';\nimport { createReader } from './reader.js';\nimport {\n buildContext,\n checkRulings,\n briefText,\n listEscalations,\n raiseEscalation,\n recordRuling,\n resolveBase,\n runAudit,\n} from './round.js';\nimport { renderMemo, type EscalationOption } from './rulings.js';\nimport { runCommand, withWorktree } from './sandbox.js';\nimport type { BriefRow, Finding } from './types.js';\nimport {\n describeActive,\n EXIT_ERROR,\n EXIT_FAILED,\n EXIT_OK,\n json,\n UsageError,\n type CliIO,\n type Options,\n type Workspace,\n openWorkspace,\n} from './workspace.js';\n\nconst reader = createReader();\n\n/** The brief a command acts on: a positional id, else the active one. A command that needs one and has none is a usage error. */\nexport async function targetBrief(workspace: Workspace, options: Options, io: CliIO, positional: string | undefined): Promise<{ brief: BriefRow; briefs: BriefRow[] }> {\n const briefs = await workspace.siblings.briefs();\n const fromBranch = workspace.branch === null ? null : briefIdFromBranch(workspace.config.branches, workspace.branch);\n const active = findActive(briefs, { flag: positional ?? options.brief, environment: io.env['SPEC_BRIEF'], branch: fromBranch });\n const { brief, note, problem } = describeActive(active);\n if (problem !== null) throw new UsageError(problem);\n if (brief === null) throw new UsageError(note ?? 'no brief is named');\n return { brief, briefs };\n}\n\nconst MARK: Record<Finding['severity'], string> = { error: 'error ', warning: 'warning', note: 'note ' };\n\nfunction printFindings(io: CliIO, findings: readonly Finding[]): void {\n for (const finding of findings) {\n const where = finding.file === undefined ? '' : `${finding.file}${finding.line === undefined ? '' : `:${finding.line}`} `;\n io.stdout.write(`${MARK[finding.severity]} ${where}${finding.message} ${finding.rule}\\n ${finding.hint}\\n`);\n }\n}\n\n/* ----------------------------------------------------------------- context */\n\nexport async function contextCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const { brief, briefs } = await targetBrief(workspace, options, io, options.positionals[0]);\n const packet = await buildContext(workspace, brief, briefs, reader);\n if (options.format === 'json') {\n io.stdout.write(json('context', { brief: brief.id, markdown: packet.markdown, included: packet.included, omitted: packet.omitted, unresolved: packet.unresolved }));\n } else {\n io.stdout.write(packet.markdown);\n }\n return EXIT_OK;\n}\n\n/* ------------------------------------------------------------------- audit */\n\nexport async function auditCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const { brief } = await targetBrief(workspace, options, io, options.positionals[0]);\n const result = await runAudit(workspace, brief, reader, options.base);\n const { counts, findings } = result.report;\n if (options.format === 'json') {\n io.stdout.write(\n json('audit', {\n ok: counts.error === 0 && (!options.strict || counts.warning === 0),\n brief: brief.id,\n base: result.base.kind === 'resolved' ? { ref: result.base.ref, mergeBase: result.base.mergeBase, head: result.base.head } : null,\n counts,\n findings,\n dependencies: result.dependencies,\n }),\n );\n } else {\n io.stdout.write(`audit of brief ${brief.id}${result.base.kind === 'resolved' ? ` from ${result.base.ref} (${result.base.mergeBase.slice(0, 12)})` : ''}\\n\\n`);\n printFindings(io, findings);\n io.stdout.write(`\\n${counts.error} error(s), ${counts.warning} warning(s), ${counts.note} note(s)\\n`);\n }\n return counts.error > 0 || (options.strict && counts.warning > 0) ? EXIT_FAILED : EXIT_OK;\n}\n\n/* -------------------------------------------------------------- escalation */\n\nfunction parseOption(raw: string): EscalationOption {\n const colon = raw.indexOf(':');\n return colon < 0 ? { label: raw.trim(), consequence: '' } : { label: raw.slice(0, colon).trim(), consequence: raw.slice(colon + 1).trim() };\n}\n\nexport async function escalateCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n if (options.list) {\n const waiting = await listEscalations(workspace);\n if (options.format === 'json') io.stdout.write(json('escalate', { waiting }));\n else if (waiting.length === 0) io.stdout.write('no escalation is waiting\\n');\n else for (const request of waiting) io.stdout.write(`${request.id} brief ${request.brief} ${request.paths.join(', ')} ${request.reason.split('\\n')[0]}\\n`);\n return EXIT_OK;\n }\n if (options.show !== undefined) {\n const request = (await listEscalations(workspace)).find((candidate) => candidate.id === options.show);\n if (request === undefined) throw new UsageError(`no escalation \"${options.show}\" is waiting`);\n io.stdout.write(options.format === 'json' ? json('escalate', { request }) : renderMemo(request));\n return EXIT_OK;\n }\n if (options.paths.length === 0 || options.reason === undefined) {\n throw new UsageError('escalate needs --path <file> (repeatable) and --reason <why>; or --list, or --show <id>');\n }\n const { brief } = await targetBrief(workspace, options, io, undefined);\n const { request, memo } = await raiseEscalation(workspace, brief, {\n paths: options.paths,\n reason: options.reason,\n options: options.options.map(parseOption),\n recommendation: options.recommend ?? null,\n });\n io.stdout.write(options.format === 'json' ? json('escalate', { request, memo }) : memo);\n // Exit 1: the round is waiting on a person, which a caller must not read as done.\n return EXIT_FAILED;\n}\n\nexport async function ruleCommand(options: Options, io: CliIO): Promise<number> {\n const id = options.positionals[0];\n if (id === undefined) throw new UsageError('rule needs the escalation id, such as E-012-1');\n if (options.allow === options.deny) throw new UsageError('rule needs exactly one of --allow or --deny');\n if (options.note === undefined || options.note.trim() === '') throw new UsageError('rule needs --note: what exactly is allowed, or why not');\n const workspace = await openWorkspace(options, io);\n const recorded = await recordRuling(workspace, id, options.allow ? 'allow' : 'deny', options.note, reader);\n const command = `git commit -S -m \"ruling ${recorded.id}: ${options.allow ? 'allow' : 'deny'}\" -- ${recorded.file}`;\n if (options.format === 'json') {\n io.stdout.write(json('rule', { ruling: recorded.id, file: recorded.file, row: recorded.row, commit: command }));\n } else {\n io.stdout.write(`${recorded.file} now holds ruling ${recorded.id}:\\n\\n ${recorded.row}\\n\\nIt counts once you commit it signed, with a key the base branch's allowed signers list:\\n\\n ${command}\\n`);\n }\n return EXIT_OK;\n}\n\nexport async function rulingsCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const { brief } = await targetBrief(workspace, options, io, options.positionals[0]);\n const base = await resolveBase(workspace, options.base);\n const check = await checkRulings(workspace, brief, await briefText(workspace, brief), reader, base);\n if (options.format === 'json') {\n io.stdout.write(json('rulings', { brief: brief.id, rows: check.rows, verified: check.verified, unverified: check.unverified, problems: check.problems }));\n } else {\n if (check.rows.length === 0) io.stdout.write(`brief ${brief.id} holds no ruling\\n`);\n for (const row of check.rows) {\n const verified = check.verified.find((ruling) => ruling.id === row.id);\n const unverified = check.unverified.find((ruling) => ruling.id === row.id);\n const state = row.decision === 'deny' ? 'refused' : verified !== undefined ? `signed by ${verified.signer}` : `not verified: ${unverified?.reason ?? ''}`;\n io.stdout.write(`${row.id} ${row.decision} ${row.paths.join(', ')} ${state}\\n`);\n }\n printFindings(io, check.problems);\n }\n return check.unverified.length > 0 || check.problems.length > 0 ? EXIT_FAILED : EXIT_OK;\n}\n\n/* ------------------------------------------------------------------- probe */\n\nasync function runProbe(directory: string, probe: ProbeSpec, runs: number, timeout: number): Promise<ProbeResult['runs']> {\n const classified = [];\n for (let i = 0; i < runs; i += 1) {\n const report = probe.junit === null ? null : join(directory, probe.junit);\n if (report !== null) await rm(report, { force: true });\n const run = await runCommand(probe.run, directory, timeout);\n const junit = report === null || !existsSync(report) ? null : readJUnit(await readFile(report, 'utf8'));\n classified.push(classify(probe, { exitCode: run.exitCode, output: run.output, junit }));\n }\n return classified;\n}\n\nexport async function probeCommand(options: Options, io: CliIO): Promise<number> {\n const workspace = await openWorkspace(options, io);\n const { brief } = await targetBrief(workspace, options, io, options.positionals[0]);\n const text = await briefText(workspace, brief);\n const set = readProbes(reader.codeBlocks(text));\n if (set.problems.length > 0) {\n for (const problem of set.problems) io.stderr.write(`spec-harness: ${brief.file}:${problem.line}: ${problem.message}\\n`);\n return EXIT_ERROR;\n }\n const probes = set.probes.filter((probe) => options.id === undefined || probe.id === options.id);\n if (probes.length === 0) {\n io.stdout.write(`brief ${brief.id} declares no probe${options.id === undefined ? '' : ` named ${options.id}`}\\n`);\n return options.id === undefined ? EXIT_OK : EXIT_ERROR;\n }\n const base = await resolveBase(workspace, options.base);\n if (base.kind === 'unresolved') throw new UsageError(base.reason);\n const at = options.at ?? (base.mergeBase === base.head ? 'base' : 'both');\n const results: ProbeResult[] = [];\n const measure = async (label: 'base' | 'head', commit: string, expected: 'red' | 'green'): Promise<void> => {\n await withWorktree(workspace.root, commit, async (directory) => {\n for (const file of set.files) {\n await mkdir(dirname(join(directory, file.path)), { recursive: true });\n await writeFile(join(directory, file.path), file.content.endsWith('\\n') ? file.content : `${file.content}\\n`);\n }\n const setups = [...new Set(probes.map((probe) => probe.setup).filter((setup): setup is string => setup !== null))];\n for (const setup of setups) {\n const run = await runCommand(setup, directory, workspace.config.probes.timeout);\n if (run.exitCode !== 0) throw new UsageError(`the probe setup \"${setup}\" failed at ${label}:\\n${run.output.slice(-2000)}`);\n }\n for (const probe of probes) {\n const runs = await runProbe(directory, probe, probe.runs ?? workspace.config.probes.runs, probe.timeout ?? workspace.config.probes.timeout);\n results.push({ probe, at: label, commit, expected, runs, verdict: verdictOf(expected, runs) });\n }\n });\n };\n if (at === 'base' || at === 'both') await measure('base', base.mergeBase, 'red');\n if (at === 'head' || at === 'both') await measure('head', base.head, 'green');\n const evidence = renderEvidence(results, set.hash, new Date().toISOString().slice(0, 10));\n const failed = results.filter((result) => result.verdict !== (result.expected === 'red' ? 'measured' : 'fixed'));\n if (options.format === 'json') {\n io.stdout.write(\n json('probe', {\n ok: failed.length === 0,\n brief: brief.id,\n hash: set.hash,\n results: results.map((result) => ({ id: result.probe.id, at: result.at, commit: result.commit, expected: result.expected, verdict: result.verdict, runs: result.runs })),\n evidence,\n }),\n );\n } else {\n io.stdout.write(`${evidence}\\n`);\n }\n return failed.length === 0 ? EXIT_OK : EXIT_FAILED;\n}\n"]}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * What `.spec-harness.json` may say, the defaults, and validation.
3
+ *
4
+ * Every key has a default, so a repository without the file works; a file
5
+ * that says something this version does not understand stops the run with
6
+ * exit 2, because a guard that silently ignored a misspelt key would be
7
+ * guarding something other than what its owner wrote.
8
+ */
9
+ export type OutOfScope = 'warn' | 'ask' | 'deny';
10
+ export interface HarnessConfig {
11
+ /** Branch-name templates that carry the active brief's id (ADR-0004). */
12
+ readonly branches: readonly string[];
13
+ /** What a write outside the brief's `affectedFiles` gets: a warning, a question to the person, or a refusal. */
14
+ readonly outOfScope: OutOfScope;
15
+ /** The branch rounds merge into, which audits and rulings are measured against. `null`: the remote's default branch. */
16
+ readonly base: string | null;
17
+ readonly rulings: {
18
+ /** The brief section whose table holds the rulings. */
19
+ readonly section: string;
20
+ /** The allowed-signers file, read from the base branch, never from the round's own tree. */
21
+ readonly allowedSigners: string;
22
+ };
23
+ readonly dependencies: {
24
+ /** Manifest file names whose added dependencies an audit reports. */
25
+ readonly manifests: readonly string[];
26
+ };
27
+ readonly context: {
28
+ /** The most a context packet may hold, in characters. Cited documents fill what the rest leaves; those that do not fit are named by path. */
29
+ readonly budget: number;
30
+ };
31
+ readonly assertions: {
32
+ /**
33
+ * Brief sections whose assertions state what was true before the round -
34
+ * the defect, measured - and are expected to stop holding when it ends.
35
+ * Every other assertion in a brief is a goal.
36
+ */
37
+ readonly premises: readonly string[];
38
+ };
39
+ readonly probes: {
40
+ /** How many times a probe runs; every run must agree. */
41
+ readonly runs: number;
42
+ /** Seconds one run may take. */
43
+ readonly timeout: number;
44
+ };
45
+ /** The command for each sibling tool, or `null` to find it (ADR-0002). */
46
+ readonly tools: Readonly<Record<SiblingName, readonly string[] | null>>;
47
+ }
48
+ export type SiblingName = 'spec-brief' | 'spec-graph' | 'spec-guard';
49
+ export declare const SIBLINGS: readonly SiblingName[];
50
+ export declare const CONFIG_FILE = ".spec-harness.json";
51
+ export declare const DEFAULT_MANIFESTS: readonly string[];
52
+ export declare const DEFAULT_CONFIG: HarnessConfig;
53
+ export declare class ConfigError extends Error {
54
+ constructor(message: string);
55
+ }
56
+ /** Validates a parsed configuration and fills in every default. */
57
+ export declare function parseConfig(raw: unknown, file?: string): HarnessConfig;
package/dist/config.js ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * What `.spec-harness.json` may say, the defaults, and validation.
3
+ *
4
+ * Every key has a default, so a repository without the file works; a file
5
+ * that says something this version does not understand stops the run with
6
+ * exit 2, because a guard that silently ignored a misspelt key would be
7
+ * guarding something other than what its owner wrote.
8
+ */
9
+ import { templateError } from './branch.js';
10
+ export const SIBLINGS = ['spec-brief', 'spec-graph', 'spec-guard'];
11
+ export const CONFIG_FILE = '.spec-harness.json';
12
+ export const DEFAULT_MANIFESTS = [
13
+ 'package.json',
14
+ 'Cargo.toml',
15
+ 'go.mod',
16
+ 'pyproject.toml',
17
+ 'requirements*.txt',
18
+ 'Directory.Packages.props',
19
+ '*.csproj',
20
+ '*.fsproj',
21
+ '*.vbproj',
22
+ 'Gemfile',
23
+ ];
24
+ export const DEFAULT_CONFIG = {
25
+ branches: ['brief/{id}', 'brief-{id}', '*/brief/{id}', '*/brief-{id}'],
26
+ outOfScope: 'warn',
27
+ base: null,
28
+ rulings: { section: 'Rulings', allowedSigners: '.github/allowed_signers' },
29
+ dependencies: { manifests: DEFAULT_MANIFESTS },
30
+ context: { budget: 60_000 },
31
+ assertions: { premises: ['The Defect, Measured', 'Premises', 'Preconditions'] },
32
+ probes: { runs: 2, timeout: 600 },
33
+ tools: { 'spec-brief': null, 'spec-graph': null, 'spec-guard': null },
34
+ };
35
+ export class ConfigError extends Error {
36
+ constructor(message) {
37
+ super(message);
38
+ this.name = 'ConfigError';
39
+ }
40
+ }
41
+ function isObject(value) {
42
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
43
+ }
44
+ function isStringList(value) {
45
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
46
+ }
47
+ function checkKeys(value, allowed, where) {
48
+ for (const key of Object.keys(value)) {
49
+ if (!allowed.includes(key)) {
50
+ throw new ConfigError(`${where}: unknown key "${key}"; the keys are ${allowed.join(', ')}`);
51
+ }
52
+ }
53
+ }
54
+ function positive(value, where) {
55
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
56
+ throw new ConfigError(`${where} must be a whole number of at least 1`);
57
+ }
58
+ return value;
59
+ }
60
+ function nonEmpty(value, where) {
61
+ if (typeof value !== 'string' || value.trim() === '')
62
+ throw new ConfigError(`${where} must be a non-empty string`);
63
+ return value;
64
+ }
65
+ /** Validates a parsed configuration and fills in every default. */
66
+ export function parseConfig(raw, file = CONFIG_FILE) {
67
+ if (!isObject(raw))
68
+ throw new ConfigError(`${file} must hold a JSON object`);
69
+ checkKeys(raw, ['$schema', 'branches', 'outOfScope', 'base', 'rulings', 'dependencies', 'context', 'assertions', 'probes', 'tools'], file);
70
+ let branches = DEFAULT_CONFIG.branches;
71
+ if (raw['branches'] !== undefined) {
72
+ if (!isStringList(raw['branches']) || raw['branches'].length === 0) {
73
+ throw new ConfigError(`${file}: "branches" must be a non-empty list of templates`);
74
+ }
75
+ for (const template of raw['branches']) {
76
+ const error = templateError(template);
77
+ if (error !== null)
78
+ throw new ConfigError(`${file}: "branches": ${error}`);
79
+ }
80
+ branches = raw['branches'];
81
+ }
82
+ let outOfScope = DEFAULT_CONFIG.outOfScope;
83
+ if (raw['outOfScope'] !== undefined) {
84
+ if (raw['outOfScope'] !== 'warn' && raw['outOfScope'] !== 'ask' && raw['outOfScope'] !== 'deny') {
85
+ throw new ConfigError(`${file}: "outOfScope" must be "warn", "ask" or "deny"`);
86
+ }
87
+ outOfScope = raw['outOfScope'];
88
+ }
89
+ let base = DEFAULT_CONFIG.base;
90
+ if (raw['base'] !== undefined && raw['base'] !== null)
91
+ base = nonEmpty(raw['base'], `${file}: "base"`);
92
+ let rulings = DEFAULT_CONFIG.rulings;
93
+ if (raw['rulings'] !== undefined) {
94
+ const value = raw['rulings'];
95
+ if (!isObject(value))
96
+ throw new ConfigError(`${file}: "rulings" must be an object`);
97
+ checkKeys(value, ['section', 'allowedSigners'], `${file}: "rulings"`);
98
+ rulings = {
99
+ section: value['section'] === undefined ? rulings.section : nonEmpty(value['section'], `${file}: "rulings.section"`),
100
+ allowedSigners: value['allowedSigners'] === undefined
101
+ ? rulings.allowedSigners
102
+ : nonEmpty(value['allowedSigners'], `${file}: "rulings.allowedSigners"`),
103
+ };
104
+ }
105
+ let dependencies = DEFAULT_CONFIG.dependencies;
106
+ if (raw['dependencies'] !== undefined) {
107
+ const value = raw['dependencies'];
108
+ if (!isObject(value))
109
+ throw new ConfigError(`${file}: "dependencies" must be an object`);
110
+ checkKeys(value, ['manifests'], `${file}: "dependencies"`);
111
+ if (value['manifests'] !== undefined) {
112
+ if (!isStringList(value['manifests']))
113
+ throw new ConfigError(`${file}: "dependencies.manifests" must be a list of file names`);
114
+ dependencies = { manifests: value['manifests'] };
115
+ }
116
+ }
117
+ let context = DEFAULT_CONFIG.context;
118
+ if (raw['context'] !== undefined) {
119
+ const value = raw['context'];
120
+ if (!isObject(value))
121
+ throw new ConfigError(`${file}: "context" must be an object`);
122
+ checkKeys(value, ['budget'], `${file}: "context"`);
123
+ if (value['budget'] !== undefined)
124
+ context = { budget: positive(value['budget'], `${file}: "context.budget"`) };
125
+ }
126
+ let assertions = DEFAULT_CONFIG.assertions;
127
+ if (raw['assertions'] !== undefined) {
128
+ const value = raw['assertions'];
129
+ if (!isObject(value))
130
+ throw new ConfigError(`${file}: "assertions" must be an object`);
131
+ checkKeys(value, ['premises'], `${file}: "assertions"`);
132
+ if (value['premises'] !== undefined) {
133
+ if (!isStringList(value['premises']))
134
+ throw new ConfigError(`${file}: "assertions.premises" must be a list of section names`);
135
+ assertions = { premises: value['premises'] };
136
+ }
137
+ }
138
+ let probes = DEFAULT_CONFIG.probes;
139
+ if (raw['probes'] !== undefined) {
140
+ const value = raw['probes'];
141
+ if (!isObject(value))
142
+ throw new ConfigError(`${file}: "probes" must be an object`);
143
+ checkKeys(value, ['runs', 'timeout'], `${file}: "probes"`);
144
+ probes = {
145
+ runs: value['runs'] === undefined ? probes.runs : positive(value['runs'], `${file}: "probes.runs"`),
146
+ timeout: value['timeout'] === undefined ? probes.timeout : positive(value['timeout'], `${file}: "probes.timeout"`),
147
+ };
148
+ }
149
+ const tools = { ...DEFAULT_CONFIG.tools };
150
+ if (raw['tools'] !== undefined) {
151
+ const value = raw['tools'];
152
+ if (!isObject(value))
153
+ throw new ConfigError(`${file}: "tools" must be an object`);
154
+ checkKeys(value, SIBLINGS, `${file}: "tools"`);
155
+ for (const name of SIBLINGS) {
156
+ const command = value[name];
157
+ if (command === undefined || command === null)
158
+ continue;
159
+ if (!isStringList(command) || command.length === 0 || command[0]?.trim() === '') {
160
+ throw new ConfigError(`${file}: "tools.${name}" must be a command as a list, such as ["npx", "${name}"], or null`);
161
+ }
162
+ tools[name] = command;
163
+ }
164
+ }
165
+ return { branches, outOfScope, base, rulings, dependencies, context, assertions, probes, tools };
166
+ }
167
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `.spec-harness.json` may say, the defaults, and validation.\n *\n * Every key has a default, so a repository without the file works; a file\n * that says something this version does not understand stops the run with\n * exit 2, because a guard that silently ignored a misspelt key would be\n * guarding something other than what its owner wrote.\n */\n\nimport { templateError } from './branch.js';\n\nexport type OutOfScope = 'warn' | 'ask' | 'deny';\n\nexport interface HarnessConfig {\n /** Branch-name templates that carry the active brief's id (ADR-0004). */\n readonly branches: readonly string[];\n /** What a write outside the brief's `affectedFiles` gets: a warning, a question to the person, or a refusal. */\n readonly outOfScope: OutOfScope;\n /** The branch rounds merge into, which audits and rulings are measured against. `null`: the remote's default branch. */\n readonly base: string | null;\n readonly rulings: {\n /** The brief section whose table holds the rulings. */\n readonly section: string;\n /** The allowed-signers file, read from the base branch, never from the round's own tree. */\n readonly allowedSigners: string;\n };\n readonly dependencies: {\n /** Manifest file names whose added dependencies an audit reports. */\n readonly manifests: readonly string[];\n };\n readonly context: {\n /** The most a context packet may hold, in characters. Cited documents fill what the rest leaves; those that do not fit are named by path. */\n readonly budget: number;\n };\n readonly assertions: {\n /**\n * Brief sections whose assertions state what was true before the round -\n * the defect, measured - and are expected to stop holding when it ends.\n * Every other assertion in a brief is a goal.\n */\n readonly premises: readonly string[];\n };\n readonly probes: {\n /** How many times a probe runs; every run must agree. */\n readonly runs: number;\n /** Seconds one run may take. */\n readonly timeout: number;\n };\n /** The command for each sibling tool, or `null` to find it (ADR-0002). */\n readonly tools: Readonly<Record<SiblingName, readonly string[] | null>>;\n}\n\nexport type SiblingName = 'spec-brief' | 'spec-graph' | 'spec-guard';\nexport const SIBLINGS: readonly SiblingName[] = ['spec-brief', 'spec-graph', 'spec-guard'];\n\nexport const CONFIG_FILE = '.spec-harness.json';\n\nexport const DEFAULT_MANIFESTS: readonly string[] = [\n 'package.json',\n 'Cargo.toml',\n 'go.mod',\n 'pyproject.toml',\n 'requirements*.txt',\n 'Directory.Packages.props',\n '*.csproj',\n '*.fsproj',\n '*.vbproj',\n 'Gemfile',\n];\n\nexport const DEFAULT_CONFIG: HarnessConfig = {\n branches: ['brief/{id}', 'brief-{id}', '*/brief/{id}', '*/brief-{id}'],\n outOfScope: 'warn',\n base: null,\n rulings: { section: 'Rulings', allowedSigners: '.github/allowed_signers' },\n dependencies: { manifests: DEFAULT_MANIFESTS },\n context: { budget: 60_000 },\n assertions: { premises: ['The Defect, Measured', 'Premises', 'Preconditions'] },\n probes: { runs: 2, timeout: 600 },\n tools: { 'spec-brief': null, 'spec-graph': null, 'spec-guard': null },\n};\n\nexport class ConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ConfigError';\n }\n}\n\ntype Json = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is Json {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isStringList(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((item) => typeof item === 'string');\n}\n\nfunction checkKeys(value: Json, allowed: readonly string[], where: string): void {\n for (const key of Object.keys(value)) {\n if (!allowed.includes(key)) {\n throw new ConfigError(`${where}: unknown key \"${key}\"; the keys are ${allowed.join(', ')}`);\n }\n }\n}\n\nfunction positive(value: unknown, where: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {\n throw new ConfigError(`${where} must be a whole number of at least 1`);\n }\n return value;\n}\n\nfunction nonEmpty(value: unknown, where: string): string {\n if (typeof value !== 'string' || value.trim() === '') throw new ConfigError(`${where} must be a non-empty string`);\n return value;\n}\n\n/** Validates a parsed configuration and fills in every default. */\nexport function parseConfig(raw: unknown, file = CONFIG_FILE): HarnessConfig {\n if (!isObject(raw)) throw new ConfigError(`${file} must hold a JSON object`);\n checkKeys(raw, ['$schema', 'branches', 'outOfScope', 'base', 'rulings', 'dependencies', 'context', 'assertions', 'probes', 'tools'], file);\n\n let branches = DEFAULT_CONFIG.branches;\n if (raw['branches'] !== undefined) {\n if (!isStringList(raw['branches']) || raw['branches'].length === 0) {\n throw new ConfigError(`${file}: \"branches\" must be a non-empty list of templates`);\n }\n for (const template of raw['branches']) {\n const error = templateError(template);\n if (error !== null) throw new ConfigError(`${file}: \"branches\": ${error}`);\n }\n branches = raw['branches'];\n }\n\n let outOfScope = DEFAULT_CONFIG.outOfScope;\n if (raw['outOfScope'] !== undefined) {\n if (raw['outOfScope'] !== 'warn' && raw['outOfScope'] !== 'ask' && raw['outOfScope'] !== 'deny') {\n throw new ConfigError(`${file}: \"outOfScope\" must be \"warn\", \"ask\" or \"deny\"`);\n }\n outOfScope = raw['outOfScope'];\n }\n\n let base = DEFAULT_CONFIG.base;\n if (raw['base'] !== undefined && raw['base'] !== null) base = nonEmpty(raw['base'], `${file}: \"base\"`);\n\n let rulings = DEFAULT_CONFIG.rulings;\n if (raw['rulings'] !== undefined) {\n const value = raw['rulings'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"rulings\" must be an object`);\n checkKeys(value, ['section', 'allowedSigners'], `${file}: \"rulings\"`);\n rulings = {\n section: value['section'] === undefined ? rulings.section : nonEmpty(value['section'], `${file}: \"rulings.section\"`),\n allowedSigners:\n value['allowedSigners'] === undefined\n ? rulings.allowedSigners\n : nonEmpty(value['allowedSigners'], `${file}: \"rulings.allowedSigners\"`),\n };\n }\n\n let dependencies = DEFAULT_CONFIG.dependencies;\n if (raw['dependencies'] !== undefined) {\n const value = raw['dependencies'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"dependencies\" must be an object`);\n checkKeys(value, ['manifests'], `${file}: \"dependencies\"`);\n if (value['manifests'] !== undefined) {\n if (!isStringList(value['manifests'])) throw new ConfigError(`${file}: \"dependencies.manifests\" must be a list of file names`);\n dependencies = { manifests: value['manifests'] };\n }\n }\n\n let context = DEFAULT_CONFIG.context;\n if (raw['context'] !== undefined) {\n const value = raw['context'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"context\" must be an object`);\n checkKeys(value, ['budget'], `${file}: \"context\"`);\n if (value['budget'] !== undefined) context = { budget: positive(value['budget'], `${file}: \"context.budget\"`) };\n }\n\n let assertions = DEFAULT_CONFIG.assertions;\n if (raw['assertions'] !== undefined) {\n const value = raw['assertions'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"assertions\" must be an object`);\n checkKeys(value, ['premises'], `${file}: \"assertions\"`);\n if (value['premises'] !== undefined) {\n if (!isStringList(value['premises'])) throw new ConfigError(`${file}: \"assertions.premises\" must be a list of section names`);\n assertions = { premises: value['premises'] };\n }\n }\n\n let probes = DEFAULT_CONFIG.probes;\n if (raw['probes'] !== undefined) {\n const value = raw['probes'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"probes\" must be an object`);\n checkKeys(value, ['runs', 'timeout'], `${file}: \"probes\"`);\n probes = {\n runs: value['runs'] === undefined ? probes.runs : positive(value['runs'], `${file}: \"probes.runs\"`),\n timeout: value['timeout'] === undefined ? probes.timeout : positive(value['timeout'], `${file}: \"probes.timeout\"`),\n };\n }\n\n const tools: Record<SiblingName, readonly string[] | null> = { ...DEFAULT_CONFIG.tools };\n if (raw['tools'] !== undefined) {\n const value = raw['tools'];\n if (!isObject(value)) throw new ConfigError(`${file}: \"tools\" must be an object`);\n checkKeys(value, SIBLINGS, `${file}: \"tools\"`);\n for (const name of SIBLINGS) {\n const command = value[name];\n if (command === undefined || command === null) continue;\n if (!isStringList(command) || command.length === 0 || command[0]?.trim() === '') {\n throw new ConfigError(`${file}: \"tools.${name}\" must be a command as a list, such as [\"npx\", \"${name}\"], or null`);\n }\n tools[name] = command;\n }\n }\n\n return { branches, outOfScope, base, rulings, dependencies, context, assertions, probes, tools };\n}\n"]}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * What an agent needs to start a round, and nothing it does not.
3
+ *
4
+ * The brief in full - it is the contract, and a summary of a contract is a
5
+ * different contract. Then what the brief implies but does not say: the scope
6
+ * as the guard will read it, the rulings already signed, the briefs it waits
7
+ * on, the rules spec-guard holds the scope's code to, and the documents the
8
+ * brief cites. The cited documents are the one part that can be large, so
9
+ * they fill what the budget leaves, in the order the brief cites them, and
10
+ * the rest are named rather than dropped silently.
11
+ */
12
+ import type { BriefRow } from './types.js';
13
+ export interface CitedDocument {
14
+ readonly path: string;
15
+ readonly title: string | null;
16
+ readonly status: string | null;
17
+ /** `null` when the link does not resolve to a readable file. */
18
+ readonly text: string | null;
19
+ }
20
+ export interface RuleInForce {
21
+ readonly document: string;
22
+ readonly line: number;
23
+ readonly kind: string;
24
+ readonly description: string;
25
+ readonly reason: string | null;
26
+ }
27
+ export interface RulingInForce {
28
+ readonly id: string;
29
+ readonly paths: readonly string[];
30
+ readonly signer: string;
31
+ }
32
+ export interface ContextInput {
33
+ readonly brief: BriefRow;
34
+ readonly briefText: string;
35
+ readonly dependencies: readonly BriefRow[];
36
+ readonly cited: readonly CitedDocument[];
37
+ /** The rules in force for the scope, or why they could not be read. */
38
+ readonly rules: readonly RuleInForce[] | {
39
+ readonly unavailable: string;
40
+ };
41
+ readonly rulings: readonly RulingInForce[];
42
+ readonly branch: string | null;
43
+ readonly base: string | null;
44
+ readonly budget: number;
45
+ }
46
+ export interface ContextPacket {
47
+ readonly markdown: string;
48
+ /** Cited documents included whole. */
49
+ readonly included: readonly string[];
50
+ /** Cited documents named but left out for the budget. */
51
+ readonly omitted: readonly string[];
52
+ /** Links in the brief that resolve to nothing. */
53
+ readonly unresolved: readonly string[];
54
+ }
55
+ /** Renders the packet, filling the budget with cited documents in the order the brief cites them. */
56
+ export declare function renderContext(input: ContextInput): ContextPacket;