@lifeaitools/rdc-skills 0.24.38 → 0.24.41

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 (196) 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 +376 -375
  10. package/README.sandbox.md +3 -3
  11. package/RELEASE.md +42 -0
  12. package/assets/watcher/viewer.html +164 -164
  13. package/bin/rdc-skills-mcp.mjs +316 -316
  14. package/commands/build.md +183 -183
  15. package/commands/collab.md +180 -180
  16. package/commands/deploy.md +152 -152
  17. package/commands/design.md +31 -31
  18. package/commands/edit.md +28 -28
  19. package/commands/fixit.md +124 -124
  20. package/commands/handoff.md +173 -173
  21. package/commands/help.md +95 -95
  22. package/commands/overnight.md +220 -220
  23. package/commands/plan.md +158 -158
  24. package/commands/preplan.md +131 -131
  25. package/commands/prototype.md +145 -145
  26. package/commands/release.md +49 -49
  27. package/commands/report.md +99 -99
  28. package/commands/review.md +120 -120
  29. package/commands/self-test.md +113 -113
  30. package/commands/status.md +86 -86
  31. package/commands/watch.md +98 -98
  32. package/commands/workitems.md +137 -137
  33. package/git-sha.json +1 -1
  34. package/guides/agent-bootstrap.md +295 -295
  35. package/guides/agents/backend.md +104 -104
  36. package/guides/agents/content.md +94 -94
  37. package/guides/agents/cs2.md +56 -56
  38. package/guides/agents/data.md +87 -87
  39. package/guides/agents/design.md +77 -77
  40. package/guides/agents/frontend.md +92 -92
  41. package/guides/agents/infrastructure.md +81 -81
  42. package/guides/agents/setup.md +281 -281
  43. package/guides/agents/verify.md +151 -151
  44. package/guides/agents/viz.md +106 -106
  45. package/guides/backend.md +146 -146
  46. package/guides/content.md +147 -147
  47. package/guides/cs2.md +190 -190
  48. package/guides/data.md +123 -123
  49. package/guides/design.md +116 -116
  50. package/guides/engineering-behavior.md +43 -43
  51. package/guides/escalation-protocol.md +125 -125
  52. package/guides/frontend.md +151 -151
  53. package/guides/history-md-spec.md +297 -297
  54. package/guides/infrastructure.md +179 -179
  55. package/guides/lessons-learned-spec.md +151 -145
  56. package/guides/output-contract.md +108 -108
  57. package/guides/publish-md-spec.md +289 -289
  58. package/guides/rdc-skills-startup.md +30 -30
  59. package/guides/verify.md +11 -11
  60. package/hooks/check-cwd.js +31 -31
  61. package/hooks/check-rdc-environment.js +164 -164
  62. package/hooks/check-services.js +6 -6
  63. package/hooks/check-stale-work-items.js +19 -19
  64. package/hooks/foreground-process-gate.js +128 -128
  65. package/hooks/gate-watchdog-selfcheck.js +257 -257
  66. package/hooks/hook-logger.js +25 -25
  67. package/hooks/lib/run-evidence-gate.mjs +241 -241
  68. package/hooks/no-stop-open-epics.js +127 -127
  69. package/hooks/post-tool-batch-gate.js +203 -203
  70. package/hooks/post-work-check.js +21 -21
  71. package/hooks/postcompact-log.js +13 -13
  72. package/hooks/precompact-log.js +13 -13
  73. package/hooks/rate-limit-retry.js +46 -46
  74. package/hooks/rdc-invocation-marker.js +157 -157
  75. package/hooks/rdc-output-contract-gate.js +94 -94
  76. package/hooks/require-work-item-on-commit.js +294 -294
  77. package/hooks/restart-brief.js +19 -19
  78. package/hooks/run-hidden-hook.ps1 +47 -47
  79. package/hooks/task-completed-gate.js +274 -274
  80. package/hooks/work-item-exit-gate.js +944 -944
  81. package/lib/catalog.mjs +236 -236
  82. package/lib/cloud-rewrite.mjs +155 -155
  83. package/package.json +57 -56
  84. package/rules/work-items-rpc.md +520 -520
  85. package/scaffold/templates/HISTORY.md.template +39 -39
  86. package/scaffold/templates/PUBLISH.md.template +21 -21
  87. package/scaffold/templates/brochure-studio-default.html +70 -70
  88. package/scripts/acceptance.mjs +502 -502
  89. package/scripts/fixtures/guides/bad-guide.md +15 -15
  90. package/scripts/fixtures/guides-clean/good-guide.md +16 -16
  91. package/scripts/install-rdc-skills.js +1289 -1289
  92. package/scripts/install.ps1 +202 -202
  93. package/scripts/install.sh +132 -132
  94. package/scripts/lib/assertions.mjs +287 -287
  95. package/scripts/lib/manifest-schema.mjs +754 -754
  96. package/scripts/lib/runner.mjs +465 -465
  97. package/scripts/lib/sandbox.mjs +435 -435
  98. package/scripts/prepack.mjs +32 -32
  99. package/scripts/rdc-brochure.mjs +482 -464
  100. package/scripts/rdc-design-cli.mjs +134 -134
  101. package/scripts/rebuild-mcp.mjs +107 -107
  102. package/scripts/self-test.mjs +1460 -1460
  103. package/scripts/stamp-git-sha.mjs +29 -29
  104. package/scripts/test-guide-validator.mjs +196 -196
  105. package/scripts/test-rdc-hooks.mjs +145 -145
  106. package/scripts/uninstall.ps1 +77 -77
  107. package/scripts/uninstall.sh +69 -69
  108. package/scripts/update.ps1 +43 -43
  109. package/scripts/update.sh +43 -43
  110. package/scripts/validate-place-histories.js +461 -461
  111. package/scripts/validate-publish-manifests.js +424 -424
  112. package/scripts/watch-init.mjs +100 -100
  113. package/skills/brochure/SKILL.md +107 -107
  114. package/skills/build/SKILL.md +563 -563
  115. package/skills/channel-formatter/SKILL.md +533 -533
  116. package/skills/co-develop/SKILL.md +196 -196
  117. package/skills/collab/SKILL.md +239 -239
  118. package/skills/convert/SKILL.md +140 -140
  119. package/skills/deploy/SKILL.md +541 -541
  120. package/skills/design/SKILL.md +211 -211
  121. package/skills/design/reference/ownership.md +16 -16
  122. package/skills/design/reference/rampa.md +92 -92
  123. package/skills/design/reference/studio-model.md +153 -153
  124. package/skills/edit/SKILL.md +98 -98
  125. package/skills/fixit/SKILL.md +165 -165
  126. package/skills/fs-mcp/SKILL.md +148 -148
  127. package/skills/handoff/SKILL.md +236 -200
  128. package/skills/help/SKILL.md +143 -143
  129. package/skills/housekeeping/SKILL.md +219 -160
  130. package/skills/lifeai-brochure-author/SKILL.md +340 -340
  131. package/skills/overnight/SKILL.md +251 -251
  132. package/skills/plan/SKILL.md +345 -345
  133. package/skills/preplan/SKILL.md +90 -90
  134. package/skills/prototype/SKILL.md +150 -150
  135. package/skills/rdc-brochurify/SKILL.md +245 -245
  136. package/skills/rdc-extract-verifier-rules/SKILL.md +191 -191
  137. package/skills/release/SKILL.md +140 -140
  138. package/skills/report/SKILL.md +100 -100
  139. package/skills/review/SKILL.md +152 -152
  140. package/skills/rpms-filemap/SKILL.cloud.md +111 -111
  141. package/skills/rpms-filemap/SKILL.md +111 -111
  142. package/skills/self-test/SKILL.md +132 -132
  143. package/skills/status/SKILL.md +99 -99
  144. package/skills/terminal-config/SKILL.md +62 -62
  145. package/skills/tests/MATRIX.md +54 -54
  146. package/skills/tests/README.md +47 -47
  147. package/skills/tests/rdc-brochure.test.json +34 -34
  148. package/skills/tests/rdc-build.test.json +36 -36
  149. package/skills/tests/rdc-channel-formatter.test.json +45 -45
  150. package/skills/tests/rdc-co-develop.test.json +29 -29
  151. package/skills/tests/rdc-collab.test.json +29 -29
  152. package/skills/tests/rdc-convert.test.json +35 -35
  153. package/skills/tests/rdc-deploy.test.json +30 -30
  154. package/skills/tests/rdc-design.test.json +27 -27
  155. package/skills/tests/rdc-edit.test.json +29 -29
  156. package/skills/tests/rdc-fixit.test.json +36 -36
  157. package/skills/tests/rdc-fs-mcp.test.json +36 -36
  158. package/skills/tests/rdc-handoff.test.json +28 -28
  159. package/skills/tests/rdc-help.test.json +29 -29
  160. package/skills/tests/rdc-housekeeping.test.json +32 -28
  161. package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
  162. package/skills/tests/rdc-overnight.test.json +37 -37
  163. package/skills/tests/rdc-plan.test.json +27 -27
  164. package/skills/tests/rdc-preplan.test.json +31 -31
  165. package/skills/tests/rdc-prototype.test.json +28 -28
  166. package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
  167. package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
  168. package/skills/tests/rdc-release.test.json +29 -29
  169. package/skills/tests/rdc-report.test.json +28 -28
  170. package/skills/tests/rdc-review.test.json +29 -29
  171. package/skills/tests/rdc-rpms-filemap.test.json +28 -28
  172. package/skills/tests/rdc-self-test.test.json +24 -24
  173. package/skills/tests/rdc-status.test.json +29 -29
  174. package/skills/tests/rdc-terminal-config.test.json +29 -29
  175. package/skills/tests/rdc-watch.test.json +24 -24
  176. package/skills/tests/rdc-workitems.test.json +27 -27
  177. package/skills/watch/SKILL.md +97 -97
  178. package/skills/workitems/SKILL.md +151 -151
  179. package/tests/acceptance.test.mjs +59 -59
  180. package/tests/channel-formatter.contract.test.mjs +251 -251
  181. package/tests/curl-surface.test.mjs +289 -289
  182. package/tests/harness-gates.test.mjs +325 -325
  183. package/tests/help-surface.test.mjs +61 -61
  184. package/tests/housekeeping-lessons-triage.test.mjs +49 -0
  185. package/tests/install-rdc-skills.test.mjs +49 -49
  186. package/tests/lessons-pipeline-contract.test.mjs +26 -0
  187. package/tests/manifest-contract-fields.test.mjs +78 -78
  188. package/tests/mcp.test.mjs +271 -271
  189. package/tests/rdc-brochure.test.mjs +125 -0
  190. package/tests/release-contract.test.mjs +16 -0
  191. package/tests/require-work-item-on-commit.test.mjs +162 -162
  192. package/tests/run-evidence-gate.test.mjs +82 -82
  193. package/tests/skill-test-matrix.test.mjs +66 -66
  194. package/tests/validate-skills.js +27 -27
  195. package/tests/work-item-exit-gate-l2.test.mjs +368 -368
  196. 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 };