@lifeaitools/rdc-skills 0.24.38 → 0.24.39

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 (191) hide show
  1. package/.claude/settings.json +15 -15
  2. package/.claude-plugin/marketplace.json +21 -21
  3. package/.claude-plugin/plugin.json +1371 -1371
  4. package/.github/workflows/publish.yml +34 -34
  5. package/.github/workflows/self-test.yml +58 -58
  6. package/CHANGELOG.md +310 -310
  7. package/LICENSE +21 -21
  8. package/MANIFEST.md +221 -221
  9. package/README.md +377 -377
  10. package/README.sandbox.md +3 -3
  11. package/assets/watcher/viewer.html +164 -164
  12. package/bin/rdc-skills-mcp.mjs +316 -316
  13. package/commands/build.md +183 -183
  14. package/commands/collab.md +180 -180
  15. package/commands/deploy.md +152 -152
  16. package/commands/design.md +31 -31
  17. package/commands/edit.md +28 -28
  18. package/commands/fixit.md +124 -124
  19. package/commands/handoff.md +173 -173
  20. package/commands/help.md +95 -95
  21. package/commands/overnight.md +220 -220
  22. package/commands/plan.md +158 -158
  23. package/commands/preplan.md +131 -131
  24. package/commands/prototype.md +145 -145
  25. package/commands/release.md +49 -49
  26. package/commands/report.md +99 -99
  27. package/commands/review.md +120 -120
  28. package/commands/self-test.md +113 -113
  29. package/commands/status.md +86 -86
  30. package/commands/watch.md +98 -98
  31. package/commands/workitems.md +137 -137
  32. package/git-sha.json +1 -1
  33. package/guides/agent-bootstrap.md +295 -295
  34. package/guides/agents/backend.md +104 -104
  35. package/guides/agents/content.md +94 -94
  36. package/guides/agents/cs2.md +56 -56
  37. package/guides/agents/data.md +87 -87
  38. package/guides/agents/design.md +77 -77
  39. package/guides/agents/frontend.md +92 -92
  40. package/guides/agents/infrastructure.md +81 -81
  41. package/guides/agents/setup.md +281 -281
  42. package/guides/agents/verify.md +151 -151
  43. package/guides/agents/viz.md +106 -106
  44. package/guides/backend.md +146 -146
  45. package/guides/content.md +147 -147
  46. package/guides/cs2.md +190 -190
  47. package/guides/data.md +123 -123
  48. package/guides/design.md +116 -116
  49. package/guides/engineering-behavior.md +43 -43
  50. package/guides/escalation-protocol.md +125 -125
  51. package/guides/frontend.md +151 -151
  52. package/guides/history-md-spec.md +297 -297
  53. package/guides/infrastructure.md +179 -179
  54. package/guides/lessons-learned-spec.md +153 -153
  55. package/guides/output-contract.md +108 -108
  56. package/guides/publish-md-spec.md +289 -289
  57. package/guides/rdc-skills-startup.md +30 -30
  58. package/guides/verify.md +11 -11
  59. package/hooks/check-cwd.js +31 -31
  60. package/hooks/check-rdc-environment.js +164 -164
  61. package/hooks/check-services.js +6 -6
  62. package/hooks/check-stale-work-items.js +19 -19
  63. package/hooks/foreground-process-gate.js +128 -128
  64. package/hooks/gate-watchdog-selfcheck.js +257 -257
  65. package/hooks/hook-logger.js +25 -25
  66. package/hooks/lib/run-evidence-gate.mjs +241 -241
  67. package/hooks/no-stop-open-epics.js +127 -127
  68. package/hooks/post-tool-batch-gate.js +203 -203
  69. package/hooks/post-work-check.js +21 -21
  70. package/hooks/postcompact-log.js +13 -13
  71. package/hooks/precompact-log.js +13 -13
  72. package/hooks/rate-limit-retry.js +46 -46
  73. package/hooks/rdc-invocation-marker.js +157 -157
  74. package/hooks/rdc-output-contract-gate.js +94 -94
  75. package/hooks/require-work-item-on-commit.js +294 -294
  76. package/hooks/restart-brief.js +19 -19
  77. package/hooks/run-hidden-hook.ps1 +47 -47
  78. package/hooks/task-completed-gate.js +274 -274
  79. package/hooks/work-item-exit-gate.js +944 -944
  80. package/lib/catalog.mjs +236 -236
  81. package/lib/cloud-rewrite.mjs +155 -155
  82. package/package.json +56 -56
  83. package/rules/work-items-rpc.md +520 -520
  84. package/scaffold/templates/HISTORY.md.template +39 -39
  85. package/scaffold/templates/PUBLISH.md.template +21 -21
  86. package/scaffold/templates/brochure-studio-default.html +70 -70
  87. package/scripts/acceptance.mjs +502 -502
  88. package/scripts/fixtures/guides/bad-guide.md +15 -15
  89. package/scripts/fixtures/guides-clean/good-guide.md +16 -16
  90. package/scripts/install-rdc-skills.js +1289 -1289
  91. package/scripts/install.ps1 +202 -202
  92. package/scripts/install.sh +132 -132
  93. package/scripts/lib/assertions.mjs +287 -287
  94. package/scripts/lib/manifest-schema.mjs +754 -754
  95. package/scripts/lib/runner.mjs +465 -465
  96. package/scripts/lib/sandbox.mjs +435 -435
  97. package/scripts/prepack.mjs +32 -32
  98. package/scripts/rdc-brochure.mjs +464 -464
  99. package/scripts/rdc-design-cli.mjs +134 -134
  100. package/scripts/rebuild-mcp.mjs +107 -107
  101. package/scripts/self-test.mjs +1460 -1460
  102. package/scripts/stamp-git-sha.mjs +29 -29
  103. package/scripts/test-guide-validator.mjs +196 -196
  104. package/scripts/test-rdc-hooks.mjs +145 -145
  105. package/scripts/uninstall.ps1 +77 -77
  106. package/scripts/uninstall.sh +69 -69
  107. package/scripts/update.ps1 +43 -43
  108. package/scripts/update.sh +43 -43
  109. package/scripts/validate-place-histories.js +461 -461
  110. package/scripts/validate-publish-manifests.js +424 -424
  111. package/scripts/watch-init.mjs +100 -100
  112. package/skills/brochure/SKILL.md +107 -107
  113. package/skills/build/SKILL.md +563 -563
  114. package/skills/channel-formatter/SKILL.md +533 -533
  115. package/skills/co-develop/SKILL.md +196 -196
  116. package/skills/collab/SKILL.md +239 -239
  117. package/skills/convert/SKILL.md +140 -140
  118. package/skills/deploy/SKILL.md +541 -541
  119. package/skills/design/SKILL.md +211 -211
  120. package/skills/design/reference/ownership.md +16 -16
  121. package/skills/design/reference/rampa.md +92 -92
  122. package/skills/design/reference/studio-model.md +153 -153
  123. package/skills/edit/SKILL.md +98 -98
  124. package/skills/fixit/SKILL.md +165 -165
  125. package/skills/fs-mcp/SKILL.md +148 -148
  126. package/skills/handoff/SKILL.md +236 -200
  127. package/skills/help/SKILL.md +143 -143
  128. package/skills/housekeeping/SKILL.md +189 -189
  129. package/skills/lifeai-brochure-author/SKILL.md +340 -340
  130. package/skills/overnight/SKILL.md +251 -251
  131. package/skills/plan/SKILL.md +345 -345
  132. package/skills/preplan/SKILL.md +90 -90
  133. package/skills/prototype/SKILL.md +150 -150
  134. package/skills/rdc-brochurify/SKILL.md +245 -245
  135. package/skills/rdc-extract-verifier-rules/SKILL.md +191 -191
  136. package/skills/release/SKILL.md +140 -140
  137. package/skills/report/SKILL.md +100 -100
  138. package/skills/review/SKILL.md +152 -152
  139. package/skills/rpms-filemap/SKILL.cloud.md +111 -111
  140. package/skills/rpms-filemap/SKILL.md +111 -111
  141. package/skills/self-test/SKILL.md +132 -132
  142. package/skills/status/SKILL.md +99 -99
  143. package/skills/terminal-config/SKILL.md +62 -62
  144. package/skills/tests/MATRIX.md +54 -54
  145. package/skills/tests/README.md +47 -47
  146. package/skills/tests/rdc-brochure.test.json +34 -34
  147. package/skills/tests/rdc-build.test.json +36 -36
  148. package/skills/tests/rdc-channel-formatter.test.json +45 -45
  149. package/skills/tests/rdc-co-develop.test.json +29 -29
  150. package/skills/tests/rdc-collab.test.json +29 -29
  151. package/skills/tests/rdc-convert.test.json +35 -35
  152. package/skills/tests/rdc-deploy.test.json +30 -30
  153. package/skills/tests/rdc-design.test.json +27 -27
  154. package/skills/tests/rdc-edit.test.json +29 -29
  155. package/skills/tests/rdc-fixit.test.json +36 -36
  156. package/skills/tests/rdc-fs-mcp.test.json +36 -36
  157. package/skills/tests/rdc-handoff.test.json +28 -28
  158. package/skills/tests/rdc-help.test.json +29 -29
  159. package/skills/tests/rdc-housekeeping.test.json +31 -31
  160. package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
  161. package/skills/tests/rdc-overnight.test.json +37 -37
  162. package/skills/tests/rdc-plan.test.json +27 -27
  163. package/skills/tests/rdc-preplan.test.json +31 -31
  164. package/skills/tests/rdc-prototype.test.json +28 -28
  165. package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
  166. package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
  167. package/skills/tests/rdc-release.test.json +29 -29
  168. package/skills/tests/rdc-report.test.json +28 -28
  169. package/skills/tests/rdc-review.test.json +29 -29
  170. package/skills/tests/rdc-rpms-filemap.test.json +28 -28
  171. package/skills/tests/rdc-self-test.test.json +24 -24
  172. package/skills/tests/rdc-status.test.json +29 -29
  173. package/skills/tests/rdc-terminal-config.test.json +29 -29
  174. package/skills/tests/rdc-watch.test.json +24 -24
  175. package/skills/tests/rdc-workitems.test.json +27 -27
  176. package/skills/watch/SKILL.md +97 -97
  177. package/skills/workitems/SKILL.md +151 -151
  178. package/tests/acceptance.test.mjs +59 -59
  179. package/tests/channel-formatter.contract.test.mjs +251 -251
  180. package/tests/curl-surface.test.mjs +289 -289
  181. package/tests/harness-gates.test.mjs +325 -325
  182. package/tests/help-surface.test.mjs +61 -61
  183. package/tests/install-rdc-skills.test.mjs +49 -49
  184. package/tests/manifest-contract-fields.test.mjs +78 -78
  185. package/tests/mcp.test.mjs +271 -271
  186. package/tests/require-work-item-on-commit.test.mjs +162 -162
  187. package/tests/run-evidence-gate.test.mjs +82 -82
  188. package/tests/skill-test-matrix.test.mjs +66 -66
  189. package/tests/validate-skills.js +27 -27
  190. package/tests/work-item-exit-gate-l2.test.mjs +368 -368
  191. package/tests/work-item-exit-gate-l3.test.mjs +197 -197
@@ -1,241 +1,241 @@
1
- #!/usr/bin/env node
2
- /**
3
- * run-evidence-gate.mjs — Truth Gate 3.0 Layer 2, the FUSED evidence primitive.
4
- *
5
- * Nidus / D4 principle: "you cannot submit evidence without running the test."
6
- * This is ONE atomic call that (1) RUNS a verification command, (2) HASHES its
7
- * captured output server-side, and (3) records the verdict TOGETHER with the
8
- * hash and a timestamp. Evidence and execution are produced in the same call —
9
- * an agent can never hand the gate a hash for a run that never happened.
10
- *
11
- * The output of runEvidenceGate() is the only legitimate shape of a
12
- * machine-parseable `verification` artifact: it carries the exact command, the
13
- * exit code, an SHA-256 of stdout+stderr, and a pass/fail verdict the CALLER
14
- * did not author. The exit-gate's L2 verifier recognises this shape (and a few
15
- * other captured-artifact shapes) and rejects anything that is bare prose.
16
- *
17
- * Pure-ish: it shells out to run the command but has no DB or network side
18
- * effects, so it is unit-testable offline.
19
- */
20
- 'use strict';
21
-
22
- import { spawnSync } from 'node:child_process';
23
- import { createHash } from 'node:crypto';
24
-
25
- /** Stable SHA-256 of a string (server-side hash — the caller cannot forge it). */
26
- export function hashOutput(text) {
27
- return createHash('sha256').update(String(text == null ? '' : text), 'utf8').digest('hex');
28
- }
29
-
30
- /**
31
- * The discriminant marker stamped on every fused artifact. The exit-gate keys
32
- * on this to recognise a real run-and-attest result vs. an agent-typed string.
33
- */
34
- export const EVIDENCE_KIND = 'run-evidence-gate/v1';
35
-
36
- /**
37
- * Run a verification command and atomically produce a hashed, verdicted
38
- * evidence artifact. There is NO path to a verdict that skips the run: the
39
- * verdict is derived from the actual exit code of the actual spawn.
40
- *
41
- * @param {object} spec
42
- * @param {string} spec.command executable to run (e.g. "node", "npx")
43
- * @param {string[]} [spec.args] argv for the command
44
- * @param {string} [spec.cwd] working directory
45
- * @param {object} [spec.env] extra env
46
- * @param {number} [spec.timeoutMs] hard timeout (default 120s)
47
- * @param {string} [spec.label] human label for the check
48
- * @param {(spec)=>{status:number,stdout:string,stderr:string}} [runner]
49
- * injectable runner — defaults to spawnSync. Lets tests drive a fake
50
- * process WITHOUT removing the "must run" property (the runner is still
51
- * invoked exactly once and its result is what the verdict is built from).
52
- * @returns {object} a fused evidence artifact (see EVIDENCE_KIND).
53
- */
54
- export function runEvidenceGate(spec, runner) {
55
- if (!spec || typeof spec !== 'object' || typeof spec.command !== 'string' || !spec.command) {
56
- // Fail-closed: a malformed request can never produce a "pass".
57
- return {
58
- kind: EVIDENCE_KIND,
59
- ran: false,
60
- verdict: 'error',
61
- reason: 'invalid-spec: command is required',
62
- ts: new Date().toISOString(),
63
- };
64
- }
65
-
66
- const exec = typeof runner === 'function' ? runner : defaultRunner;
67
-
68
- let result;
69
- try {
70
- result = exec(spec);
71
- } catch (e) {
72
- // Spawn itself threw — fail-closed.
73
- return {
74
- kind: EVIDENCE_KIND,
75
- ran: false,
76
- verdict: 'error',
77
- reason: `runner-threw: ${e && e.message ? e.message : String(e)}`,
78
- command: renderCommand(spec),
79
- ts: new Date().toISOString(),
80
- };
81
- }
82
-
83
- // The runner MUST return a numeric status for the verdict to exist. No status
84
- // (e.g. the process could not be spawned) => no run => fail-closed.
85
- const status = result && typeof result.status === 'number' ? result.status : null;
86
- const stdout = result && result.stdout != null ? String(result.stdout) : '';
87
- const stderr = result && result.stderr != null ? String(result.stderr) : '';
88
-
89
- if (status === null) {
90
- return {
91
- kind: EVIDENCE_KIND,
92
- ran: false,
93
- verdict: 'error',
94
- reason: 'runner-produced-no-exit-status (process did not run)',
95
- command: renderCommand(spec),
96
- ts: new Date().toISOString(),
97
- };
98
- }
99
-
100
- const combined = `EXIT:${status}\n--STDOUT--\n${stdout}\n--STDERR--\n${stderr}`;
101
- return {
102
- kind: EVIDENCE_KIND,
103
- ran: true,
104
- label: spec.label || null,
105
- command: renderCommand(spec),
106
- exit_code: status,
107
- verdict: status === 0 ? 'pass' : 'fail',
108
- output_sha256: hashOutput(combined),
109
- output_bytes: Buffer.byteLength(combined, 'utf8'),
110
- ts: new Date().toISOString(),
111
- };
112
- }
113
-
114
- function renderCommand(spec) {
115
- return [spec.command, ...(Array.isArray(spec.args) ? spec.args : [])].join(' ');
116
- }
117
-
118
- function defaultRunner(spec) {
119
- const res = spawnSync(spec.command, Array.isArray(spec.args) ? spec.args : [], {
120
- cwd: spec.cwd || process.cwd(),
121
- env: { ...process.env, ...(spec.env || {}) },
122
- encoding: 'utf8',
123
- timeout: typeof spec.timeoutMs === 'number' ? spec.timeoutMs : 120000,
124
- maxBuffer: 16 * 1024 * 1024,
125
- });
126
- return { status: res.status, stdout: res.stdout, stderr: res.stderr };
127
- }
128
-
129
- /**
130
- * Is `v` a legitimate machine-parseable verification artifact (NOT prose)?
131
- *
132
- * Accepts, in order of strength:
133
- * 1. A fused run-evidence-gate artifact (object or its JSON string) — strongest.
134
- * 2. A captured-artifact OBJECT with a recognised machine shape:
135
- * - { exit_code: <number> } (tsc / test-runner exit)
136
- * - { http_status: <number> } (captured HTTP status)
137
- * - { rowcount: <number> } / row_count (SQL rowcount)
138
- * - { passed: <number>, ... } (test-runner JSON, e.g. vitest)
139
- * (a JSON string of any of these is also accepted)
140
- * REJECTS:
141
- * - bare strings ("HTTP 200", "107 nodes", "works", "done") — proxy/prose.
142
- * - objects with no machine field.
143
- */
144
- export function isMachineArtifact(v) {
145
- if (v == null) return false;
146
-
147
- // String input: only accepted if it parses to a recognised JSON artifact.
148
- if (typeof v === 'string') {
149
- const s = v.trim();
150
- if (!(s.startsWith('{') || s.startsWith('['))) return false; // bare prose
151
- let parsed;
152
- try { parsed = JSON.parse(s); } catch { return false; }
153
- return isMachineArtifact(parsed);
154
- }
155
-
156
- if (typeof v !== 'object') return false;
157
-
158
- // 1. Fused artifact.
159
- if (v.kind === EVIDENCE_KIND && v.ran === true && typeof v.output_sha256 === 'string') {
160
- return true;
161
- }
162
-
163
- // 2. Recognised captured-artifact object shapes.
164
- if (typeof v.exit_code === 'number') return true;
165
- if (typeof v.http_status === 'number' || typeof v.httpStatus === 'number') return true;
166
- if (typeof v.status_code === 'number' || typeof v.statusCode === 'number') return true;
167
- if (typeof v.rowcount === 'number' || typeof v.row_count === 'number' || typeof v.rowCount === 'number') return true;
168
- if (typeof v.passed === 'number' && (typeof v.failed === 'number' || typeof v.total === 'number')) return true;
169
- if (typeof v.tsc_errors === 'number' || typeof v.tscErrors === 'number') return true;
170
-
171
- return false;
172
- }
173
-
174
- /**
175
- * Does `v` represent a verification whose OUTCOME is a PASS — not merely that it
176
- * ran? This is the outcome gate that complements isMachineArtifact (the shape
177
- * gate). A failing run (exit_code:1), an error HTTP status (500), a fused
178
- * artifact with verdict:'fail', or a test artifact with failures must NOT be
179
- * accepted as evidence of a passing verification.
180
- *
181
- * Returns true ONLY when the artifact is a recognised machine shape AND its
182
- * outcome reads as a pass. Anything ambiguous or non-passing returns false.
183
- *
184
- * Pass rules (mirrors isMachineArtifact's accepted shapes):
185
- * - fused run-evidence-gate/v1 → ran===true && verdict==='pass'
186
- * - { exit_code } → exit_code === 0
187
- * - { http_status|status_code }→ 200 <= s <= 399
188
- * - { passed, failed } → failed === 0
189
- * - { passed, total } → passed === total
190
- * - { tsc_errors } → tsc_errors === 0
191
- * - { rowcount } → a captured rowcount is presence-only evidence;
192
- * any numeric rowcount counts as a pass.
193
- */
194
- export function isPassingArtifact(v) {
195
- if (v == null) return false;
196
-
197
- // String input: only accepted if it parses to a recognised JSON artifact.
198
- if (typeof v === 'string') {
199
- const s = v.trim();
200
- if (!(s.startsWith('{') || s.startsWith('['))) return false; // bare prose
201
- let parsed;
202
- try { parsed = JSON.parse(s); } catch { return false; }
203
- return isPassingArtifact(parsed);
204
- }
205
-
206
- if (typeof v !== 'object') return false;
207
-
208
- // Must be a recognised machine shape first.
209
- if (!isMachineArtifact(v)) return false;
210
-
211
- // 1. Fused artifact — the verdict is authoritative.
212
- if (v.kind === EVIDENCE_KIND) {
213
- return v.ran === true && v.verdict === 'pass';
214
- }
215
-
216
- // 2. Captured-artifact shapes — read the outcome, not just the presence.
217
- // A tsc/test error count is checked even alongside another field.
218
- if (typeof v.tsc_errors === 'number') return v.tsc_errors === 0;
219
- if (typeof v.tscErrors === 'number') return v.tscErrors === 0;
220
-
221
- if (typeof v.exit_code === 'number') return v.exit_code === 0;
222
-
223
- if (typeof v.http_status === 'number') return v.http_status >= 200 && v.http_status <= 399;
224
- if (typeof v.httpStatus === 'number') return v.httpStatus >= 200 && v.httpStatus <= 399;
225
- if (typeof v.status_code === 'number') return v.status_code >= 200 && v.status_code <= 399;
226
- if (typeof v.statusCode === 'number') return v.statusCode >= 200 && v.statusCode <= 399;
227
-
228
- if (typeof v.passed === 'number') {
229
- if (typeof v.failed === 'number') return v.failed === 0;
230
- if (typeof v.total === 'number') return v.passed === v.total;
231
- }
232
-
233
- if (typeof v.rowcount === 'number') return true;
234
- if (typeof v.row_count === 'number') return true;
235
- if (typeof v.rowCount === 'number') return true;
236
-
237
- // Recognised shape but no readable outcome → not a pass (fail-closed).
238
- return false;
239
- }
240
-
241
- export default { runEvidenceGate, hashOutput, isMachineArtifact, isPassingArtifact, EVIDENCE_KIND };
1
+ #!/usr/bin/env node
2
+ /**
3
+ * run-evidence-gate.mjs — Truth Gate 3.0 Layer 2, the FUSED evidence primitive.
4
+ *
5
+ * Nidus / D4 principle: "you cannot submit evidence without running the test."
6
+ * This is ONE atomic call that (1) RUNS a verification command, (2) HASHES its
7
+ * captured output server-side, and (3) records the verdict TOGETHER with the
8
+ * hash and a timestamp. Evidence and execution are produced in the same call —
9
+ * an agent can never hand the gate a hash for a run that never happened.
10
+ *
11
+ * The output of runEvidenceGate() is the only legitimate shape of a
12
+ * machine-parseable `verification` artifact: it carries the exact command, the
13
+ * exit code, an SHA-256 of stdout+stderr, and a pass/fail verdict the CALLER
14
+ * did not author. The exit-gate's L2 verifier recognises this shape (and a few
15
+ * other captured-artifact shapes) and rejects anything that is bare prose.
16
+ *
17
+ * Pure-ish: it shells out to run the command but has no DB or network side
18
+ * effects, so it is unit-testable offline.
19
+ */
20
+ 'use strict';
21
+
22
+ import { spawnSync } from 'node:child_process';
23
+ import { createHash } from 'node:crypto';
24
+
25
+ /** Stable SHA-256 of a string (server-side hash — the caller cannot forge it). */
26
+ export function hashOutput(text) {
27
+ return createHash('sha256').update(String(text == null ? '' : text), 'utf8').digest('hex');
28
+ }
29
+
30
+ /**
31
+ * The discriminant marker stamped on every fused artifact. The exit-gate keys
32
+ * on this to recognise a real run-and-attest result vs. an agent-typed string.
33
+ */
34
+ export const EVIDENCE_KIND = 'run-evidence-gate/v1';
35
+
36
+ /**
37
+ * Run a verification command and atomically produce a hashed, verdicted
38
+ * evidence artifact. There is NO path to a verdict that skips the run: the
39
+ * verdict is derived from the actual exit code of the actual spawn.
40
+ *
41
+ * @param {object} spec
42
+ * @param {string} spec.command executable to run (e.g. "node", "npx")
43
+ * @param {string[]} [spec.args] argv for the command
44
+ * @param {string} [spec.cwd] working directory
45
+ * @param {object} [spec.env] extra env
46
+ * @param {number} [spec.timeoutMs] hard timeout (default 120s)
47
+ * @param {string} [spec.label] human label for the check
48
+ * @param {(spec)=>{status:number,stdout:string,stderr:string}} [runner]
49
+ * injectable runner — defaults to spawnSync. Lets tests drive a fake
50
+ * process WITHOUT removing the "must run" property (the runner is still
51
+ * invoked exactly once and its result is what the verdict is built from).
52
+ * @returns {object} a fused evidence artifact (see EVIDENCE_KIND).
53
+ */
54
+ export function runEvidenceGate(spec, runner) {
55
+ if (!spec || typeof spec !== 'object' || typeof spec.command !== 'string' || !spec.command) {
56
+ // Fail-closed: a malformed request can never produce a "pass".
57
+ return {
58
+ kind: EVIDENCE_KIND,
59
+ ran: false,
60
+ verdict: 'error',
61
+ reason: 'invalid-spec: command is required',
62
+ ts: new Date().toISOString(),
63
+ };
64
+ }
65
+
66
+ const exec = typeof runner === 'function' ? runner : defaultRunner;
67
+
68
+ let result;
69
+ try {
70
+ result = exec(spec);
71
+ } catch (e) {
72
+ // Spawn itself threw — fail-closed.
73
+ return {
74
+ kind: EVIDENCE_KIND,
75
+ ran: false,
76
+ verdict: 'error',
77
+ reason: `runner-threw: ${e && e.message ? e.message : String(e)}`,
78
+ command: renderCommand(spec),
79
+ ts: new Date().toISOString(),
80
+ };
81
+ }
82
+
83
+ // The runner MUST return a numeric status for the verdict to exist. No status
84
+ // (e.g. the process could not be spawned) => no run => fail-closed.
85
+ const status = result && typeof result.status === 'number' ? result.status : null;
86
+ const stdout = result && result.stdout != null ? String(result.stdout) : '';
87
+ const stderr = result && result.stderr != null ? String(result.stderr) : '';
88
+
89
+ if (status === null) {
90
+ return {
91
+ kind: EVIDENCE_KIND,
92
+ ran: false,
93
+ verdict: 'error',
94
+ reason: 'runner-produced-no-exit-status (process did not run)',
95
+ command: renderCommand(spec),
96
+ ts: new Date().toISOString(),
97
+ };
98
+ }
99
+
100
+ const combined = `EXIT:${status}\n--STDOUT--\n${stdout}\n--STDERR--\n${stderr}`;
101
+ return {
102
+ kind: EVIDENCE_KIND,
103
+ ran: true,
104
+ label: spec.label || null,
105
+ command: renderCommand(spec),
106
+ exit_code: status,
107
+ verdict: status === 0 ? 'pass' : 'fail',
108
+ output_sha256: hashOutput(combined),
109
+ output_bytes: Buffer.byteLength(combined, 'utf8'),
110
+ ts: new Date().toISOString(),
111
+ };
112
+ }
113
+
114
+ function renderCommand(spec) {
115
+ return [spec.command, ...(Array.isArray(spec.args) ? spec.args : [])].join(' ');
116
+ }
117
+
118
+ function defaultRunner(spec) {
119
+ const res = spawnSync(spec.command, Array.isArray(spec.args) ? spec.args : [], {
120
+ cwd: spec.cwd || process.cwd(),
121
+ env: { ...process.env, ...(spec.env || {}) },
122
+ encoding: 'utf8',
123
+ timeout: typeof spec.timeoutMs === 'number' ? spec.timeoutMs : 120000,
124
+ maxBuffer: 16 * 1024 * 1024,
125
+ });
126
+ return { status: res.status, stdout: res.stdout, stderr: res.stderr };
127
+ }
128
+
129
+ /**
130
+ * Is `v` a legitimate machine-parseable verification artifact (NOT prose)?
131
+ *
132
+ * Accepts, in order of strength:
133
+ * 1. A fused run-evidence-gate artifact (object or its JSON string) — strongest.
134
+ * 2. A captured-artifact OBJECT with a recognised machine shape:
135
+ * - { exit_code: <number> } (tsc / test-runner exit)
136
+ * - { http_status: <number> } (captured HTTP status)
137
+ * - { rowcount: <number> } / row_count (SQL rowcount)
138
+ * - { passed: <number>, ... } (test-runner JSON, e.g. vitest)
139
+ * (a JSON string of any of these is also accepted)
140
+ * REJECTS:
141
+ * - bare strings ("HTTP 200", "107 nodes", "works", "done") — proxy/prose.
142
+ * - objects with no machine field.
143
+ */
144
+ export function isMachineArtifact(v) {
145
+ if (v == null) return false;
146
+
147
+ // String input: only accepted if it parses to a recognised JSON artifact.
148
+ if (typeof v === 'string') {
149
+ const s = v.trim();
150
+ if (!(s.startsWith('{') || s.startsWith('['))) return false; // bare prose
151
+ let parsed;
152
+ try { parsed = JSON.parse(s); } catch { return false; }
153
+ return isMachineArtifact(parsed);
154
+ }
155
+
156
+ if (typeof v !== 'object') return false;
157
+
158
+ // 1. Fused artifact.
159
+ if (v.kind === EVIDENCE_KIND && v.ran === true && typeof v.output_sha256 === 'string') {
160
+ return true;
161
+ }
162
+
163
+ // 2. Recognised captured-artifact object shapes.
164
+ if (typeof v.exit_code === 'number') return true;
165
+ if (typeof v.http_status === 'number' || typeof v.httpStatus === 'number') return true;
166
+ if (typeof v.status_code === 'number' || typeof v.statusCode === 'number') return true;
167
+ if (typeof v.rowcount === 'number' || typeof v.row_count === 'number' || typeof v.rowCount === 'number') return true;
168
+ if (typeof v.passed === 'number' && (typeof v.failed === 'number' || typeof v.total === 'number')) return true;
169
+ if (typeof v.tsc_errors === 'number' || typeof v.tscErrors === 'number') return true;
170
+
171
+ return false;
172
+ }
173
+
174
+ /**
175
+ * Does `v` represent a verification whose OUTCOME is a PASS — not merely that it
176
+ * ran? This is the outcome gate that complements isMachineArtifact (the shape
177
+ * gate). A failing run (exit_code:1), an error HTTP status (500), a fused
178
+ * artifact with verdict:'fail', or a test artifact with failures must NOT be
179
+ * accepted as evidence of a passing verification.
180
+ *
181
+ * Returns true ONLY when the artifact is a recognised machine shape AND its
182
+ * outcome reads as a pass. Anything ambiguous or non-passing returns false.
183
+ *
184
+ * Pass rules (mirrors isMachineArtifact's accepted shapes):
185
+ * - fused run-evidence-gate/v1 → ran===true && verdict==='pass'
186
+ * - { exit_code } → exit_code === 0
187
+ * - { http_status|status_code }→ 200 <= s <= 399
188
+ * - { passed, failed } → failed === 0
189
+ * - { passed, total } → passed === total
190
+ * - { tsc_errors } → tsc_errors === 0
191
+ * - { rowcount } → a captured rowcount is presence-only evidence;
192
+ * any numeric rowcount counts as a pass.
193
+ */
194
+ export function isPassingArtifact(v) {
195
+ if (v == null) return false;
196
+
197
+ // String input: only accepted if it parses to a recognised JSON artifact.
198
+ if (typeof v === 'string') {
199
+ const s = v.trim();
200
+ if (!(s.startsWith('{') || s.startsWith('['))) return false; // bare prose
201
+ let parsed;
202
+ try { parsed = JSON.parse(s); } catch { return false; }
203
+ return isPassingArtifact(parsed);
204
+ }
205
+
206
+ if (typeof v !== 'object') return false;
207
+
208
+ // Must be a recognised machine shape first.
209
+ if (!isMachineArtifact(v)) return false;
210
+
211
+ // 1. Fused artifact — the verdict is authoritative.
212
+ if (v.kind === EVIDENCE_KIND) {
213
+ return v.ran === true && v.verdict === 'pass';
214
+ }
215
+
216
+ // 2. Captured-artifact shapes — read the outcome, not just the presence.
217
+ // A tsc/test error count is checked even alongside another field.
218
+ if (typeof v.tsc_errors === 'number') return v.tsc_errors === 0;
219
+ if (typeof v.tscErrors === 'number') return v.tscErrors === 0;
220
+
221
+ if (typeof v.exit_code === 'number') return v.exit_code === 0;
222
+
223
+ if (typeof v.http_status === 'number') return v.http_status >= 200 && v.http_status <= 399;
224
+ if (typeof v.httpStatus === 'number') return v.httpStatus >= 200 && v.httpStatus <= 399;
225
+ if (typeof v.status_code === 'number') return v.status_code >= 200 && v.status_code <= 399;
226
+ if (typeof v.statusCode === 'number') return v.statusCode >= 200 && v.statusCode <= 399;
227
+
228
+ if (typeof v.passed === 'number') {
229
+ if (typeof v.failed === 'number') return v.failed === 0;
230
+ if (typeof v.total === 'number') return v.passed === v.total;
231
+ }
232
+
233
+ if (typeof v.rowcount === 'number') return true;
234
+ if (typeof v.row_count === 'number') return true;
235
+ if (typeof v.rowCount === 'number') return true;
236
+
237
+ // Recognised shape but no readable outcome → not a pass (fail-closed).
238
+ return false;
239
+ }
240
+
241
+ export default { runEvidenceGate, hashOutput, isMachineArtifact, isPassingArtifact, EVIDENCE_KIND };