@descent-vtt/spec-harness 0.1.1 → 0.1.3

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 (50) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.mcp.json +5 -4
  4. package/CHANGELOG.md +77 -1
  5. package/README.md +22 -12
  6. package/dist/audit.d.ts +23 -2
  7. package/dist/audit.js +30 -1
  8. package/dist/audit.js.map +1 -1
  9. package/dist/cli.js +30 -5
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands.js +1 -1
  12. package/dist/commands.js.map +1 -1
  13. package/dist/configure.d.ts +111 -0
  14. package/dist/configure.js +226 -0
  15. package/dist/configure.js.map +1 -0
  16. package/dist/context.d.ts +31 -4
  17. package/dist/context.js +40 -7
  18. package/dist/context.js.map +1 -1
  19. package/dist/fs.d.ts +2 -0
  20. package/dist/fs.js +12 -0
  21. package/dist/fs.js.map +1 -1
  22. package/dist/git.d.ts +2 -0
  23. package/dist/git.js +5 -0
  24. package/dist/git.js.map +1 -1
  25. package/dist/guard.d.ts +2 -0
  26. package/dist/guard.js +5 -1
  27. package/dist/guard.js.map +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/plugin.d.ts +2 -1
  32. package/dist/plugin.js +4 -9
  33. package/dist/plugin.js.map +1 -1
  34. package/dist/premises.d.ts +4 -0
  35. package/dist/premises.js +20 -14
  36. package/dist/premises.js.map +1 -1
  37. package/dist/round.d.ts +7 -3
  38. package/dist/round.js +35 -26
  39. package/dist/round.js.map +1 -1
  40. package/dist/server.js +38 -14
  41. package/dist/server.js.map +1 -1
  42. package/dist/setup.d.ts +0 -9
  43. package/dist/setup.js +103 -69
  44. package/dist/setup.js.map +1 -1
  45. package/dist/siblings.d.ts +2 -1
  46. package/dist/siblings.js +1 -1
  47. package/dist/siblings.js.map +1 -1
  48. package/hooks/hooks.json +12 -2
  49. package/package.json +1 -1
  50. package/skills/close-round/SKILL.md +4 -0
package/dist/context.js CHANGED
@@ -10,11 +10,42 @@
10
10
  * the rest are named rather than dropped silently.
11
11
  */
12
12
  import { sameId } from './branch.js';
13
+ /**
14
+ * spec-guard's answer to `query --json`, read. It exits 2 both when no spec
15
+ * file matched its patterns - a repository that keeps none, or keeps them
16
+ * where it does not look - and when it could not read the specs it has; only
17
+ * the first prints a document, one with no spec file in it, so that is how
18
+ * the two are told apart. What it said on stderr is the reason for the second.
19
+ */
20
+ export function readRules(answer) {
21
+ const document = (answer.document ?? {});
22
+ if (answer.code !== 0) {
23
+ if (Array.isArray(document.specFiles) && document.specFiles.length === 0) {
24
+ return { none: 'no spec file matched its patterns ("specs" in its configuration, docs/**/*.md by default)' };
25
+ }
26
+ const said = (answer.stderr.trim().split('\n')[0] ?? '').replace(/^spec-guard:\s*/, '');
27
+ return { unavailable: said === '' ? `spec-guard exited ${answer.code}` : `spec-guard exited ${answer.code}: ${said}` };
28
+ }
29
+ const seen = new Set();
30
+ const out = [];
31
+ for (const result of document.results ?? []) {
32
+ for (const rule of result.rules ?? []) {
33
+ const key = `${rule.document}:${rule.line}`;
34
+ if (rule.inForce === false || seen.has(key))
35
+ continue;
36
+ seen.add(key);
37
+ out.push({ document: rule.document, line: rule.line, kind: rule.kind, description: rule.description, reason: rule.reason ?? null });
38
+ }
39
+ }
40
+ return out;
41
+ }
13
42
  function list(items, empty) {
14
43
  return items.length === 0 ? `- ${empty}` : items.map((item) => `- \`${item}\``).join('\n');
15
44
  }
16
45
  function rulesSection(rules) {
17
- if (!Array.isArray(rules)) {
46
+ if ('none' in rules)
47
+ return `spec-guard holds no rule over this scope: ${rules.none}.`;
48
+ if ('unavailable' in rules) {
18
49
  return `The rules spec-guard holds this code to could not be read: ${rules.unavailable}. Treat every ADR as binding until they can.`;
19
50
  }
20
51
  if (rules.length === 0)
@@ -32,14 +63,15 @@ function rulesSection(rules) {
32
63
  return parts.join('\n').trimEnd();
33
64
  }
34
65
  /**
35
- * A brief's title without the id it repeats, `012 - Rotate tokens` read as
36
- * `Rotate tokens`. Only the brief's own id is taken off: `Fix - the login
37
- * bug` is a title, not an id and a title.
66
+ * A brief's title without the id it repeats: `012 - Rotate tokens` read as
67
+ * `Rotate tokens`, and the same with an en or an em dash, which `spec-brief
68
+ * new` writes, or with a colon after the id. Only the brief's own id is taken
69
+ * off: `Fix - the login bug` is a title, not an id and a title.
38
70
  */
39
- function titleOf(brief) {
71
+ export function titleOf(brief) {
40
72
  if (brief.title === null)
41
73
  return null;
42
- return brief.title.replace(/^\s*(\S+)\s+-\s+/, (whole, first) => (sameId(first, brief.id) ? '' : whole));
74
+ return brief.title.replace(/^\s*([^\s:]+)(?:\s+[-\u2013\u2014]|:)\s+/, (whole, first) => (sameId(first, brief.id) ? '' : whole));
43
75
  }
44
76
  /** Renders the packet, filling the budget with cited documents in the order the brief cites them. */
45
77
  export function renderContext(input) {
@@ -77,7 +109,8 @@ export function renderContext(input) {
77
109
  ].join('\n');
78
110
  const waiting = input.dependencies.map((dependency) => {
79
111
  const state = dependency.phase === 'archived' ? 'archived, done' : `still ${dependency.status ?? 'live'}`;
80
- return `- ${dependency.id}${dependency.title === null ? '' : ` ${dependency.title}`}: ${state}`;
112
+ const named = titleOf(dependency);
113
+ return `- ${dependency.id}${named === null ? '' : ` ${named}`}: ${state}`;
81
114
  });
82
115
  const dependencies = ['## Depends on', '', waiting.length === 0 ? '- nothing' : waiting.join('\n'), ''].join('\n');
83
116
  const rules = ['## Rules in force for this scope', '', rulesSection(input.rules), ''].join('\n');
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What an agent needs to start a round, and nothing it does not.\n *\n * The brief in full - it is the contract, and a summary of a contract is a\n * different contract. Then what the brief implies but does not say: the scope\n * as the guard will read it, the rulings already signed, the briefs it waits\n * on, the rules spec-guard holds the scope's code to, and the documents the\n * brief cites. The cited documents are the one part that can be large, so\n * they fill what the budget leaves, in the order the brief cites them, and\n * the rest are named rather than dropped silently.\n */\n\nimport { sameId } from './branch.js';\nimport type { BriefRow } from './types.js';\n\nexport interface CitedDocument {\n readonly path: string;\n readonly title: string | null;\n readonly status: string | null;\n /** `null` when the link does not resolve to a readable file. */\n readonly text: string | null;\n}\n\nexport interface RuleInForce {\n readonly document: string;\n readonly line: number;\n readonly kind: string;\n readonly description: string;\n readonly reason: string | null;\n}\n\nexport interface RulingInForce {\n readonly id: string;\n readonly paths: readonly string[];\n readonly signer: string;\n}\n\nexport interface ContextInput {\n readonly brief: BriefRow;\n readonly briefText: string;\n readonly dependencies: readonly BriefRow[];\n readonly cited: readonly CitedDocument[];\n /** The rules in force for the scope, or why they could not be read. */\n readonly rules: readonly RuleInForce[] | { readonly unavailable: string };\n readonly rulings: readonly RulingInForce[];\n readonly branch: string | null;\n readonly base: string | null;\n readonly budget: number;\n}\n\nexport interface ContextPacket {\n readonly markdown: string;\n /** Cited documents included whole. */\n readonly included: readonly string[];\n /** Cited documents named but left out for the budget. */\n readonly omitted: readonly string[];\n /** Links in the brief that resolve to nothing. */\n readonly unresolved: readonly string[];\n}\n\nfunction list(items: readonly string[], empty: string): string {\n return items.length === 0 ? `- ${empty}` : items.map((item) => `- \\`${item}\\``).join('\\n');\n}\n\nfunction rulesSection(rules: ContextInput['rules']): string {\n if (!Array.isArray(rules)) {\n return `The rules spec-guard holds this code to could not be read: ${(rules as { unavailable: string }).unavailable}. Treat every ADR as binding until they can.`;\n }\n if (rules.length === 0) return 'spec-guard holds no rule over this scope.';\n const byDocument = new Map<string, RuleInForce[]>();\n for (const rule of rules) byDocument.set(rule.document, [...(byDocument.get(rule.document) ?? []), rule]);\n const parts: string[] = [];\n for (const [document, found] of byDocument) {\n parts.push(`### ${document}`, '');\n for (const rule of found) parts.push(`- line ${rule.line}: ${rule.description}${rule.reason === null ? '' : ` - ${rule.reason}`}`);\n parts.push('');\n }\n return parts.join('\\n').trimEnd();\n}\n\n/**\n * A brief's title without the id it repeats, `012 - Rotate tokens` read as\n * `Rotate tokens`. Only the brief's own id is taken off: `Fix - the login\n * bug` is a title, not an id and a title.\n */\nfunction titleOf(brief: BriefRow): string | null {\n if (brief.title === null) return null;\n return brief.title.replace(/^\\s*(\\S+)\\s+-\\s+/, (whole, first: string) => (sameId(first, brief.id) ? '' : whole));\n}\n\n/** Renders the packet, filling the budget with cited documents in the order the brief cites them. */\nexport function renderContext(input: ContextInput): ContextPacket {\n const { brief } = input;\n const title = titleOf(brief);\n const header = [\n `# Round ${brief.id}${title === null ? '' : `: ${title}`}`,\n '',\n [\n `Status ${brief.status ?? 'unknown'}`,\n brief.wave === null ? null : `wave ${brief.wave}`,\n input.branch === null ? null : `branch \\`${input.branch}\\``,\n input.base === null ? null : `measured from \\`${input.base}\\``,\n ]\n .filter((part) => part !== null)\n .join(' · '),\n '',\n ].join('\\n');\n\n const contract = ['## The contract', '', `\\`${brief.file}\\`, in full:`, '', '````markdown', input.briefText.trimEnd(), '````', ''].join('\\n');\n\n const rulings =\n input.rulings.length === 0\n ? '- none'\n : input.rulings.map((ruling) => `- ${ruling.id}, signed by ${ruling.signer}: ${ruling.paths.map((p) => `\\`${p}\\``).join(', ')}`).join('\\n');\n const scope = [\n '## Scope, as the guard reads it',\n '',\n 'May write:',\n list(brief.affectedFiles, 'nothing declared: every write is outside the scope'),\n '',\n 'Must not change without a ruling:',\n list(brief.protectedFiles, 'nothing declared'),\n '',\n 'Rulings in force:',\n rulings,\n '',\n ].join('\\n');\n\n const waiting = input.dependencies.map((dependency) => {\n const state = dependency.phase === 'archived' ? 'archived, done' : `still ${dependency.status ?? 'live'}`;\n return `- ${dependency.id}${dependency.title === null ? '' : ` ${dependency.title}`}: ${state}`;\n });\n const dependencies = ['## Depends on', '', waiting.length === 0 ? '- nothing' : waiting.join('\\n'), ''].join('\\n');\n\n const rules = ['## Rules in force for this scope', '', rulesSection(input.rules), ''].join('\\n');\n\n const howTo = [\n '## How this round works',\n '',\n '- Before writing a file, ask whether the round may: `spec-harness guard <path>`, or the `check_path` tool.',\n '- A protected file is changed only under a ruling a person signs. If the round cannot be done without one, stop and ask: `spec-harness escalate --path <file> --reason <why>`, or the `request_escalation` tool. Do not work around it.',\n '- Every box in the brief ends ticked, or with a note under it that starts with a disposition (`**Delegated`, `**Accepted debt`, `**Rejected`, or what the repository configures).',\n '- When the work is done, run `spec-harness audit` and fix what it reports. The archive is a person\\'s decision, not the round\\'s.',\n '',\n ].join('\\n');\n\n const fixed = [header, contract, scope, dependencies, rules, howTo].join('\\n');\n const included: string[] = [];\n const omitted: string[] = [];\n const unresolved: string[] = [];\n const documents: string[] = [];\n let used = fixed.length;\n for (const cited of input.cited) {\n if (cited.text === null) {\n unresolved.push(cited.path);\n continue;\n }\n const heading = `### \\`${cited.path}\\`${cited.title === null ? '' : ` - ${cited.title}`}${cited.status === null ? '' : ` (${cited.status})`}`;\n const block = [heading, '', '````markdown', cited.text.trimEnd(), '````', ''].join('\\n');\n if (used + block.length > input.budget) {\n omitted.push(cited.path);\n continue;\n }\n used += block.length;\n included.push(cited.path);\n documents.push(block);\n }\n const citedSection = ['## Documents the brief cites', ''];\n if (documents.length === 0 && omitted.length === 0 && unresolved.length === 0) citedSection.push('None.', '');\n citedSection.push(...documents);\n if (omitted.length > 0) {\n citedSection.push(`Left out to stay within ${input.budget} characters; read them when the work reaches them:`, list(omitted, ''), '');\n }\n if (unresolved.length > 0) citedSection.push('Cited but not found in the repository:', list(unresolved, ''), '');\n\n return { markdown: `${fixed}\\n${citedSection.join('\\n').trimEnd()}\\n`, included, omitted, unresolved };\n}\n"]}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What an agent needs to start a round, and nothing it does not.\n *\n * The brief in full - it is the contract, and a summary of a contract is a\n * different contract. Then what the brief implies but does not say: the scope\n * as the guard will read it, the rulings already signed, the briefs it waits\n * on, the rules spec-guard holds the scope's code to, and the documents the\n * brief cites. The cited documents are the one part that can be large, so\n * they fill what the budget leaves, in the order the brief cites them, and\n * the rest are named rather than dropped silently.\n */\n\nimport { sameId } from './branch.js';\nimport type { BriefRow } from './types.js';\n\nexport interface CitedDocument {\n readonly path: string;\n readonly title: string | null;\n readonly status: string | null;\n /** `null` when the link does not resolve to a readable file. */\n readonly text: string | null;\n}\n\nexport interface RuleInForce {\n readonly document: string;\n readonly line: number;\n readonly kind: string;\n readonly description: string;\n readonly reason: string | null;\n}\n\n/**\n * The rules in force for the scope; `none` when spec-guard reads no\n * specification here, so none can be in force; `unavailable` when the rules\n * could not be read.\n */\nexport type Rules = readonly RuleInForce[] | { readonly none: string } | { readonly unavailable: string };\n\ninterface QueryDocument {\n readonly specFiles?: unknown;\n readonly results?: readonly { readonly rules?: readonly (RuleInForce & { readonly reason?: string | null; readonly inForce?: boolean })[] }[];\n}\n\n/**\n * spec-guard's answer to `query --json`, read. It exits 2 both when no spec\n * file matched its patterns - a repository that keeps none, or keeps them\n * where it does not look - and when it could not read the specs it has; only\n * the first prints a document, one with no spec file in it, so that is how\n * the two are told apart. What it said on stderr is the reason for the second.\n */\nexport function readRules(answer: { readonly code: number; readonly document: unknown; readonly stderr: string }): Rules {\n const document = (answer.document ?? {}) as QueryDocument;\n if (answer.code !== 0) {\n if (Array.isArray(document.specFiles) && document.specFiles.length === 0) {\n return { none: 'no spec file matched its patterns (\"specs\" in its configuration, docs/**/*.md by default)' };\n }\n const said = (answer.stderr.trim().split('\\n')[0] ?? '').replace(/^spec-guard:\\s*/, '');\n return { unavailable: said === '' ? `spec-guard exited ${answer.code}` : `spec-guard exited ${answer.code}: ${said}` };\n }\n const seen = new Set<string>();\n const out: RuleInForce[] = [];\n for (const result of document.results ?? []) {\n for (const rule of result.rules ?? []) {\n const key = `${rule.document}:${rule.line}`;\n if (rule.inForce === false || seen.has(key)) continue;\n seen.add(key);\n out.push({ document: rule.document, line: rule.line, kind: rule.kind, description: rule.description, reason: rule.reason ?? null });\n }\n }\n return out;\n}\n\nexport interface RulingInForce {\n readonly id: string;\n readonly paths: readonly string[];\n readonly signer: string;\n}\n\nexport interface ContextInput {\n readonly brief: BriefRow;\n readonly briefText: string;\n readonly dependencies: readonly BriefRow[];\n readonly cited: readonly CitedDocument[];\n /** The rules in force for the scope, none because spec-guard reads no specification, or why they could not be read. */\n readonly rules: Rules;\n readonly rulings: readonly RulingInForce[];\n readonly branch: string | null;\n readonly base: string | null;\n readonly budget: number;\n}\n\nexport interface ContextPacket {\n readonly markdown: string;\n /** Cited documents included whole. */\n readonly included: readonly string[];\n /** Cited documents named but left out for the budget. */\n readonly omitted: readonly string[];\n /** Links in the brief that resolve to nothing. */\n readonly unresolved: readonly string[];\n}\n\nfunction list(items: readonly string[], empty: string): string {\n return items.length === 0 ? `- ${empty}` : items.map((item) => `- \\`${item}\\``).join('\\n');\n}\n\nfunction rulesSection(rules: Rules): string {\n if ('none' in rules) return `spec-guard holds no rule over this scope: ${rules.none}.`;\n if ('unavailable' in rules) {\n return `The rules spec-guard holds this code to could not be read: ${rules.unavailable}. Treat every ADR as binding until they can.`;\n }\n if (rules.length === 0) return 'spec-guard holds no rule over this scope.';\n const byDocument = new Map<string, RuleInForce[]>();\n for (const rule of rules) byDocument.set(rule.document, [...(byDocument.get(rule.document) ?? []), rule]);\n const parts: string[] = [];\n for (const [document, found] of byDocument) {\n parts.push(`### ${document}`, '');\n for (const rule of found) parts.push(`- line ${rule.line}: ${rule.description}${rule.reason === null ? '' : ` - ${rule.reason}`}`);\n parts.push('');\n }\n return parts.join('\\n').trimEnd();\n}\n\n/**\n * A brief's title without the id it repeats: `012 - Rotate tokens` read as\n * `Rotate tokens`, and the same with an en or an em dash, which `spec-brief\n * new` writes, or with a colon after the id. Only the brief's own id is taken\n * off: `Fix - the login bug` is a title, not an id and a title.\n */\nexport function titleOf(brief: Pick<BriefRow, 'id' | 'title'>): string | null {\n if (brief.title === null) return null;\n return brief.title.replace(/^\\s*([^\\s:]+)(?:\\s+[-\\u2013\\u2014]|:)\\s+/, (whole, first: string) => (sameId(first, brief.id) ? '' : whole));\n}\n\n/** Renders the packet, filling the budget with cited documents in the order the brief cites them. */\nexport function renderContext(input: ContextInput): ContextPacket {\n const { brief } = input;\n const title = titleOf(brief);\n const header = [\n `# Round ${brief.id}${title === null ? '' : `: ${title}`}`,\n '',\n [\n `Status ${brief.status ?? 'unknown'}`,\n brief.wave === null ? null : `wave ${brief.wave}`,\n input.branch === null ? null : `branch \\`${input.branch}\\``,\n input.base === null ? null : `measured from \\`${input.base}\\``,\n ]\n .filter((part) => part !== null)\n .join(' · '),\n '',\n ].join('\\n');\n\n const contract = ['## The contract', '', `\\`${brief.file}\\`, in full:`, '', '````markdown', input.briefText.trimEnd(), '````', ''].join('\\n');\n\n const rulings =\n input.rulings.length === 0\n ? '- none'\n : input.rulings.map((ruling) => `- ${ruling.id}, signed by ${ruling.signer}: ${ruling.paths.map((p) => `\\`${p}\\``).join(', ')}`).join('\\n');\n const scope = [\n '## Scope, as the guard reads it',\n '',\n 'May write:',\n list(brief.affectedFiles, 'nothing declared: every write is outside the scope'),\n '',\n 'Must not change without a ruling:',\n list(brief.protectedFiles, 'nothing declared'),\n '',\n 'Rulings in force:',\n rulings,\n '',\n ].join('\\n');\n\n const waiting = input.dependencies.map((dependency) => {\n const state = dependency.phase === 'archived' ? 'archived, done' : `still ${dependency.status ?? 'live'}`;\n const named = titleOf(dependency);\n return `- ${dependency.id}${named === null ? '' : ` ${named}`}: ${state}`;\n });\n const dependencies = ['## Depends on', '', waiting.length === 0 ? '- nothing' : waiting.join('\\n'), ''].join('\\n');\n\n const rules = ['## Rules in force for this scope', '', rulesSection(input.rules), ''].join('\\n');\n\n const howTo = [\n '## How this round works',\n '',\n '- Before writing a file, ask whether the round may: `spec-harness guard <path>`, or the `check_path` tool.',\n '- A protected file is changed only under a ruling a person signs. If the round cannot be done without one, stop and ask: `spec-harness escalate --path <file> --reason <why>`, or the `request_escalation` tool. Do not work around it.',\n '- Every box in the brief ends ticked, or with a note under it that starts with a disposition (`**Delegated`, `**Accepted debt`, `**Rejected`, or what the repository configures).',\n '- When the work is done, run `spec-harness audit` and fix what it reports. The archive is a person\\'s decision, not the round\\'s.',\n '',\n ].join('\\n');\n\n const fixed = [header, contract, scope, dependencies, rules, howTo].join('\\n');\n const included: string[] = [];\n const omitted: string[] = [];\n const unresolved: string[] = [];\n const documents: string[] = [];\n let used = fixed.length;\n for (const cited of input.cited) {\n if (cited.text === null) {\n unresolved.push(cited.path);\n continue;\n }\n const heading = `### \\`${cited.path}\\`${cited.title === null ? '' : ` - ${cited.title}`}${cited.status === null ? '' : ` (${cited.status})`}`;\n const block = [heading, '', '````markdown', cited.text.trimEnd(), '````', ''].join('\\n');\n if (used + block.length > input.budget) {\n omitted.push(cited.path);\n continue;\n }\n used += block.length;\n included.push(cited.path);\n documents.push(block);\n }\n const citedSection = ['## Documents the brief cites', ''];\n if (documents.length === 0 && omitted.length === 0 && unresolved.length === 0) citedSection.push('None.', '');\n citedSection.push(...documents);\n if (omitted.length > 0) {\n citedSection.push(`Left out to stay within ${input.budget} characters; read them when the work reaches them:`, list(omitted, ''), '');\n }\n if (unresolved.length > 0) citedSection.push('Cited but not found in the repository:', list(unresolved, ''), '');\n\n return { markdown: `${fixed}\\n${citedSection.join('\\n').trimEnd()}\\n`, included, omitted, unresolved };\n}\n"]}
package/dist/fs.d.ts CHANGED
@@ -26,4 +26,6 @@ export declare function repositoryPath(given: string, root: string, cwd: string)
26
26
  export declare function stateDirectory(commonDir: string, ...parts: string[]): Promise<string>;
27
27
  /** Writes a file whole or not at all: a temporary file renamed over the target. */
28
28
  export declare function writeAtomic(file: string, content: string): Promise<void>;
29
+ /** A JSON object from a file: `null` when there is no file, `'unreadable'` when it holds anything else. */
30
+ export declare function readJsonObject(file: string): Promise<Record<string, unknown> | null | 'unreadable'>;
29
31
  export declare function readText(file: string): Promise<string | null>;
package/dist/fs.js CHANGED
@@ -74,6 +74,18 @@ export async function writeAtomic(file, content) {
74
74
  await writeFile(temporary, content, 'utf8');
75
75
  await rename(temporary, file);
76
76
  }
77
+ /** A JSON object from a file: `null` when there is no file, `'unreadable'` when it holds anything else. */
78
+ export async function readJsonObject(file) {
79
+ if (!existsSync(file))
80
+ return null;
81
+ try {
82
+ const value = JSON.parse(await readFile(file, 'utf8'));
83
+ return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : 'unreadable';
84
+ }
85
+ catch {
86
+ return 'unreadable';
87
+ }
88
+ }
77
89
  export async function readText(file) {
78
90
  try {
79
91
  return await readFile(file, 'utf8');
package/dist/fs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets the disk: the configuration, the state\n * directory outside the work tree, and paths as the filesystem spells them.\n */\n\nimport { existsSync, realpathSync } from 'node:fs';\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\n\nimport { ConfigError, CONFIG_FILE, DEFAULT_CONFIG, parseConfig, type HarnessConfig } from './config.js';\n\n/** Reads `.spec-harness.json` at the root, or the defaults when there is none. */\nexport async function loadConfig(root: string): Promise<{ config: HarnessConfig; file: string | null }> {\n const file = join(root, CONFIG_FILE);\n if (!existsSync(file)) return { config: DEFAULT_CONFIG, file: null };\n let raw: unknown;\n try {\n raw = JSON.parse(await readFile(file, 'utf8'));\n } catch (error) {\n throw new ConfigError(`${CONFIG_FILE} is not valid JSON: ${(error as Error).message}`);\n }\n return { config: parseConfig(raw), file };\n}\n\n/**\n * The real spelling of a path: links resolved and, on a filesystem that\n * ignores case, the case the directory entries have. A path that does not\n * exist yet is resolved through its nearest existing ancestor.\n *\n * This is what stops `SRC/db/schema.ts` from walking past a protection on\n * `src/db/schema.ts` on Windows or macOS, where both name one file.\n */\nexport function realSpelling(path: string): string {\n const missing: string[] = [];\n let current = resolve(path);\n for (;;) {\n try {\n const real = realpathSync.native(current);\n return missing.length === 0 ? real : join(real, ...missing.reverse());\n } catch {\n const parent = dirname(current);\n if (parent === current) return resolve(path);\n missing.push(current.slice(parent.length).replace(/^[\\\\/]+/, ''));\n current = parent;\n }\n }\n}\n\n/**\n * A path as the repository names it - relative to the root, POSIX-separated,\n * real spelling - or `null` when it lies outside the root.\n */\nexport function repositoryPath(given: string, root: string, cwd: string): string | null {\n const absolute = isAbsolute(given) ? given : resolve(cwd, given);\n const real = realSpelling(absolute);\n const realRoot = realSpelling(root);\n const rel = relative(realRoot, real);\n if (rel === '') return '';\n // `..env` at the root is inside it; only a `..` segment climbs out.\n if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;\n return rel.split(sep).join('/');\n}\n\n/** The harness's own directory under the git common directory, created on demand (ADR-0003). */\nexport async function stateDirectory(commonDir: string, ...parts: string[]): Promise<string> {\n const directory = join(commonDir, 'spec-harness', ...parts);\n await mkdir(directory, { recursive: true });\n return directory;\n}\n\n/** Writes a file whole or not at all: a temporary file renamed over the target. */\nexport async function writeAtomic(file: string, content: string): Promise<void> {\n await mkdir(dirname(file), { recursive: true });\n const temporary = `${file}.${process.pid}.tmp`;\n await writeFile(temporary, content, 'utf8');\n await rename(temporary, file);\n}\n\nexport async function readText(file: string): Promise<string | null> {\n try {\n return await readFile(file, 'utf8');\n } catch {\n return null;\n }\n}\n"]}
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets the disk: the configuration, the state\n * directory outside the work tree, and paths as the filesystem spells them.\n */\n\nimport { existsSync, realpathSync } from 'node:fs';\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\n\nimport { ConfigError, CONFIG_FILE, DEFAULT_CONFIG, parseConfig, type HarnessConfig } from './config.js';\n\n/** Reads `.spec-harness.json` at the root, or the defaults when there is none. */\nexport async function loadConfig(root: string): Promise<{ config: HarnessConfig; file: string | null }> {\n const file = join(root, CONFIG_FILE);\n if (!existsSync(file)) return { config: DEFAULT_CONFIG, file: null };\n let raw: unknown;\n try {\n raw = JSON.parse(await readFile(file, 'utf8'));\n } catch (error) {\n throw new ConfigError(`${CONFIG_FILE} is not valid JSON: ${(error as Error).message}`);\n }\n return { config: parseConfig(raw), file };\n}\n\n/**\n * The real spelling of a path: links resolved and, on a filesystem that\n * ignores case, the case the directory entries have. A path that does not\n * exist yet is resolved through its nearest existing ancestor.\n *\n * This is what stops `SRC/db/schema.ts` from walking past a protection on\n * `src/db/schema.ts` on Windows or macOS, where both name one file.\n */\nexport function realSpelling(path: string): string {\n const missing: string[] = [];\n let current = resolve(path);\n for (;;) {\n try {\n const real = realpathSync.native(current);\n return missing.length === 0 ? real : join(real, ...missing.reverse());\n } catch {\n const parent = dirname(current);\n if (parent === current) return resolve(path);\n missing.push(current.slice(parent.length).replace(/^[\\\\/]+/, ''));\n current = parent;\n }\n }\n}\n\n/**\n * A path as the repository names it - relative to the root, POSIX-separated,\n * real spelling - or `null` when it lies outside the root.\n */\nexport function repositoryPath(given: string, root: string, cwd: string): string | null {\n const absolute = isAbsolute(given) ? given : resolve(cwd, given);\n const real = realSpelling(absolute);\n const realRoot = realSpelling(root);\n const rel = relative(realRoot, real);\n if (rel === '') return '';\n // `..env` at the root is inside it; only a `..` segment climbs out.\n if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;\n return rel.split(sep).join('/');\n}\n\n/** The harness's own directory under the git common directory, created on demand (ADR-0003). */\nexport async function stateDirectory(commonDir: string, ...parts: string[]): Promise<string> {\n const directory = join(commonDir, 'spec-harness', ...parts);\n await mkdir(directory, { recursive: true });\n return directory;\n}\n\n/** Writes a file whole or not at all: a temporary file renamed over the target. */\nexport async function writeAtomic(file: string, content: string): Promise<void> {\n await mkdir(dirname(file), { recursive: true });\n const temporary = `${file}.${process.pid}.tmp`;\n await writeFile(temporary, content, 'utf8');\n await rename(temporary, file);\n}\n\n/** A JSON object from a file: `null` when there is no file, `'unreadable'` when it holds anything else. */\nexport async function readJsonObject(file: string): Promise<Record<string, unknown> | null | 'unreadable'> {\n if (!existsSync(file)) return null;\n try {\n const value = JSON.parse(await readFile(file, 'utf8')) as unknown;\n return typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : 'unreadable';\n } catch {\n return 'unreadable';\n }\n}\n\nexport async function readText(file: string): Promise<string | null> {\n try {\n return await readFile(file, 'utf8');\n } catch {\n return null;\n }\n}\n"]}
package/dist/git.d.ts CHANGED
@@ -25,6 +25,8 @@ export declare function commonDirectory(cwd: string): Promise<string | null>;
25
25
  export declare function currentBranch(cwd: string): Promise<string | null>;
26
26
  /** The remote's default branch as a local ref, such as `origin/main`, or `null`. */
27
27
  export declare function remoteDefault(cwd: string): Promise<string | null>;
28
+ /** Every local branch, by its short name. */
29
+ export declare function localBranches(cwd: string): Promise<string[]>;
28
30
  export declare function revision(ref: string, cwd: string): Promise<string | null>;
29
31
  export declare function mergeBase(a: string, b: string, cwd: string): Promise<string | null>;
30
32
  /** Parses `git diff --name-status -z` output. */
package/dist/git.js CHANGED
@@ -44,6 +44,11 @@ export async function currentBranch(cwd) {
44
44
  export async function remoteDefault(cwd) {
45
45
  return value(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], cwd);
46
46
  }
47
+ /** Every local branch, by its short name. */
48
+ export async function localBranches(cwd) {
49
+ const listed = await value(['for-each-ref', '--format=%(refname:short)', 'refs/heads/'], cwd);
50
+ return listed === null || listed === '' ? [] : listed.split('\n').map((name) => name.trim());
51
+ }
47
52
  export async function revision(ref, cwd) {
48
53
  return value(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], cwd);
49
54
  }
package/dist/git.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets git.\n *\n * Reads, except for two things ADR-0003 allows and bounds: a temporary\n * worktree, added and always removed by the sandbox, and nothing else. Every\n * call is an argument vector, never a shell string, so a path or a ref cannot\n * become a command.\n */\n\nimport { execFile } from 'node:child_process';\n\nimport type { FileChange } from './types.js';\n\nexport interface Run {\n readonly code: number;\n readonly stdout: string;\n readonly stderr: string;\n}\n\n/** Runs git and answers whatever it answered; a missing git is a code of -1. */\nexport function git(args: readonly string[], cwd: string, input?: string): Promise<Run> {\n return new Promise((resolve) => {\n const child = execFile(\n 'git',\n [...args],\n { cwd, maxBuffer: 256 * 1024 * 1024, encoding: 'utf8', windowsHide: true },\n (error, stdout, stderr) => {\n const code = error === null ? 0 : typeof (error as { code?: unknown }).code === 'number' ? ((error as { code: number }).code) : -1;\n resolve({ code, stdout, stderr });\n },\n );\n if (input !== undefined) child.stdin?.end(input);\n });\n}\n\nasync function value(args: readonly string[], cwd: string): Promise<string | null> {\n const run = await git(args, cwd);\n return run.code === 0 ? run.stdout.trim() : null;\n}\n\nexport class GitError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'GitError';\n }\n}\n\n/** The top of the work tree holding `cwd`, POSIX-separated, or `null` outside one. */\nexport async function workTreeRoot(cwd: string): Promise<string | null> {\n return value(['rev-parse', '--show-toplevel'], cwd);\n}\n\n/** The git directory every worktree of this repository shares. */\nexport async function commonDirectory(cwd: string): Promise<string | null> {\n return value(['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);\n}\n\n/** The checked-out branch's short name, or `null` on a detached head. */\nexport async function currentBranch(cwd: string): Promise<string | null> {\n return value(['symbolic-ref', '--quiet', '--short', 'HEAD'], cwd);\n}\n\n/** The remote's default branch as a local ref, such as `origin/main`, or `null`. */\nexport async function remoteDefault(cwd: string): Promise<string | null> {\n return value(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], cwd);\n}\n\nexport async function revision(ref: string, cwd: string): Promise<string | null> {\n return value(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], cwd);\n}\n\nexport async function mergeBase(a: string, b: string, cwd: string): Promise<string | null> {\n return value(['merge-base', a, b], cwd);\n}\n\n/** Parses `git diff --name-status -z` output. */\nexport function parseNameStatus(output: string): FileChange[] {\n const fields = output.split('\\0');\n const out: FileChange[] = [];\n for (let i = 0; i < fields.length; ) {\n const code = fields[i] ?? '';\n if (code === '') {\n i += 1;\n continue;\n }\n const status = code.charAt(0) as FileChange['status'];\n if (status === 'R' || status === 'C') {\n out.push({ status, from: fields[i + 1] ?? '', path: fields[i + 2] ?? '' });\n i += 3;\n } else {\n out.push({ status, path: fields[i + 1] ?? '' });\n i += 2;\n }\n }\n return out;\n}\n\n/** The files changed from one revision to another, renames found. */\nexport async function changes(from: string, to: string, cwd: string): Promise<FileChange[]> {\n const run = await git(['diff', '--name-status', '-z', '--find-renames', '--no-color', from, to, '--'], cwd);\n if (run.code !== 0) throw new GitError(`git diff ${from} ${to} failed: ${run.stderr.trim()}`);\n return parseNameStatus(run.stdout);\n}\n\n/** The files a commit would change: what is staged. */\nexport async function stagedChanges(cwd: string): Promise<FileChange[]> {\n const run = await git(['diff', '--cached', '--name-status', '-z', '--find-renames', '--no-color', '--'], cwd);\n if (run.code !== 0) throw new GitError(`git diff --cached failed: ${run.stderr.trim()}`);\n return parseNameStatus(run.stdout);\n}\n\n/** A file's content at a revision, or `null` when it does not exist there. */\nexport async function show(ref: string, path: string, cwd: string): Promise<string | null> {\n const run = await git(['show', `${ref}:${path}`], cwd);\n return run.code === 0 ? run.stdout : null;\n}\n\n/**\n * The commit that last changed one line of a file, as of a revision, or of\n * the working tree when `ref` is `null` - where a line nobody committed yet\n * belongs to the all-zero commit.\n */\nexport async function blameLine(ref: string | null, path: string, line: number, cwd: string): Promise<string | null> {\n const run = await git(['blame', '--porcelain', '-L', `${line},${line}`, ...(ref === null ? [] : [ref]), '--', path], cwd);\n if (run.code !== 0) return null;\n const sha = /^([0-9a-f]{40,64}) /.exec(run.stdout)?.[1];\n return sha ?? null;\n}\n\nexport interface Signature {\n readonly good: boolean;\n /** The allowed signer's principal, for a good signature. */\n readonly principal: string | null;\n readonly detail: string;\n}\n\n/**\n * Whether a commit carries a good SSH signature by a principal in the given\n * allowed-signers file. The file is passed in, never read from the commit's\n * own tree: a round cannot vouch for itself (ADR-0006).\n */\nexport async function verifyCommit(sha: string, allowedSignersFile: string, cwd: string): Promise<Signature> {\n const run = await git(\n ['-c', `gpg.ssh.allowedSignersFile=${allowedSignersFile}`, '-c', 'gpg.format=ssh', 'verify-commit', '--raw', sha],\n cwd,\n );\n const text = `${run.stderr}\\n${run.stdout}`;\n const principal = /Good \"git\" signature for (\\S+)/.exec(text)?.[1] ?? null;\n return { good: run.code === 0 && principal !== null, principal, detail: text.trim().split('\\n')[0] ?? '' };\n}\n\nexport async function addWorktree(path: string, rev: string, cwd: string): Promise<void> {\n const run = await git(['worktree', 'add', '--detach', '--force', path, rev], cwd);\n if (run.code !== 0) throw new GitError(`git worktree add failed: ${run.stderr.trim()}`);\n}\n\nexport async function removeWorktree(path: string, cwd: string): Promise<boolean> {\n const run = await git(['worktree', 'remove', '--force', path], cwd);\n if (run.code === 0) return true;\n await git(['worktree', 'prune'], cwd);\n return false;\n}\n"]}
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets git.\n *\n * Reads, except for two things ADR-0003 allows and bounds: a temporary\n * worktree, added and always removed by the sandbox, and nothing else. Every\n * call is an argument vector, never a shell string, so a path or a ref cannot\n * become a command.\n */\n\nimport { execFile } from 'node:child_process';\n\nimport type { FileChange } from './types.js';\n\nexport interface Run {\n readonly code: number;\n readonly stdout: string;\n readonly stderr: string;\n}\n\n/** Runs git and answers whatever it answered; a missing git is a code of -1. */\nexport function git(args: readonly string[], cwd: string, input?: string): Promise<Run> {\n return new Promise((resolve) => {\n const child = execFile(\n 'git',\n [...args],\n { cwd, maxBuffer: 256 * 1024 * 1024, encoding: 'utf8', windowsHide: true },\n (error, stdout, stderr) => {\n const code = error === null ? 0 : typeof (error as { code?: unknown }).code === 'number' ? ((error as { code: number }).code) : -1;\n resolve({ code, stdout, stderr });\n },\n );\n if (input !== undefined) child.stdin?.end(input);\n });\n}\n\nasync function value(args: readonly string[], cwd: string): Promise<string | null> {\n const run = await git(args, cwd);\n return run.code === 0 ? run.stdout.trim() : null;\n}\n\nexport class GitError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'GitError';\n }\n}\n\n/** The top of the work tree holding `cwd`, POSIX-separated, or `null` outside one. */\nexport async function workTreeRoot(cwd: string): Promise<string | null> {\n return value(['rev-parse', '--show-toplevel'], cwd);\n}\n\n/** The git directory every worktree of this repository shares. */\nexport async function commonDirectory(cwd: string): Promise<string | null> {\n return value(['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);\n}\n\n/** The checked-out branch's short name, or `null` on a detached head. */\nexport async function currentBranch(cwd: string): Promise<string | null> {\n return value(['symbolic-ref', '--quiet', '--short', 'HEAD'], cwd);\n}\n\n/** The remote's default branch as a local ref, such as `origin/main`, or `null`. */\nexport async function remoteDefault(cwd: string): Promise<string | null> {\n return value(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], cwd);\n}\n\n/** Every local branch, by its short name. */\nexport async function localBranches(cwd: string): Promise<string[]> {\n const listed = await value(['for-each-ref', '--format=%(refname:short)', 'refs/heads/'], cwd);\n return listed === null || listed === '' ? [] : listed.split('\\n').map((name) => name.trim());\n}\n\nexport async function revision(ref: string, cwd: string): Promise<string | null> {\n return value(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], cwd);\n}\n\nexport async function mergeBase(a: string, b: string, cwd: string): Promise<string | null> {\n return value(['merge-base', a, b], cwd);\n}\n\n/** Parses `git diff --name-status -z` output. */\nexport function parseNameStatus(output: string): FileChange[] {\n const fields = output.split('\\0');\n const out: FileChange[] = [];\n for (let i = 0; i < fields.length; ) {\n const code = fields[i] ?? '';\n if (code === '') {\n i += 1;\n continue;\n }\n const status = code.charAt(0) as FileChange['status'];\n if (status === 'R' || status === 'C') {\n out.push({ status, from: fields[i + 1] ?? '', path: fields[i + 2] ?? '' });\n i += 3;\n } else {\n out.push({ status, path: fields[i + 1] ?? '' });\n i += 2;\n }\n }\n return out;\n}\n\n/** The files changed from one revision to another, renames found. */\nexport async function changes(from: string, to: string, cwd: string): Promise<FileChange[]> {\n const run = await git(['diff', '--name-status', '-z', '--find-renames', '--no-color', from, to, '--'], cwd);\n if (run.code !== 0) throw new GitError(`git diff ${from} ${to} failed: ${run.stderr.trim()}`);\n return parseNameStatus(run.stdout);\n}\n\n/** The files a commit would change: what is staged. */\nexport async function stagedChanges(cwd: string): Promise<FileChange[]> {\n const run = await git(['diff', '--cached', '--name-status', '-z', '--find-renames', '--no-color', '--'], cwd);\n if (run.code !== 0) throw new GitError(`git diff --cached failed: ${run.stderr.trim()}`);\n return parseNameStatus(run.stdout);\n}\n\n/** A file's content at a revision, or `null` when it does not exist there. */\nexport async function show(ref: string, path: string, cwd: string): Promise<string | null> {\n const run = await git(['show', `${ref}:${path}`], cwd);\n return run.code === 0 ? run.stdout : null;\n}\n\n/**\n * The commit that last changed one line of a file, as of a revision, or of\n * the working tree when `ref` is `null` - where a line nobody committed yet\n * belongs to the all-zero commit.\n */\nexport async function blameLine(ref: string | null, path: string, line: number, cwd: string): Promise<string | null> {\n const run = await git(['blame', '--porcelain', '-L', `${line},${line}`, ...(ref === null ? [] : [ref]), '--', path], cwd);\n if (run.code !== 0) return null;\n const sha = /^([0-9a-f]{40,64}) /.exec(run.stdout)?.[1];\n return sha ?? null;\n}\n\nexport interface Signature {\n readonly good: boolean;\n /** The allowed signer's principal, for a good signature. */\n readonly principal: string | null;\n readonly detail: string;\n}\n\n/**\n * Whether a commit carries a good SSH signature by a principal in the given\n * allowed-signers file. The file is passed in, never read from the commit's\n * own tree: a round cannot vouch for itself (ADR-0006).\n */\nexport async function verifyCommit(sha: string, allowedSignersFile: string, cwd: string): Promise<Signature> {\n const run = await git(\n ['-c', `gpg.ssh.allowedSignersFile=${allowedSignersFile}`, '-c', 'gpg.format=ssh', 'verify-commit', '--raw', sha],\n cwd,\n );\n const text = `${run.stderr}\\n${run.stdout}`;\n const principal = /Good \"git\" signature for (\\S+)/.exec(text)?.[1] ?? null;\n return { good: run.code === 0 && principal !== null, principal, detail: text.trim().split('\\n')[0] ?? '' };\n}\n\nexport async function addWorktree(path: string, rev: string, cwd: string): Promise<void> {\n const run = await git(['worktree', 'add', '--detach', '--force', path, rev], cwd);\n if (run.code !== 0) throw new GitError(`git worktree add failed: ${run.stderr.trim()}`);\n}\n\nexport async function removeWorktree(path: string, cwd: string): Promise<boolean> {\n const run = await git(['worktree', 'remove', '--force', path], cwd);\n if (run.code === 0) return true;\n await git(['worktree', 'prune'], cwd);\n return false;\n}\n"]}
package/dist/guard.d.ts CHANGED
@@ -39,5 +39,7 @@ export interface GuardInput {
39
39
  readonly rulings: readonly VerifiedRuling[];
40
40
  readonly outOfScope: OutOfScope;
41
41
  }
42
+ /** The first verified ruling whose paths cover `path`, read as the guard reads a scope. */
43
+ export declare function rulingFor(rulings: readonly VerifiedRuling[], path: string): VerifiedRuling | undefined;
42
44
  /** Decides one path. Pure: the caller resolved the path, the brief and the rulings. */
43
45
  export declare function decide(input: GuardInput): Decision;
package/dist/guard.js CHANGED
@@ -25,6 +25,10 @@ function matching(patterns, path) {
25
25
  }
26
26
  return { matched, unreadable };
27
27
  }
28
+ /** The first verified ruling whose paths cover `path`, read as the guard reads a scope. */
29
+ export function rulingFor(rulings, path) {
30
+ return rulings.find((candidate) => matching(candidate.paths, path).matched.length > 0);
31
+ }
28
32
  const ESCALATE = 'if the round cannot be done without it, stop and ask for a ruling: spec-harness escalate --path <file> --reason <why>, or the request_escalation tool';
29
33
  /** Decides one path. Pure: the caller resolved the path, the brief and the rulings. */
30
34
  export function decide(input) {
@@ -73,7 +77,7 @@ export function decide(input) {
73
77
  };
74
78
  }
75
79
  if (protectedHits.matched.length > 0) {
76
- const ruling = input.rulings.find((candidate) => matching(candidate.paths, path).matched.length > 0);
80
+ const ruling = rulingFor(input.rulings, path);
77
81
  if (ruling !== undefined) {
78
82
  return {
79
83
  path,
package/dist/guard.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * May this round write this file?\n *\n * The answer is read from the brief the round runs under: `protectedFiles`\n * are refused unless a person has ruled otherwise, `affectedFiles` are the\n * round's to write, and anything else is outside the scope the person\n * approved. A guard is a guardrail, not a boundary - an agent that writes\n * through a shell passes it - so the audit and spec-brief's archive are the\n * gates, and this is what keeps an honest agent from needing them (ADR-0005).\n */\n\nimport { parseGlob, type Glob } from './vendor/spec-core/pattern/index.js';\nimport type { OutOfScope } from './config.js';\nimport type { BriefRow } from './types.js';\n\nexport type Verdict = 'allow' | 'warn' | 'ask' | 'deny';\n\nexport type Reason =\n | 'outside-repository'\n | 'no-brief'\n | 'brief-file'\n | 'protected'\n | 'ruled'\n | 'unreadable-protection'\n | 'in-scope'\n | 'out-of-scope';\n\nexport interface Decision {\n /** Repository-relative, or the path as given when it lies outside. */\n readonly path: string;\n readonly verdict: Verdict;\n readonly reason: Reason;\n /** The patterns or ruling that decided it. */\n readonly because: readonly string[];\n readonly message: string;\n readonly hint: string;\n}\n\n/** A ruling whose signature has been verified: a person allowed these paths. */\nexport interface VerifiedRuling {\n readonly id: string;\n readonly paths: readonly string[];\n readonly signer: string;\n}\n\nexport interface GuardInput {\n /** Repository-relative POSIX path, or `null` for a path outside the repository. */\n readonly path: string | null;\n /** The path as the caller gave it, for messages. */\n readonly given: string;\n readonly brief: BriefRow | null;\n /** Why there is no brief, when there is none. */\n readonly noBrief?: string | undefined;\n readonly rulings: readonly VerifiedRuling[];\n readonly outOfScope: OutOfScope;\n}\n\nfunction compile(pattern: string): Glob | string {\n const parsed = parseGlob(pattern, { dialect: 'path', caseSensitive: true, literal: 'either' });\n return parsed.ok ? parsed.glob : parsed.error;\n}\n\nfunction matching(patterns: readonly string[], path: string): { matched: string[]; unreadable: string[] } {\n const matched: string[] = [];\n const unreadable: string[] = [];\n for (const pattern of patterns) {\n const glob = compile(pattern);\n if (typeof glob === 'string') unreadable.push(`${pattern} (${glob})`);\n else if (glob.match(path)) matched.push(pattern);\n }\n return { matched, unreadable };\n}\n\nconst ESCALATE =\n 'if the round cannot be done without it, stop and ask for a ruling: spec-harness escalate --path <file> --reason <why>, or the request_escalation tool';\n\n/** Decides one path. Pure: the caller resolved the path, the brief and the rulings. */\nexport function decide(input: GuardInput): Decision {\n const { path, given, brief } = input;\n if (path === null) {\n return {\n path: given,\n verdict: 'allow',\n reason: 'outside-repository',\n because: [],\n message: `${given} is outside the repository; no brief governs it`,\n hint: 'nothing to do',\n };\n }\n if (brief === null) {\n return {\n path,\n verdict: 'allow',\n reason: 'no-brief',\n because: [],\n message: `no brief governs ${path}: ${input.noBrief ?? 'no active brief'}`,\n hint: 'name the brief this work belongs to, so its scope can be checked',\n };\n }\n if (path === brief.file) {\n return {\n path,\n verdict: 'allow',\n reason: 'brief-file',\n because: [],\n message: `${path} is brief ${brief.id} itself`,\n hint: 'tick or disposition its boxes as the work lands',\n };\n }\n\n const protectedHits = matching(brief.protectedFiles, path);\n if (protectedHits.unreadable.length > 0) {\n // A protection that cannot be read cannot be honoured, and cannot be\n // assumed not to apply: refuse until the brief is fixed.\n return {\n path,\n verdict: 'deny',\n reason: 'unreadable-protection',\n because: protectedHits.unreadable,\n message: `brief ${brief.id} protects files with a pattern that cannot be read: ${protectedHits.unreadable.join('; ')}`,\n hint: `fix protectedFiles in ${brief.file} (spec-brief lint names the problem)`,\n };\n }\n if (protectedHits.matched.length > 0) {\n const ruling = input.rulings.find((candidate) => matching(candidate.paths, path).matched.length > 0);\n if (ruling !== undefined) {\n return {\n path,\n verdict: 'allow',\n reason: 'ruled',\n because: [ruling.id],\n message: `${path} is protected by brief ${brief.id}, and ruling ${ruling.id}, signed by ${ruling.signer}, allows it`,\n hint: 'keep the change to what the ruling allows',\n };\n }\n return {\n path,\n verdict: 'deny',\n reason: 'protected',\n because: protectedHits.matched,\n message: `brief ${brief.id} does not empower this round to change ${path} (protectedFiles: ${protectedHits.matched.join(', ')})`,\n hint: ESCALATE,\n };\n }\n\n const affectedHits = matching(brief.affectedFiles, path);\n if (affectedHits.matched.length > 0) {\n return {\n path,\n verdict: 'allow',\n reason: 'in-scope',\n because: affectedHits.matched,\n message: `${path} is in brief ${brief.id}'s scope (${affectedHits.matched.join(', ')})`,\n hint: 'nothing to do',\n };\n }\n\n const verdict: Verdict = input.outOfScope;\n const scope = brief.affectedFiles.length === 0 ? 'declares no affectedFiles' : `covers ${brief.affectedFiles.join(', ')}`;\n return {\n path,\n verdict,\n reason: 'out-of-scope',\n because: affectedHits.unreadable,\n message: `${path} is outside brief ${brief.id}'s scope, which ${scope}`,\n hint:\n verdict === 'deny'\n ? `add it to affectedFiles in ${brief.file} if the round needs it, which is a change the person who approved the brief must see; or ${ESCALATE}`\n : `if the round needs it, say so in ${brief.file} and add it to affectedFiles; the archive reports every file outside the scope`,\n };\n}\n"]}
1
+ {"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * May this round write this file?\n *\n * The answer is read from the brief the round runs under: `protectedFiles`\n * are refused unless a person has ruled otherwise, `affectedFiles` are the\n * round's to write, and anything else is outside the scope the person\n * approved. A guard is a guardrail, not a boundary - an agent that writes\n * through a shell passes it - so the audit and spec-brief's archive are the\n * gates, and this is what keeps an honest agent from needing them (ADR-0005).\n */\n\nimport { parseGlob, type Glob } from './vendor/spec-core/pattern/index.js';\nimport type { OutOfScope } from './config.js';\nimport type { BriefRow } from './types.js';\n\nexport type Verdict = 'allow' | 'warn' | 'ask' | 'deny';\n\nexport type Reason =\n | 'outside-repository'\n | 'no-brief'\n | 'brief-file'\n | 'protected'\n | 'ruled'\n | 'unreadable-protection'\n | 'in-scope'\n | 'out-of-scope';\n\nexport interface Decision {\n /** Repository-relative, or the path as given when it lies outside. */\n readonly path: string;\n readonly verdict: Verdict;\n readonly reason: Reason;\n /** The patterns or ruling that decided it. */\n readonly because: readonly string[];\n readonly message: string;\n readonly hint: string;\n}\n\n/** A ruling whose signature has been verified: a person allowed these paths. */\nexport interface VerifiedRuling {\n readonly id: string;\n readonly paths: readonly string[];\n readonly signer: string;\n}\n\nexport interface GuardInput {\n /** Repository-relative POSIX path, or `null` for a path outside the repository. */\n readonly path: string | null;\n /** The path as the caller gave it, for messages. */\n readonly given: string;\n readonly brief: BriefRow | null;\n /** Why there is no brief, when there is none. */\n readonly noBrief?: string | undefined;\n readonly rulings: readonly VerifiedRuling[];\n readonly outOfScope: OutOfScope;\n}\n\nfunction compile(pattern: string): Glob | string {\n const parsed = parseGlob(pattern, { dialect: 'path', caseSensitive: true, literal: 'either' });\n return parsed.ok ? parsed.glob : parsed.error;\n}\n\nfunction matching(patterns: readonly string[], path: string): { matched: string[]; unreadable: string[] } {\n const matched: string[] = [];\n const unreadable: string[] = [];\n for (const pattern of patterns) {\n const glob = compile(pattern);\n if (typeof glob === 'string') unreadable.push(`${pattern} (${glob})`);\n else if (glob.match(path)) matched.push(pattern);\n }\n return { matched, unreadable };\n}\n\n/** The first verified ruling whose paths cover `path`, read as the guard reads a scope. */\nexport function rulingFor(rulings: readonly VerifiedRuling[], path: string): VerifiedRuling | undefined {\n return rulings.find((candidate) => matching(candidate.paths, path).matched.length > 0);\n}\n\nconst ESCALATE =\n 'if the round cannot be done without it, stop and ask for a ruling: spec-harness escalate --path <file> --reason <why>, or the request_escalation tool';\n\n/** Decides one path. Pure: the caller resolved the path, the brief and the rulings. */\nexport function decide(input: GuardInput): Decision {\n const { path, given, brief } = input;\n if (path === null) {\n return {\n path: given,\n verdict: 'allow',\n reason: 'outside-repository',\n because: [],\n message: `${given} is outside the repository; no brief governs it`,\n hint: 'nothing to do',\n };\n }\n if (brief === null) {\n return {\n path,\n verdict: 'allow',\n reason: 'no-brief',\n because: [],\n message: `no brief governs ${path}: ${input.noBrief ?? 'no active brief'}`,\n hint: 'name the brief this work belongs to, so its scope can be checked',\n };\n }\n if (path === brief.file) {\n return {\n path,\n verdict: 'allow',\n reason: 'brief-file',\n because: [],\n message: `${path} is brief ${brief.id} itself`,\n hint: 'tick or disposition its boxes as the work lands',\n };\n }\n\n const protectedHits = matching(brief.protectedFiles, path);\n if (protectedHits.unreadable.length > 0) {\n // A protection that cannot be read cannot be honoured, and cannot be\n // assumed not to apply: refuse until the brief is fixed.\n return {\n path,\n verdict: 'deny',\n reason: 'unreadable-protection',\n because: protectedHits.unreadable,\n message: `brief ${brief.id} protects files with a pattern that cannot be read: ${protectedHits.unreadable.join('; ')}`,\n hint: `fix protectedFiles in ${brief.file} (spec-brief lint names the problem)`,\n };\n }\n if (protectedHits.matched.length > 0) {\n const ruling = rulingFor(input.rulings, path);\n if (ruling !== undefined) {\n return {\n path,\n verdict: 'allow',\n reason: 'ruled',\n because: [ruling.id],\n message: `${path} is protected by brief ${brief.id}, and ruling ${ruling.id}, signed by ${ruling.signer}, allows it`,\n hint: 'keep the change to what the ruling allows',\n };\n }\n return {\n path,\n verdict: 'deny',\n reason: 'protected',\n because: protectedHits.matched,\n message: `brief ${brief.id} does not empower this round to change ${path} (protectedFiles: ${protectedHits.matched.join(', ')})`,\n hint: ESCALATE,\n };\n }\n\n const affectedHits = matching(brief.affectedFiles, path);\n if (affectedHits.matched.length > 0) {\n return {\n path,\n verdict: 'allow',\n reason: 'in-scope',\n because: affectedHits.matched,\n message: `${path} is in brief ${brief.id}'s scope (${affectedHits.matched.join(', ')})`,\n hint: 'nothing to do',\n };\n }\n\n const verdict: Verdict = input.outOfScope;\n const scope = brief.affectedFiles.length === 0 ? 'declares no affectedFiles' : `covers ${brief.affectedFiles.join(', ')}`;\n return {\n path,\n verdict,\n reason: 'out-of-scope',\n because: affectedHits.unreadable,\n message: `${path} is outside brief ${brief.id}'s scope, which ${scope}`,\n hint:\n verdict === 'deny'\n ? `add it to affectedFiles in ${brief.file} if the round needs it, which is a change the person who approved the brief must see; or ${ESCALATE}`\n : `if the round needs it, say so in ${brief.file} and add it to affectedFiles; the archive reports every file outside the scope`,\n };\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -27,5 +27,5 @@ export { createReader } from './reader.js';
27
27
  export { addRulingRow, nextId, pathsOf, readRulings, renderMemo, renderRow } from './rulings.js';
28
28
  export type { EscalationOption, EscalationRequest, RulingRow, TableView } from './rulings.js';
29
29
  export { buildContext, checkPaths, checkRulings, raiseEscalation, recordRuling, resolveBase, runAudit } from './round.js';
30
- export { mergeClaudeSettings, mergeMcp, mergeSpecGraph, HOOK_COMMAND } from './setup.js';
30
+ export { GUARD_HOOK, mcpServer, mergeClaudeSettings, mergeMcp, mergeSpecGraph, PROJECT_DIR, PROJECT_DIR_OR_HERE } from './configure.js';
31
31
  export type { BriefRow, FileChange, Finding, Severity } from './types.js';
package/dist/index.js CHANGED
@@ -16,5 +16,5 @@ export { classify, readProbes, renderEvidence, verdictOf } from './probe.js';
16
16
  export { createReader } from './reader.js';
17
17
  export { addRulingRow, nextId, pathsOf, readRulings, renderMemo, renderRow } from './rulings.js';
18
18
  export { buildContext, checkPaths, checkRulings, raiseEscalation, recordRuling, resolveBase, runAudit } from './round.js';
19
- export { mergeClaudeSettings, mergeMcp, mergeSpecGraph, HOOK_COMMAND } from './setup.js';
19
+ export { GUARD_HOOK, mcpServer, mergeClaudeSettings, mergeMcp, mergeSpecGraph, PROJECT_DIR, PROJECT_DIR_OR_HERE } from './configure.js';
20
20
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAG5F,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAExG,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE9F,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEpC,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEzF,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE1F,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE7E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEjG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC1H,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC","sourcesContent":["/**\n * Programmatic API. The command line is a thin shell over these; a harness of\n * another kind - a CI job, an orchestrator - can ask the same questions.\n */\n\nexport { main, run, EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version } from './cli.js';\nexport type { CliIO } from './cli.js';\n\nexport { audit } from './audit.js';\nexport type { AssertionOutcome, AuditInput, AuditReport } from './audit.js';\nexport { briefIdFromBranch, idFromBranch, sameId, templateError } from './branch.js';\nexport { findActive, parseBriefList, SiblingOutputError, SUPPORTED_SCHEMA_VERSIONS } from './briefs.js';\nexport type { ActiveBrief, ActiveSource } from './briefs.js';\nexport { ConfigError, CONFIG_FILE, DEFAULT_CONFIG, parseConfig, SIBLINGS } from './config.js';\nexport type { HarnessConfig, OutOfScope, SiblingName } from './config.js';\nexport { renderContext } from './context.js';\nexport type { CitedDocument, ContextInput, ContextPacket, RuleInForce, RulingInForce } from './context.js';\nexport { decide } from './guard.js';\nexport type { Decision, GuardInput, Reason, Verdict, VerifiedRuling } from './guard.js';\nexport { claudeResponse, gitResponse, parseClaudeHook, WRITING_TOOLS } from './hooks.js';\nexport type { HookRequest, HookResponse } from './hooks.js';\nexport { readJUnit } from './junit.js';\nexport type { JUnitCase, JUnitRead } from './junit.js';\nexport { diffManifest, ecosystemOf, manifestMatcher, readManifest } from './manifests.js';\nexport type { Dependency, DependencyChange, Ecosystem } from './manifests.js';\nexport { classify, readProbes, renderEvidence, verdictOf } from './probe.js';\nexport type { Classified, CodeBlock, ProbeFile, ProbeResult, ProbeRun, ProbeSet, ProbeSpec } from './probe.js';\nexport { createReader } from './reader.js';\nexport { addRulingRow, nextId, pathsOf, readRulings, renderMemo, renderRow } from './rulings.js';\nexport type { EscalationOption, EscalationRequest, RulingRow, TableView } from './rulings.js';\nexport { buildContext, checkPaths, checkRulings, raiseEscalation, recordRuling, resolveBase, runAudit } from './round.js';\nexport { mergeClaudeSettings, mergeMcp, mergeSpecGraph, HOOK_COMMAND } from './setup.js';\nexport type { BriefRow, FileChange, Finding, Severity } from './types.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAG5F,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAExG,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE9F,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEpC,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEzF,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE1F,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE7E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEjG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC1H,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,mBAAmB,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC","sourcesContent":["/**\n * Programmatic API. The command line is a thin shell over these; a harness of\n * another kind - a CI job, an orchestrator - can ask the same questions.\n */\n\nexport { main, run, EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version } from './cli.js';\nexport type { CliIO } from './cli.js';\n\nexport { audit } from './audit.js';\nexport type { AssertionOutcome, AuditInput, AuditReport } from './audit.js';\nexport { briefIdFromBranch, idFromBranch, sameId, templateError } from './branch.js';\nexport { findActive, parseBriefList, SiblingOutputError, SUPPORTED_SCHEMA_VERSIONS } from './briefs.js';\nexport type { ActiveBrief, ActiveSource } from './briefs.js';\nexport { ConfigError, CONFIG_FILE, DEFAULT_CONFIG, parseConfig, SIBLINGS } from './config.js';\nexport type { HarnessConfig, OutOfScope, SiblingName } from './config.js';\nexport { renderContext } from './context.js';\nexport type { CitedDocument, ContextInput, ContextPacket, RuleInForce, RulingInForce } from './context.js';\nexport { decide } from './guard.js';\nexport type { Decision, GuardInput, Reason, Verdict, VerifiedRuling } from './guard.js';\nexport { claudeResponse, gitResponse, parseClaudeHook, WRITING_TOOLS } from './hooks.js';\nexport type { HookRequest, HookResponse } from './hooks.js';\nexport { readJUnit } from './junit.js';\nexport type { JUnitCase, JUnitRead } from './junit.js';\nexport { diffManifest, ecosystemOf, manifestMatcher, readManifest } from './manifests.js';\nexport type { Dependency, DependencyChange, Ecosystem } from './manifests.js';\nexport { classify, readProbes, renderEvidence, verdictOf } from './probe.js';\nexport type { Classified, CodeBlock, ProbeFile, ProbeResult, ProbeRun, ProbeSet, ProbeSpec } from './probe.js';\nexport { createReader } from './reader.js';\nexport { addRulingRow, nextId, pathsOf, readRulings, renderMemo, renderRow } from './rulings.js';\nexport type { EscalationOption, EscalationRequest, RulingRow, TableView } from './rulings.js';\nexport { buildContext, checkPaths, checkRulings, raiseEscalation, recordRuling, resolveBase, runAudit } from './round.js';\nexport { GUARD_HOOK, mcpServer, mergeClaudeSettings, mergeMcp, mergeSpecGraph, PROJECT_DIR, PROJECT_DIR_OR_HERE } from './configure.js';\nexport type { BriefRow, FileChange, Finding, Severity } from './types.js';\n"]}
package/dist/plugin.d.ts CHANGED
@@ -6,7 +6,8 @@
6
6
  * (ADR-0006), the refusal is lifted - but spec-brief does not read signatures,
7
7
  * and should not: the tool that defines a format ships the check for it
8
8
  * (spec-brief ADR-0007). So the check ships here, and spec-brief asks it
9
- * through its plugin `waive` hook:
9
+ * through its plugin `waive` hook, once its configuration loads the plugin,
10
+ * as `spec-harness init` writes it:
10
11
  *
11
12
  * ```json
12
13
  * { "plugins": ["@descent-vtt/spec-harness/spec-brief-plugin"] }
package/dist/plugin.js CHANGED
@@ -6,7 +6,8 @@
6
6
  * (ADR-0006), the refusal is lifted - but spec-brief does not read signatures,
7
7
  * and should not: the tool that defines a format ships the check for it
8
8
  * (spec-brief ADR-0007). So the check ships here, and spec-brief asks it
9
- * through its plugin `waive` hook:
9
+ * through its plugin `waive` hook, once its configuration loads the plugin,
10
+ * as `spec-harness init` writes it:
10
11
  *
11
12
  * ```json
12
13
  * { "plugins": ["@descent-vtt/spec-harness/spec-brief-plugin"] }
@@ -17,16 +18,10 @@
17
18
  */
18
19
  import { loadConfig } from './fs.js';
19
20
  import { commonDirectory, currentBranch } from './git.js';
21
+ import { rulingFor } from './guard.js';
20
22
  import { createReader } from './reader.js';
21
23
  import { checkRulings, resolveBase } from './round.js';
22
24
  import { createSiblings } from './siblings.js';
23
- import { parseGlob } from './vendor/spec-core/pattern/index.js';
24
- function covers(ruling, path) {
25
- return ruling.paths.some((pattern) => {
26
- const parsed = parseGlob(pattern, { dialect: 'path', caseSensitive: true, literal: 'either' });
27
- return parsed.ok && parsed.glob.match(path);
28
- });
29
- }
30
25
  /** Paths a finding names, in either of the shapes spec-brief reports them. */
31
26
  function pathsOf(finding) {
32
27
  if (finding.path !== undefined)
@@ -65,7 +60,7 @@ export async function waive(context) {
65
60
  const { verified } = await checkRulings(workspace, brief, context.brief.text, createReader(), base);
66
61
  const waivers = [];
67
62
  for (const path of protectedPaths) {
68
- const ruling = verified.find((candidate) => covers(candidate, path));
63
+ const ruling = rulingFor(verified, path);
69
64
  if (ruling !== undefined) {
70
65
  waivers.push({ rule: 'protected-file', path, reason: `ruling ${ruling.id}, signed by ${ruling.signer}, allows it` });
71
66
  }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The spec-brief plugin: signed rulings, read where spec-brief decides.\n *\n * spec-brief's archive refuses a round that changed a file its brief protects.\n * When a person has ruled that the round may change it, and signed the ruling\n * (ADR-0006), the refusal is lifted - but spec-brief does not read signatures,\n * and should not: the tool that defines a format ships the check for it\n * (spec-brief ADR-0007). So the check ships here, and spec-brief asks it\n * through its plugin `waive` hook:\n *\n * ```json\n * { \"plugins\": [\"@descent-vtt/spec-harness/spec-brief-plugin\"] }\n * ```\n *\n * It waives a `protected-file` refusal for a path a verified `allow` ruling\n * names, and nothing else. A ruling that does not verify waives nothing.\n */\n\nimport { loadConfig } from './fs.js';\nimport { commonDirectory, currentBranch } from './git.js';\nimport type { VerifiedRuling } from './guard.js';\nimport { createReader } from './reader.js';\nimport { checkRulings, resolveBase } from './round.js';\nimport { createSiblings } from './siblings.js';\nimport type { BriefRow } from './types.js';\nimport { parseGlob } from './vendor/spec-core/pattern/index.js';\nimport type { Workspace } from './workspace.js';\n\nexport interface WaiveContext {\n readonly root: string;\n /** spec-brief passes a `null` id for a brief it could not name; its file still identifies it. */\n readonly brief: { readonly id: string | null; readonly file: string; readonly text: string };\n readonly findings: readonly { readonly rule: string; readonly path?: string | undefined; readonly paths?: readonly string[] | undefined }[];\n readonly base: string | null;\n readonly commit: string | null;\n}\n\nexport interface Waiver {\n readonly rule: string;\n readonly path: string;\n readonly reason: string;\n}\n\nfunction covers(ruling: VerifiedRuling, path: string): boolean {\n return ruling.paths.some((pattern) => {\n const parsed = parseGlob(pattern, { dialect: 'path', caseSensitive: true, literal: 'either' });\n return parsed.ok && parsed.glob.match(path);\n });\n}\n\n/** Paths a finding names, in either of the shapes spec-brief reports them. */\nfunction pathsOf(finding: WaiveContext['findings'][number]): string[] {\n if (finding.path !== undefined) return [finding.path];\n return [...(finding.paths ?? [])];\n}\n\nexport async function waive(context: WaiveContext): Promise<Waiver[]> {\n const protectedPaths = context.findings.filter((finding) => finding.rule === 'protected-file').flatMap(pathsOf);\n if (protectedPaths.length === 0) return [];\n const { config } = await loadConfig(context.root);\n const workspace: Workspace = {\n root: context.root,\n commonDir: (await commonDirectory(context.root)) ?? `${context.root}/.git`,\n config,\n siblings: createSiblings(context.root, config),\n branch: await currentBranch(context.root),\n cwd: context.root,\n };\n const base = await resolveBase(workspace, context.base ?? undefined);\n const brief: BriefRow = {\n id: context.brief.id ?? context.brief.file,\n file: context.brief.file,\n title: null,\n phase: 'live',\n status: null,\n type: null,\n wave: null,\n dependsOn: [],\n affectedFiles: [],\n protectedFiles: [],\n tasks: { total: 0, checked: 0 },\n ready: false,\n waitingOn: [],\n };\n const { verified } = await checkRulings(workspace, brief, context.brief.text, createReader(), base);\n const waivers: Waiver[] = [];\n for (const path of protectedPaths) {\n const ruling = verified.find((candidate) => covers(candidate, path));\n if (ruling !== undefined) {\n waivers.push({ rule: 'protected-file', path, reason: `ruling ${ruling.id}, signed by ${ruling.signer}, allows it` });\n }\n }\n return waivers;\n}\n\n/** The plugin, as spec-brief loads it: a function of its options returning `{ name, rules, waive }`. */\nexport default function specHarnessPlugin(): { name: string; rules: readonly never[]; waive: typeof waive } {\n return { name: 'spec-harness', rules: [], waive };\n}\n"]}
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The spec-brief plugin: signed rulings, read where spec-brief decides.\n *\n * spec-brief's archive refuses a round that changed a file its brief protects.\n * When a person has ruled that the round may change it, and signed the ruling\n * (ADR-0006), the refusal is lifted - but spec-brief does not read signatures,\n * and should not: the tool that defines a format ships the check for it\n * (spec-brief ADR-0007). So the check ships here, and spec-brief asks it\n * through its plugin `waive` hook, once its configuration loads the plugin,\n * as `spec-harness init` writes it:\n *\n * ```json\n * { \"plugins\": [\"@descent-vtt/spec-harness/spec-brief-plugin\"] }\n * ```\n *\n * It waives a `protected-file` refusal for a path a verified `allow` ruling\n * names, and nothing else. A ruling that does not verify waives nothing.\n */\n\nimport { loadConfig } from './fs.js';\nimport { commonDirectory, currentBranch } from './git.js';\nimport { rulingFor } from './guard.js';\nimport { createReader } from './reader.js';\nimport { checkRulings, resolveBase } from './round.js';\nimport { createSiblings } from './siblings.js';\nimport type { BriefRow } from './types.js';\nimport type { Workspace } from './workspace.js';\n\nexport interface WaiveContext {\n readonly root: string;\n /** spec-brief passes a `null` id for a brief it could not name; its file still identifies it. */\n readonly brief: { readonly id: string | null; readonly file: string; readonly text: string };\n readonly findings: readonly { readonly rule: string; readonly path?: string | undefined; readonly paths?: readonly string[] | undefined }[];\n readonly base: string | null;\n readonly commit: string | null;\n}\n\nexport interface Waiver {\n readonly rule: string;\n readonly path: string;\n readonly reason: string;\n}\n\n/** Paths a finding names, in either of the shapes spec-brief reports them. */\nfunction pathsOf(finding: WaiveContext['findings'][number]): string[] {\n if (finding.path !== undefined) return [finding.path];\n return [...(finding.paths ?? [])];\n}\n\nexport async function waive(context: WaiveContext): Promise<Waiver[]> {\n const protectedPaths = context.findings.filter((finding) => finding.rule === 'protected-file').flatMap(pathsOf);\n if (protectedPaths.length === 0) return [];\n const { config } = await loadConfig(context.root);\n const workspace: Workspace = {\n root: context.root,\n commonDir: (await commonDirectory(context.root)) ?? `${context.root}/.git`,\n config,\n siblings: createSiblings(context.root, config),\n branch: await currentBranch(context.root),\n cwd: context.root,\n };\n const base = await resolveBase(workspace, context.base ?? undefined);\n const brief: BriefRow = {\n id: context.brief.id ?? context.brief.file,\n file: context.brief.file,\n title: null,\n phase: 'live',\n status: null,\n type: null,\n wave: null,\n dependsOn: [],\n affectedFiles: [],\n protectedFiles: [],\n tasks: { total: 0, checked: 0 },\n ready: false,\n waitingOn: [],\n };\n const { verified } = await checkRulings(workspace, brief, context.brief.text, createReader(), base);\n const waivers: Waiver[] = [];\n for (const path of protectedPaths) {\n const ruling = rulingFor(verified, path);\n if (ruling !== undefined) {\n waivers.push({ rule: 'protected-file', path, reason: `ruling ${ruling.id}, signed by ${ruling.signer}, allows it` });\n }\n }\n return waivers;\n}\n\n/** The plugin, as spec-brief loads it: a function of its options returning `{ name, rules, waive }`. */\nexport default function specHarnessPlugin(): { name: string; rules: readonly never[]; waive: typeof waive } {\n return { name: 'spec-harness', rules: [], waive };\n}\n"]}
@@ -11,6 +11,10 @@
11
11
  *
12
12
  * Goals are the audit's (`audit`), not this: a goal fails until its round is
13
13
  * done, and a build that failed for that would fail for every brief in flight.
14
+ * For the same reason the premise of the brief a round is working on - the
15
+ * one the flag, SPEC_BRIEF or the branch names - is reported as the audit
16
+ * reports it, retired as the round intends, and fails nothing: on the round's
17
+ * branch, a premise that no longer holds is the work being done.
14
18
  */
15
19
  import { type CliIO, type Options } from './workspace.js';
16
20
  export declare function premisesCommand(options: Options, io: CliIO): Promise<number>;
package/dist/premises.js CHANGED
@@ -11,15 +11,25 @@
11
11
  *
12
12
  * Goals are the audit's (`audit`), not this: a goal fails until its round is
13
13
  * done, and a build that failed for that would fail for every brief in flight.
14
+ * For the same reason the premise of the brief a round is working on - the
15
+ * one the flag, SPEC_BRIEF or the branch names - is reported as the audit
16
+ * reports it, retired as the round intends, and fails nothing: on the round's
17
+ * branch, a premise that no longer holds is the work being done.
14
18
  */
15
- import { isPremise } from './audit.js';
19
+ import { isPremise, premiseFinding } from './audit.js';
20
+ import { briefIdFromBranch } from './branch.js';
21
+ import { findActive } from './briefs.js';
16
22
  import { createReader } from './reader.js';
17
23
  import { briefText } from './round.js';
18
24
  import { EXIT_ERROR, EXIT_FAILED, EXIT_OK, json, openWorkspace } from './workspace.js';
19
25
  export async function premisesCommand(options, io) {
20
26
  const workspace = await openWorkspace(options, io);
21
27
  const reader = createReader();
22
- const live = (await workspace.siblings.briefs()).filter((brief) => brief.phase === 'live');
28
+ const briefs = await workspace.siblings.briefs();
29
+ const live = briefs.filter((brief) => brief.phase === 'live');
30
+ const fromBranch = workspace.branch === null ? null : briefIdFromBranch(workspace.config.branches, workspace.branch);
31
+ const active = findActive(briefs, { flag: options.brief, environment: io.env['SPEC_BRIEF'], branch: fromBranch });
32
+ const round = active.kind === 'found' ? active.brief.file : null;
23
33
  if (live.length === 0) {
24
34
  io.stdout.write(options.format === 'json' ? json('premises', { ok: true, checked: 0, findings: [] }) : 'no live brief\n');
25
35
  return EXIT_OK;
@@ -48,24 +58,20 @@ export async function premisesCommand(options, io) {
48
58
  premises += 1;
49
59
  if (result.ok)
50
60
  continue;
51
- findings.push({
52
- rule: 'stale-premise',
53
- severity: 'error',
54
- message: `brief ${brief.id}'s premise no longer holds: ${result.description}: ${result.message}`,
55
- hint: 'what the brief was written against has changed; archive the brief if its work is done, or rewrite its premise before a round is run on it',
56
- file: brief.file,
57
- line,
58
- });
61
+ findings.push(premiseFinding(brief, { description: result.description, message: result.message, line }, brief.file === round));
59
62
  }
60
63
  }
64
+ const stale = findings.filter((finding) => finding.severity === 'error').length;
65
+ const retired = findings.length - stale;
61
66
  if (options.format === 'json') {
62
- io.stdout.write(json('premises', { ok: findings.length === 0, briefs: live.length, premises, findings }));
67
+ io.stdout.write(json('premises', { ok: stale === 0, briefs: live.length, premises, findings }));
63
68
  }
64
69
  else {
65
70
  for (const finding of findings)
66
- io.stdout.write(`error ${finding.file}:${finding.line} ${finding.message}\n ${finding.hint}\n`);
67
- io.stdout.write(`${premises} premise(s) in ${live.length} live brief(s), ${findings.length} no longer hold\n`);
71
+ 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`);
68
74
  }
69
- return findings.length > 0 ? EXIT_FAILED : EXIT_OK;
75
+ return stale > 0 ? EXIT_FAILED : EXIT_OK;
70
76
  }
71
77
  //# sourceMappingURL=premises.js.map