@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
package/dist/guard.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * May this round write this file?
3
+ *
4
+ * The answer is read from the brief the round runs under: `protectedFiles`
5
+ * are refused unless a person has ruled otherwise, `affectedFiles` are the
6
+ * round's to write, and anything else is outside the scope the person
7
+ * approved. A guard is a guardrail, not a boundary - an agent that writes
8
+ * through a shell passes it - so the audit and spec-brief's archive are the
9
+ * gates, and this is what keeps an honest agent from needing them (ADR-0005).
10
+ */
11
+ import { parseGlob } from './vendor/spec-core/pattern/index.js';
12
+ function compile(pattern) {
13
+ const parsed = parseGlob(pattern, { dialect: 'path', caseSensitive: true, literal: 'either' });
14
+ return parsed.ok ? parsed.glob : parsed.error;
15
+ }
16
+ function matching(patterns, path) {
17
+ const matched = [];
18
+ const unreadable = [];
19
+ for (const pattern of patterns) {
20
+ const glob = compile(pattern);
21
+ if (typeof glob === 'string')
22
+ unreadable.push(`${pattern} (${glob})`);
23
+ else if (glob.match(path))
24
+ matched.push(pattern);
25
+ }
26
+ return { matched, unreadable };
27
+ }
28
+ 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
+ /** Decides one path. Pure: the caller resolved the path, the brief and the rulings. */
30
+ export function decide(input) {
31
+ const { path, given, brief } = input;
32
+ if (path === null) {
33
+ return {
34
+ path: given,
35
+ verdict: 'allow',
36
+ reason: 'outside-repository',
37
+ because: [],
38
+ message: `${given} is outside the repository; no brief governs it`,
39
+ hint: 'nothing to do',
40
+ };
41
+ }
42
+ if (brief === null) {
43
+ return {
44
+ path,
45
+ verdict: 'allow',
46
+ reason: 'no-brief',
47
+ because: [],
48
+ message: `no brief governs ${path}: ${input.noBrief ?? 'no active brief'}`,
49
+ hint: 'name the brief this work belongs to, so its scope can be checked',
50
+ };
51
+ }
52
+ if (path === brief.file) {
53
+ return {
54
+ path,
55
+ verdict: 'allow',
56
+ reason: 'brief-file',
57
+ because: [],
58
+ message: `${path} is brief ${brief.id} itself`,
59
+ hint: 'tick or disposition its boxes as the work lands',
60
+ };
61
+ }
62
+ const protectedHits = matching(brief.protectedFiles, path);
63
+ if (protectedHits.unreadable.length > 0) {
64
+ // A protection that cannot be read cannot be honoured, and cannot be
65
+ // assumed not to apply: refuse until the brief is fixed.
66
+ return {
67
+ path,
68
+ verdict: 'deny',
69
+ reason: 'unreadable-protection',
70
+ because: protectedHits.unreadable,
71
+ message: `brief ${brief.id} protects files with a pattern that cannot be read: ${protectedHits.unreadable.join('; ')}`,
72
+ hint: `fix protectedFiles in ${brief.file} (spec-brief lint names the problem)`,
73
+ };
74
+ }
75
+ if (protectedHits.matched.length > 0) {
76
+ const ruling = input.rulings.find((candidate) => matching(candidate.paths, path).matched.length > 0);
77
+ if (ruling !== undefined) {
78
+ return {
79
+ path,
80
+ verdict: 'allow',
81
+ reason: 'ruled',
82
+ because: [ruling.id],
83
+ message: `${path} is protected by brief ${brief.id}, and ruling ${ruling.id}, signed by ${ruling.signer}, allows it`,
84
+ hint: 'keep the change to what the ruling allows',
85
+ };
86
+ }
87
+ return {
88
+ path,
89
+ verdict: 'deny',
90
+ reason: 'protected',
91
+ because: protectedHits.matched,
92
+ message: `brief ${brief.id} does not empower this round to change ${path} (protectedFiles: ${protectedHits.matched.join(', ')})`,
93
+ hint: ESCALATE,
94
+ };
95
+ }
96
+ const affectedHits = matching(brief.affectedFiles, path);
97
+ if (affectedHits.matched.length > 0) {
98
+ return {
99
+ path,
100
+ verdict: 'allow',
101
+ reason: 'in-scope',
102
+ because: affectedHits.matched,
103
+ message: `${path} is in brief ${brief.id}'s scope (${affectedHits.matched.join(', ')})`,
104
+ hint: 'nothing to do',
105
+ };
106
+ }
107
+ const verdict = input.outOfScope;
108
+ const scope = brief.affectedFiles.length === 0 ? 'declares no affectedFiles' : `covers ${brief.affectedFiles.join(', ')}`;
109
+ return {
110
+ path,
111
+ verdict,
112
+ reason: 'out-of-scope',
113
+ because: affectedHits.unreadable,
114
+ message: `${path} is outside brief ${brief.id}'s scope, which ${scope}`,
115
+ hint: verdict === 'deny'
116
+ ? `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}`
117
+ : `if the round needs it, say so in ${brief.file} and add it to affectedFiles; the archive reports every file outside the scope`,
118
+ };
119
+ }
120
+ //# sourceMappingURL=guard.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The guard, spoken in the languages that ask it: an agent's pre- and
3
+ * post-tool hooks, and git's pre-commit.
4
+ *
5
+ * Claude Code sends a hook one JSON object on stdin naming the tool and its
6
+ * input. A file-editing tool names its file under `file_path`, `path` or
7
+ * `notebook_path` depending on the tool and the release; all three are read,
8
+ * because a guard that missed a renamed field would allow everything.
9
+ *
10
+ * What the answer may not do matters as much as what it does. Returning
11
+ * `allow` from a pre-tool hook skips the person's own permission prompt, so
12
+ * the guard never says `allow`: it says `deny`, or `ask`, or nothing. A
13
+ * warning - a write outside the scope when the repository asked only to be
14
+ * warned - is told to the model after the write, through the post-tool hook's
15
+ * additional context, where it cannot unlock anything.
16
+ */
17
+ import type { Decision } from './guard.js';
18
+ /** The tools whose input names a file they write. */
19
+ export declare const WRITING_TOOLS: readonly string[];
20
+ export interface HookRequest {
21
+ readonly event: string;
22
+ readonly tool: string;
23
+ /** The session's working directory, when the hook says. */
24
+ readonly cwd: string | null;
25
+ /** Files the tool writes, as the tool named them - absolute, usually. */
26
+ readonly paths: readonly string[];
27
+ }
28
+ /** Reads a Claude Code hook's stdin, or says why it cannot. */
29
+ export declare function parseClaudeHook(raw: string): HookRequest | {
30
+ readonly error: string;
31
+ };
32
+ export interface HookResponse {
33
+ /** What to print on stdout: JSON, or nothing. */
34
+ readonly stdout: string;
35
+ readonly exitCode: number;
36
+ }
37
+ /**
38
+ * The answer to a Claude Code hook: a refusal or a question before a write,
39
+ * a warning after one, or nothing.
40
+ */
41
+ export declare function claudeResponse(event: string, decisions: readonly Decision[]): HookResponse;
42
+ /**
43
+ * The answer to git's pre-commit hook: every file the commit would change
44
+ * that the round may not, and exit 1 if any is refused. A warning prints and
45
+ * lets the commit through; a question has no one to ask in git, so it refuses.
46
+ */
47
+ export declare function gitResponse(decisions: readonly Decision[]): {
48
+ readonly text: string;
49
+ readonly exitCode: number;
50
+ };
package/dist/hooks.js ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The guard, spoken in the languages that ask it: an agent's pre- and
3
+ * post-tool hooks, and git's pre-commit.
4
+ *
5
+ * Claude Code sends a hook one JSON object on stdin naming the tool and its
6
+ * input. A file-editing tool names its file under `file_path`, `path` or
7
+ * `notebook_path` depending on the tool and the release; all three are read,
8
+ * because a guard that missed a renamed field would allow everything.
9
+ *
10
+ * What the answer may not do matters as much as what it does. Returning
11
+ * `allow` from a pre-tool hook skips the person's own permission prompt, so
12
+ * the guard never says `allow`: it says `deny`, or `ask`, or nothing. A
13
+ * warning - a write outside the scope when the repository asked only to be
14
+ * warned - is told to the model after the write, through the post-tool hook's
15
+ * additional context, where it cannot unlock anything.
16
+ */
17
+ /** The tools whose input names a file they write. */
18
+ export const WRITING_TOOLS = ['Edit', 'Write', 'MultiEdit', 'NotebookEdit'];
19
+ const PATH_FIELDS = ['file_path', 'path', 'notebook_path'];
20
+ function isObject(value) {
21
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
22
+ }
23
+ /** Reads a Claude Code hook's stdin, or says why it cannot. */
24
+ export function parseClaudeHook(raw) {
25
+ let value;
26
+ try {
27
+ value = JSON.parse(raw);
28
+ }
29
+ catch {
30
+ return { error: 'the hook input is not JSON' };
31
+ }
32
+ if (!isObject(value))
33
+ return { error: 'the hook input is not a JSON object' };
34
+ const event = value['hook_event_name'];
35
+ const tool = value['tool_name'];
36
+ if (typeof event !== 'string' || typeof tool !== 'string')
37
+ return { error: 'the hook input names no event or tool' };
38
+ const input = isObject(value['tool_input']) ? value['tool_input'] : {};
39
+ const paths = [];
40
+ if (WRITING_TOOLS.includes(tool)) {
41
+ for (const field of PATH_FIELDS) {
42
+ const path = input[field];
43
+ if (typeof path === 'string' && path !== '' && !paths.includes(path))
44
+ paths.push(path);
45
+ }
46
+ }
47
+ const cwd = value['cwd'];
48
+ return { event, tool, cwd: typeof cwd === 'string' ? cwd : null, paths };
49
+ }
50
+ const NONE = { stdout: '', exitCode: 0 };
51
+ function reasonOf(decisions) {
52
+ return decisions.map((decision) => `spec-harness: ${decision.message}. Next: ${decision.hint}.`).join('\n');
53
+ }
54
+ /**
55
+ * The answer to a Claude Code hook: a refusal or a question before a write,
56
+ * a warning after one, or nothing.
57
+ */
58
+ export function claudeResponse(event, decisions) {
59
+ if (event === 'PreToolUse') {
60
+ const denied = decisions.filter((decision) => decision.verdict === 'deny');
61
+ const asked = decisions.filter((decision) => decision.verdict === 'ask');
62
+ const decided = denied.length > 0 ? { verdict: 'deny', list: denied } : asked.length > 0 ? { verdict: 'ask', list: asked } : null;
63
+ if (decided === null)
64
+ return NONE;
65
+ return {
66
+ stdout: `${JSON.stringify({
67
+ hookSpecificOutput: {
68
+ hookEventName: 'PreToolUse',
69
+ permissionDecision: decided.verdict,
70
+ permissionDecisionReason: reasonOf(decided.list),
71
+ },
72
+ })}\n`,
73
+ exitCode: 0,
74
+ };
75
+ }
76
+ if (event === 'PostToolUse') {
77
+ const warned = decisions.filter((decision) => decision.verdict === 'warn');
78
+ if (warned.length === 0)
79
+ return NONE;
80
+ return {
81
+ stdout: `${JSON.stringify({
82
+ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: reasonOf(warned) },
83
+ })}\n`,
84
+ exitCode: 0,
85
+ };
86
+ }
87
+ return NONE;
88
+ }
89
+ /**
90
+ * The answer to git's pre-commit hook: every file the commit would change
91
+ * that the round may not, and exit 1 if any is refused. A warning prints and
92
+ * lets the commit through; a question has no one to ask in git, so it refuses.
93
+ */
94
+ export function gitResponse(decisions) {
95
+ const lines = [];
96
+ let refused = 0;
97
+ for (const decision of decisions) {
98
+ if (decision.verdict === 'allow')
99
+ continue;
100
+ const blocking = decision.verdict === 'deny' || decision.verdict === 'ask';
101
+ if (blocking)
102
+ refused += 1;
103
+ lines.push(`${blocking ? 'refused' : 'warning'} ${decision.message}`, ` ${decision.hint}`);
104
+ }
105
+ if (refused > 0)
106
+ lines.push(`spec-harness: ${refused} file(s) this round may not change; the commit was stopped`);
107
+ return { text: lines.length === 0 ? '' : `${lines.join('\n')}\n`, exitCode: refused > 0 ? 1 : 0 };
108
+ }
109
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The guard, spoken in the languages that ask it: an agent's pre- and\n * post-tool hooks, and git's pre-commit.\n *\n * Claude Code sends a hook one JSON object on stdin naming the tool and its\n * input. A file-editing tool names its file under `file_path`, `path` or\n * `notebook_path` depending on the tool and the release; all three are read,\n * because a guard that missed a renamed field would allow everything.\n *\n * What the answer may not do matters as much as what it does. Returning\n * `allow` from a pre-tool hook skips the person's own permission prompt, so\n * the guard never says `allow`: it says `deny`, or `ask`, or nothing. A\n * warning - a write outside the scope when the repository asked only to be\n * warned - is told to the model after the write, through the post-tool hook's\n * additional context, where it cannot unlock anything.\n */\n\nimport type { Decision } from './guard.js';\n\n/** The tools whose input names a file they write. */\nexport const WRITING_TOOLS: readonly string[] = ['Edit', 'Write', 'MultiEdit', 'NotebookEdit'];\n\nconst PATH_FIELDS = ['file_path', 'path', 'notebook_path'] as const;\n\nexport interface HookRequest {\n readonly event: string;\n readonly tool: string;\n /** The session's working directory, when the hook says. */\n readonly cwd: string | null;\n /** Files the tool writes, as the tool named them - absolute, usually. */\n readonly paths: readonly string[];\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\n/** Reads a Claude Code hook's stdin, or says why it cannot. */\nexport function parseClaudeHook(raw: string): HookRequest | { readonly error: string } {\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch {\n return { error: 'the hook input is not JSON' };\n }\n if (!isObject(value)) return { error: 'the hook input is not a JSON object' };\n const event = value['hook_event_name'];\n const tool = value['tool_name'];\n if (typeof event !== 'string' || typeof tool !== 'string') return { error: 'the hook input names no event or tool' };\n const input = isObject(value['tool_input']) ? value['tool_input'] : {};\n const paths: string[] = [];\n if (WRITING_TOOLS.includes(tool)) {\n for (const field of PATH_FIELDS) {\n const path = input[field];\n if (typeof path === 'string' && path !== '' && !paths.includes(path)) paths.push(path);\n }\n }\n const cwd = value['cwd'];\n return { event, tool, cwd: typeof cwd === 'string' ? cwd : null, paths };\n}\n\nexport interface HookResponse {\n /** What to print on stdout: JSON, or nothing. */\n readonly stdout: string;\n readonly exitCode: number;\n}\n\nconst NONE: HookResponse = { stdout: '', exitCode: 0 };\n\nfunction reasonOf(decisions: readonly Decision[]): string {\n return decisions.map((decision) => `spec-harness: ${decision.message}. Next: ${decision.hint}.`).join('\\n');\n}\n\n/**\n * The answer to a Claude Code hook: a refusal or a question before a write,\n * a warning after one, or nothing.\n */\nexport function claudeResponse(event: string, decisions: readonly Decision[]): HookResponse {\n if (event === 'PreToolUse') {\n const denied = decisions.filter((decision) => decision.verdict === 'deny');\n const asked = decisions.filter((decision) => decision.verdict === 'ask');\n const decided = denied.length > 0 ? { verdict: 'deny', list: denied } : asked.length > 0 ? { verdict: 'ask', list: asked } : null;\n if (decided === null) return NONE;\n return {\n stdout: `${JSON.stringify({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: decided.verdict,\n permissionDecisionReason: reasonOf(decided.list),\n },\n })}\\n`,\n exitCode: 0,\n };\n }\n if (event === 'PostToolUse') {\n const warned = decisions.filter((decision) => decision.verdict === 'warn');\n if (warned.length === 0) return NONE;\n return {\n stdout: `${JSON.stringify({\n hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: reasonOf(warned) },\n })}\\n`,\n exitCode: 0,\n };\n }\n return NONE;\n}\n\n/**\n * The answer to git's pre-commit hook: every file the commit would change\n * that the round may not, and exit 1 if any is refused. A warning prints and\n * lets the commit through; a question has no one to ask in git, so it refuses.\n */\nexport function gitResponse(decisions: readonly Decision[]): { readonly text: string; readonly exitCode: number } {\n const lines: string[] = [];\n let refused = 0;\n for (const decision of decisions) {\n if (decision.verdict === 'allow') continue;\n const blocking = decision.verdict === 'deny' || decision.verdict === 'ask';\n if (blocking) refused += 1;\n lines.push(`${blocking ? 'refused' : 'warning'} ${decision.message}`, ` ${decision.hint}`);\n }\n if (refused > 0) lines.push(`spec-harness: ${refused} file(s) this round may not change; the commit was stopped`);\n return { text: lines.length === 0 ? '' : `${lines.join('\\n')}\\n`, exitCode: refused > 0 ? 1 : 0 };\n}\n"]}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Programmatic API. The command line is a thin shell over these; a harness of
3
+ * another kind - a CI job, an orchestrator - can ask the same questions.
4
+ */
5
+ export { main, run, EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version } from './cli.js';
6
+ export type { CliIO } from './cli.js';
7
+ export { audit } from './audit.js';
8
+ export type { AssertionOutcome, AuditInput, AuditReport } from './audit.js';
9
+ export { briefIdFromBranch, idFromBranch, sameId, templateError } from './branch.js';
10
+ export { findActive, parseBriefList, SiblingOutputError, SUPPORTED_SCHEMA_VERSIONS } from './briefs.js';
11
+ export type { ActiveBrief, ActiveSource } from './briefs.js';
12
+ export { ConfigError, CONFIG_FILE, DEFAULT_CONFIG, parseConfig, SIBLINGS } from './config.js';
13
+ export type { HarnessConfig, OutOfScope, SiblingName } from './config.js';
14
+ export { renderContext } from './context.js';
15
+ export type { CitedDocument, ContextInput, ContextPacket, RuleInForce, RulingInForce } from './context.js';
16
+ export { decide } from './guard.js';
17
+ export type { Decision, GuardInput, Reason, Verdict, VerifiedRuling } from './guard.js';
18
+ export { claudeResponse, gitResponse, parseClaudeHook, WRITING_TOOLS } from './hooks.js';
19
+ export type { HookRequest, HookResponse } from './hooks.js';
20
+ export { readJUnit } from './junit.js';
21
+ export type { JUnitCase, JUnitRead } from './junit.js';
22
+ export { diffManifest, ecosystemOf, manifestMatcher, readManifest } from './manifests.js';
23
+ export type { Dependency, DependencyChange, Ecosystem } from './manifests.js';
24
+ export { classify, readProbes, renderEvidence, verdictOf } from './probe.js';
25
+ export type { Classified, CodeBlock, ProbeFile, ProbeResult, ProbeRun, ProbeSet, ProbeSpec } from './probe.js';
26
+ export { createReader } from './reader.js';
27
+ export { addRulingRow, nextId, pathsOf, readRulings, renderMemo, renderRow } from './rulings.js';
28
+ export type { EscalationOption, EscalationRequest, RulingRow, TableView } from './rulings.js';
29
+ export { buildContext, checkPaths, checkRulings, raiseEscalation, recordRuling, resolveBase, runAudit } from './round.js';
30
+ export { mergeClaudeSettings, mergeMcp, mergeSpecGraph, HOOK_COMMAND } from './setup.js';
31
+ export type { BriefRow, FileChange, Finding, Severity } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Programmatic API. The command line is a thin shell over these; a harness of
3
+ * another kind - a CI job, an orchestrator - can ask the same questions.
4
+ */
5
+ export { main, run, EXIT_ERROR, EXIT_FAILED, EXIT_OK, UsageError, version } from './cli.js';
6
+ export { audit } from './audit.js';
7
+ export { briefIdFromBranch, idFromBranch, sameId, templateError } from './branch.js';
8
+ export { findActive, parseBriefList, SiblingOutputError, SUPPORTED_SCHEMA_VERSIONS } from './briefs.js';
9
+ export { ConfigError, CONFIG_FILE, DEFAULT_CONFIG, parseConfig, SIBLINGS } from './config.js';
10
+ export { renderContext } from './context.js';
11
+ export { decide } from './guard.js';
12
+ export { claudeResponse, gitResponse, parseClaudeHook, WRITING_TOOLS } from './hooks.js';
13
+ export { readJUnit } from './junit.js';
14
+ export { diffManifest, ecosystemOf, manifestMatcher, readManifest } from './manifests.js';
15
+ export { classify, readProbes, renderEvidence, verdictOf } from './probe.js';
16
+ export { createReader } from './reader.js';
17
+ export { addRulingRow, nextId, pathsOf, readRulings, renderMemo, renderRow } from './rulings.js';
18
+ export { buildContext, checkPaths, checkRulings, raiseEscalation, recordRuling, resolveBase, runAudit } from './round.js';
19
+ export { mergeClaudeSettings, mergeMcp, mergeSpecGraph, HOOK_COMMAND } from './setup.js';
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The failures a JUnit XML report records.
3
+ *
4
+ * JUnit XML is the one result format nearly every test runner writes, natively
5
+ * or through a reporter - Vitest, Jest, pytest, `go test` through
6
+ * go-junit-report, `cargo nextest`, `dotnet test` through its logger - so a
7
+ * probe that reads it reads them all (ADR-0007). This reads the part of the
8
+ * format a probe needs: which test cases failed or errored, and with what
9
+ * message. It is not an XML parser; it is a tokenizer for elements and
10
+ * attributes, entity-aware, that skips everything else, and it refuses text
11
+ * it cannot read rather than guessing at it.
12
+ */
13
+ export interface JUnitCase {
14
+ readonly name: string;
15
+ readonly classname: string;
16
+ readonly outcome: 'passed' | 'failed' | 'errored' | 'skipped';
17
+ /** The failure's or error's message and body, joined. */
18
+ readonly message: string;
19
+ }
20
+ export type JUnitRead = {
21
+ readonly ok: true;
22
+ readonly cases: readonly JUnitCase[];
23
+ } | {
24
+ readonly ok: false;
25
+ readonly error: string;
26
+ };
27
+ /** Replaces the five named entities and numeric references. An unknown entity stays as written. */
28
+ export declare function decodeEntities(text: string): string;
29
+ /** Reads the test cases of a JUnit XML report, nested suites included. */
30
+ export declare function readJUnit(xml: string): JUnitRead;
package/dist/junit.js ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * The failures a JUnit XML report records.
3
+ *
4
+ * JUnit XML is the one result format nearly every test runner writes, natively
5
+ * or through a reporter - Vitest, Jest, pytest, `go test` through
6
+ * go-junit-report, `cargo nextest`, `dotnet test` through its logger - so a
7
+ * probe that reads it reads them all (ADR-0007). This reads the part of the
8
+ * format a probe needs: which test cases failed or errored, and with what
9
+ * message. It is not an XML parser; it is a tokenizer for elements and
10
+ * attributes, entity-aware, that skips everything else, and it refuses text
11
+ * it cannot read rather than guessing at it.
12
+ */
13
+ const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
14
+ /** Replaces the five named entities and numeric references. An unknown entity stays as written. */
15
+ export function decodeEntities(text) {
16
+ return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/g, (whole, body) => {
17
+ if (body.startsWith('#x')) {
18
+ const point = Number.parseInt(body.slice(2), 16);
19
+ return point <= 0x10ffff ? String.fromCodePoint(point) : whole;
20
+ }
21
+ if (body.startsWith('#')) {
22
+ const point = Number.parseInt(body.slice(1), 10);
23
+ return point <= 0x10ffff ? String.fromCodePoint(point) : whole;
24
+ }
25
+ return ENTITIES[body] ?? whole;
26
+ });
27
+ }
28
+ function attributes(text) {
29
+ const out = {};
30
+ const pattern = /([A-Za-z_:][\w:.-]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
31
+ for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
32
+ out[match[1]] = decodeEntities(match[3] ?? match[4] ?? '');
33
+ }
34
+ return out;
35
+ }
36
+ /** The tags of a document, comments, CDATA and declarations skipped, in one pass. */
37
+ function tags(xml) {
38
+ const out = [];
39
+ let i = 0;
40
+ while (i < xml.length) {
41
+ const open = xml.indexOf('<', i);
42
+ if (open < 0)
43
+ break;
44
+ if (xml.startsWith('<!--', open)) {
45
+ const close = xml.indexOf('-->', open + 4);
46
+ if (close < 0)
47
+ return 'a comment is never closed';
48
+ i = close + 3;
49
+ continue;
50
+ }
51
+ if (xml.startsWith('<![CDATA[', open)) {
52
+ const close = xml.indexOf(']]>', open + 9);
53
+ if (close < 0)
54
+ return 'a CDATA section is never closed';
55
+ i = close + 3;
56
+ continue;
57
+ }
58
+ if (xml.startsWith('<?', open) || xml.startsWith('<!', open)) {
59
+ const close = xml.indexOf('>', open);
60
+ if (close < 0)
61
+ return 'a declaration is never closed';
62
+ i = close + 1;
63
+ continue;
64
+ }
65
+ let close = open + 1;
66
+ let quote = null;
67
+ for (; close < xml.length; close += 1) {
68
+ const ch = xml.charAt(close);
69
+ if (quote !== null) {
70
+ if (ch === quote)
71
+ quote = null;
72
+ }
73
+ else if (ch === '"' || ch === "'")
74
+ quote = ch;
75
+ else if (ch === '>')
76
+ break;
77
+ }
78
+ if (close >= xml.length)
79
+ return 'a tag is never closed';
80
+ const body = xml.slice(open + 1, close);
81
+ const closing = body.startsWith('/');
82
+ const self = body.endsWith('/');
83
+ const name = /^\/?\s*([A-Za-z_:][\w:.-]*)/.exec(body)?.[1];
84
+ if (name === undefined)
85
+ return `"<${body.slice(0, 20)}" is not a tag`;
86
+ out.push({
87
+ kind: closing ? 'close' : self ? 'self' : 'open',
88
+ name,
89
+ attributes: closing ? {} : attributes(body.slice(name.length + 1)),
90
+ start: open,
91
+ end: close + 1,
92
+ });
93
+ i = close + 1;
94
+ }
95
+ return out;
96
+ }
97
+ /**
98
+ * The text between two offsets, tags dropped and entities decoded outside
99
+ * CDATA, and CDATA kept as written: a stack trace's `Object.<anonymous>` or
100
+ * `a < b` inside CDATA is text, not a tag.
101
+ */
102
+ function innerText(xml, from, to) {
103
+ const slice = xml.slice(from, to);
104
+ const markup = (text) => decodeEntities(text.replace(/<[^>]*>/g, ''));
105
+ let out = '';
106
+ let last = 0;
107
+ for (const match of slice.matchAll(/<!\[CDATA\[([\s\S]*?)\]\]>/g)) {
108
+ out += markup(slice.slice(last, match.index)) + match[1];
109
+ last = match.index + match[0].length;
110
+ }
111
+ return (out + markup(slice.slice(last))).trim();
112
+ }
113
+ /** Reads the test cases of a JUnit XML report, nested suites included. */
114
+ export function readJUnit(xml) {
115
+ const list = tags(xml);
116
+ if (typeof list === 'string')
117
+ return { ok: false, error: list };
118
+ if (!list.some((tag) => tag.name === 'testsuite' || tag.name === 'testsuites')) {
119
+ return { ok: false, error: 'no <testsuite> element: this is not a JUnit report' };
120
+ }
121
+ const cases = [];
122
+ for (let i = 0; i < list.length; i += 1) {
123
+ const tag = list[i];
124
+ if (tag.name !== 'testcase' || tag.kind === 'close')
125
+ continue;
126
+ const name = tag.attributes['name'] ?? '';
127
+ const classname = tag.attributes['classname'] ?? '';
128
+ if (tag.kind === 'self') {
129
+ cases.push({ name, classname, outcome: 'passed', message: '' });
130
+ continue;
131
+ }
132
+ let outcome = 'passed';
133
+ const messages = [];
134
+ let j = i + 1;
135
+ for (; j < list.length; j += 1) {
136
+ const inner = list[j];
137
+ if (inner.name === 'testcase' && inner.kind === 'close')
138
+ break;
139
+ if (inner.kind === 'close')
140
+ continue;
141
+ if (inner.name === 'failure' || inner.name === 'error') {
142
+ outcome = inner.name === 'failure' ? 'failed' : outcome === 'failed' ? 'failed' : 'errored';
143
+ const message = inner.attributes['message'];
144
+ if (message !== undefined && message !== '')
145
+ messages.push(message);
146
+ if (inner.kind === 'open') {
147
+ const end = list.findIndex((candidate, k) => k > j && candidate.name === inner.name && candidate.kind === 'close');
148
+ if (end > j) {
149
+ const body = innerText(xml, inner.end, list[end].start);
150
+ if (body !== '')
151
+ messages.push(body);
152
+ }
153
+ }
154
+ }
155
+ else if (inner.name === 'skipped' && outcome === 'passed') {
156
+ outcome = 'skipped';
157
+ }
158
+ }
159
+ if (j >= list.length)
160
+ return { ok: false, error: `test case "${name}" is never closed` };
161
+ cases.push({ name, classname, outcome, message: messages.join('\n') });
162
+ i = j;
163
+ }
164
+ return { ok: true, cases };
165
+ }
166
+ //# sourceMappingURL=junit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"junit.js","sourceRoot":"","sources":["../src/junit.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The failures a JUnit XML report records.\n *\n * JUnit XML is the one result format nearly every test runner writes, natively\n * or through a reporter - Vitest, Jest, pytest, `go test` through\n * go-junit-report, `cargo nextest`, `dotnet test` through its logger - so a\n * probe that reads it reads them all (ADR-0007). This reads the part of the\n * format a probe needs: which test cases failed or errored, and with what\n * message. It is not an XML parser; it is a tokenizer for elements and\n * attributes, entity-aware, that skips everything else, and it refuses text\n * it cannot read rather than guessing at it.\n */\n\nexport interface JUnitCase {\n readonly name: string;\n readonly classname: string;\n readonly outcome: 'passed' | 'failed' | 'errored' | 'skipped';\n /** The failure's or error's message and body, joined. */\n readonly message: string;\n}\n\nexport type JUnitRead = { readonly ok: true; readonly cases: readonly JUnitCase[] } | { readonly ok: false; readonly error: string };\n\nconst ENTITIES: Readonly<Record<string, string>> = { amp: '&', lt: '<', gt: '>', quot: '\"', apos: \"'\" };\n\n/** Replaces the five named entities and numeric references. An unknown entity stays as written. */\nexport function decodeEntities(text: string): string {\n return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/g, (whole, body: string) => {\n if (body.startsWith('#x')) {\n const point = Number.parseInt(body.slice(2), 16);\n return point <= 0x10ffff ? String.fromCodePoint(point) : whole;\n }\n if (body.startsWith('#')) {\n const point = Number.parseInt(body.slice(1), 10);\n return point <= 0x10ffff ? String.fromCodePoint(point) : whole;\n }\n return ENTITIES[body] ?? whole;\n });\n}\n\nfunction attributes(text: string): Record<string, string> {\n const out: Record<string, string> = {};\n const pattern = /([A-Za-z_:][\\w:.-]*)\\s*=\\s*(\"([^\"]*)\"|'([^']*)')/g;\n for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {\n out[match[1] as string] = decodeEntities(match[3] ?? match[4] ?? '');\n }\n return out;\n}\n\ninterface Tag {\n readonly kind: 'open' | 'close' | 'self';\n readonly name: string;\n readonly attributes: Record<string, string>;\n readonly start: number;\n readonly end: number;\n}\n\n/** The tags of a document, comments, CDATA and declarations skipped, in one pass. */\nfunction tags(xml: string): Tag[] | string {\n const out: Tag[] = [];\n let i = 0;\n while (i < xml.length) {\n const open = xml.indexOf('<', i);\n if (open < 0) break;\n if (xml.startsWith('<!--', open)) {\n const close = xml.indexOf('-->', open + 4);\n if (close < 0) return 'a comment is never closed';\n i = close + 3;\n continue;\n }\n if (xml.startsWith('<![CDATA[', open)) {\n const close = xml.indexOf(']]>', open + 9);\n if (close < 0) return 'a CDATA section is never closed';\n i = close + 3;\n continue;\n }\n if (xml.startsWith('<?', open) || xml.startsWith('<!', open)) {\n const close = xml.indexOf('>', open);\n if (close < 0) return 'a declaration is never closed';\n i = close + 1;\n continue;\n }\n let close = open + 1;\n let quote: string | null = null;\n for (; close < xml.length; close += 1) {\n const ch = xml.charAt(close);\n if (quote !== null) {\n if (ch === quote) quote = null;\n } else if (ch === '\"' || ch === \"'\") quote = ch;\n else if (ch === '>') break;\n }\n if (close >= xml.length) return 'a tag is never closed';\n const body = xml.slice(open + 1, close);\n const closing = body.startsWith('/');\n const self = body.endsWith('/');\n const name = /^\\/?\\s*([A-Za-z_:][\\w:.-]*)/.exec(body)?.[1];\n if (name === undefined) return `\"<${body.slice(0, 20)}\" is not a tag`;\n out.push({\n kind: closing ? 'close' : self ? 'self' : 'open',\n name,\n attributes: closing ? {} : attributes(body.slice(name.length + 1)),\n start: open,\n end: close + 1,\n });\n i = close + 1;\n }\n return out;\n}\n\n/**\n * The text between two offsets, tags dropped and entities decoded outside\n * CDATA, and CDATA kept as written: a stack trace's `Object.<anonymous>` or\n * `a < b` inside CDATA is text, not a tag.\n */\nfunction innerText(xml: string, from: number, to: number): string {\n const slice = xml.slice(from, to);\n const markup = (text: string): string => decodeEntities(text.replace(/<[^>]*>/g, ''));\n let out = '';\n let last = 0;\n for (const match of slice.matchAll(/<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>/g)) {\n out += markup(slice.slice(last, match.index)) + (match[1] as string);\n last = match.index + match[0].length;\n }\n return (out + markup(slice.slice(last))).trim();\n}\n\n/** Reads the test cases of a JUnit XML report, nested suites included. */\nexport function readJUnit(xml: string): JUnitRead {\n const list = tags(xml);\n if (typeof list === 'string') return { ok: false, error: list };\n if (!list.some((tag) => tag.name === 'testsuite' || tag.name === 'testsuites')) {\n return { ok: false, error: 'no <testsuite> element: this is not a JUnit report' };\n }\n const cases: JUnitCase[] = [];\n for (let i = 0; i < list.length; i += 1) {\n const tag = list[i] as Tag;\n if (tag.name !== 'testcase' || tag.kind === 'close') continue;\n const name = tag.attributes['name'] ?? '';\n const classname = tag.attributes['classname'] ?? '';\n if (tag.kind === 'self') {\n cases.push({ name, classname, outcome: 'passed', message: '' });\n continue;\n }\n let outcome: JUnitCase['outcome'] = 'passed';\n const messages: string[] = [];\n let j = i + 1;\n for (; j < list.length; j += 1) {\n const inner = list[j] as Tag;\n if (inner.name === 'testcase' && inner.kind === 'close') break;\n if (inner.kind === 'close') continue;\n if (inner.name === 'failure' || inner.name === 'error') {\n outcome = inner.name === 'failure' ? 'failed' : outcome === 'failed' ? 'failed' : 'errored';\n const message = inner.attributes['message'];\n if (message !== undefined && message !== '') messages.push(message);\n if (inner.kind === 'open') {\n const end = list.findIndex((candidate, k) => k > j && candidate.name === inner.name && candidate.kind === 'close');\n if (end > j) {\n const body = innerText(xml, inner.end, (list[end] as Tag).start);\n if (body !== '') messages.push(body);\n }\n }\n } else if (inner.name === 'skipped' && outcome === 'passed') {\n outcome = 'skipped';\n }\n }\n if (j >= list.length) return { ok: false, error: `test case \"${name}\" is never closed` };\n cases.push({ name, classname, outcome, message: messages.join('\\n') });\n i = j;\n }\n return { ok: true, cases };\n}\n"]}