@veraxhq/verax 0.3.0 → 0.4.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 (191) hide show
  1. package/README.md +28 -20
  2. package/bin/verax.js +11 -18
  3. package/package.json +28 -7
  4. package/src/cli/commands/baseline.js +1 -2
  5. package/src/cli/commands/default.js +72 -81
  6. package/src/cli/commands/doctor.js +29 -0
  7. package/src/cli/commands/ga.js +3 -0
  8. package/src/cli/commands/gates.js +1 -1
  9. package/src/cli/commands/inspect.js +6 -133
  10. package/src/cli/commands/release-check.js +2 -0
  11. package/src/cli/commands/run.js +74 -246
  12. package/src/cli/commands/security-check.js +2 -1
  13. package/src/cli/commands/truth.js +0 -1
  14. package/src/cli/entry.js +82 -309
  15. package/src/cli/util/angular-component-extractor.js +2 -2
  16. package/src/cli/util/angular-navigation-detector.js +2 -2
  17. package/src/cli/util/ast-interactive-detector.js +4 -6
  18. package/src/cli/util/ast-network-detector.js +3 -3
  19. package/src/cli/util/ast-promise-extractor.js +581 -0
  20. package/src/cli/util/ast-usestate-detector.js +3 -3
  21. package/src/cli/util/atomic-write.js +12 -1
  22. package/src/cli/util/console-reporter.js +72 -0
  23. package/src/cli/util/detection-engine.js +105 -41
  24. package/src/cli/util/determinism-runner.js +2 -1
  25. package/src/cli/util/determinism-writer.js +1 -1
  26. package/src/cli/util/digest-engine.js +359 -0
  27. package/src/cli/util/dom-diff.js +226 -0
  28. package/src/cli/util/env-url.js +0 -4
  29. package/src/cli/util/evidence-engine.js +287 -0
  30. package/src/cli/util/expectation-extractor.js +217 -367
  31. package/src/cli/util/findings-writer.js +19 -126
  32. package/src/cli/util/framework-detector.js +572 -0
  33. package/src/cli/util/idgen.js +1 -1
  34. package/src/cli/util/interaction-planner.js +529 -0
  35. package/src/cli/util/learn-writer.js +2 -2
  36. package/src/cli/util/ledger-writer.js +110 -0
  37. package/src/cli/util/monorepo-resolver.js +162 -0
  38. package/src/cli/util/observation-engine.js +127 -278
  39. package/src/cli/util/observe-writer.js +2 -2
  40. package/src/cli/util/paths.js +12 -3
  41. package/src/cli/util/project-discovery.js +284 -3
  42. package/src/cli/util/project-writer.js +2 -2
  43. package/src/cli/util/run-id.js +23 -27
  44. package/src/cli/util/run-result.js +778 -0
  45. package/src/cli/util/selector-resolver.js +235 -0
  46. package/src/cli/util/summary-writer.js +2 -1
  47. package/src/cli/util/svelte-navigation-detector.js +3 -3
  48. package/src/cli/util/svelte-sfc-extractor.js +0 -1
  49. package/src/cli/util/svelte-state-detector.js +1 -2
  50. package/src/cli/util/trust-activation-integration.js +496 -0
  51. package/src/cli/util/trust-activation-wrapper.js +85 -0
  52. package/src/cli/util/trust-integration-hooks.js +164 -0
  53. package/src/cli/util/types.js +153 -0
  54. package/src/cli/util/url-validation.js +40 -0
  55. package/src/cli/util/vue-navigation-detector.js +4 -3
  56. package/src/cli/util/vue-sfc-extractor.js +1 -2
  57. package/src/cli/util/vue-state-detector.js +1 -1
  58. package/src/types/fs-augment.d.ts +23 -0
  59. package/src/types/global.d.ts +137 -0
  60. package/src/types/internal-types.d.ts +35 -0
  61. package/src/verax/cli/finding-explainer.js +3 -56
  62. package/src/verax/cli/init.js +4 -18
  63. package/src/verax/core/action-classifier.js +4 -3
  64. package/src/verax/core/artifacts/registry.js +0 -15
  65. package/src/verax/core/artifacts/verifier.js +18 -8
  66. package/src/verax/core/baseline/baseline.snapshot.js +2 -0
  67. package/src/verax/core/capabilities/gates.js +7 -1
  68. package/src/verax/core/confidence/confidence-compute.js +14 -7
  69. package/src/verax/core/confidence/confidence.loader.js +1 -0
  70. package/src/verax/core/confidence-engine-refactor.js +8 -3
  71. package/src/verax/core/confidence-engine.js +162 -23
  72. package/src/verax/core/contracts/types.js +1 -0
  73. package/src/verax/core/contracts/validators.js +79 -4
  74. package/src/verax/core/decision-snapshot.js +3 -30
  75. package/src/verax/core/decisions/decision.trace.js +2 -0
  76. package/src/verax/core/determinism/contract-writer.js +2 -2
  77. package/src/verax/core/determinism/contract.js +1 -1
  78. package/src/verax/core/determinism/diff.js +42 -1
  79. package/src/verax/core/determinism/engine.js +7 -6
  80. package/src/verax/core/determinism/finding-identity.js +3 -2
  81. package/src/verax/core/determinism/normalize.js +32 -4
  82. package/src/verax/core/determinism/report-writer.js +1 -0
  83. package/src/verax/core/determinism/run-fingerprint.js +7 -2
  84. package/src/verax/core/dynamic-route-intelligence.js +8 -7
  85. package/src/verax/core/evidence/evidence-capture-service.js +1 -0
  86. package/src/verax/core/evidence/evidence-intent-ledger.js +2 -1
  87. package/src/verax/core/evidence-builder.js +2 -2
  88. package/src/verax/core/execution-mode-context.js +1 -1
  89. package/src/verax/core/execution-mode-detector.js +5 -3
  90. package/src/verax/core/failures/exit-codes.js +39 -37
  91. package/src/verax/core/failures/failure-summary.js +1 -1
  92. package/src/verax/core/failures/failure.factory.js +3 -3
  93. package/src/verax/core/failures/failure.ledger.js +3 -2
  94. package/src/verax/core/ga/ga.artifact.js +1 -1
  95. package/src/verax/core/ga/ga.contract.js +3 -2
  96. package/src/verax/core/ga/ga.enforcer.js +1 -0
  97. package/src/verax/core/guardrails/policy.loader.js +1 -0
  98. package/src/verax/core/guardrails/truth-reconciliation.js +1 -1
  99. package/src/verax/core/guardrails-engine.js +2 -2
  100. package/src/verax/core/incremental-store.js +1 -0
  101. package/src/verax/core/integrity/budget.js +138 -0
  102. package/src/verax/core/integrity/determinism.js +342 -0
  103. package/src/verax/core/integrity/integrity.js +208 -0
  104. package/src/verax/core/integrity/poisoning.js +108 -0
  105. package/src/verax/core/integrity/transaction.js +140 -0
  106. package/src/verax/core/observe/run-timeline.js +2 -0
  107. package/src/verax/core/perf/perf.report.js +2 -0
  108. package/src/verax/core/pipeline-tracker.js +5 -0
  109. package/src/verax/core/release/provenance.builder.js +73 -214
  110. package/src/verax/core/release/release.enforcer.js +14 -9
  111. package/src/verax/core/release/reproducibility.check.js +1 -0
  112. package/src/verax/core/release/sbom.builder.js +32 -23
  113. package/src/verax/core/replay-validator.js +2 -0
  114. package/src/verax/core/replay.js +4 -0
  115. package/src/verax/core/report/cross-index.js +6 -3
  116. package/src/verax/core/report/human-summary.js +141 -1
  117. package/src/verax/core/route-intelligence.js +4 -3
  118. package/src/verax/core/run-id.js +6 -3
  119. package/src/verax/core/run-manifest.js +4 -3
  120. package/src/verax/core/security/secrets.scan.js +10 -7
  121. package/src/verax/core/security/security.enforcer.js +4 -0
  122. package/src/verax/core/security/supplychain.policy.js +9 -1
  123. package/src/verax/core/security/vuln.scan.js +2 -2
  124. package/src/verax/core/truth/truth.certificate.js +3 -1
  125. package/src/verax/core/ui-feedback-intelligence.js +12 -46
  126. package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
  127. package/src/verax/detect/confidence-engine.js +100 -660
  128. package/src/verax/detect/confidence-helper.js +1 -0
  129. package/src/verax/detect/detection-engine.js +1 -18
  130. package/src/verax/detect/dynamic-route-findings.js +17 -14
  131. package/src/verax/detect/expectation-chain-detector.js +1 -1
  132. package/src/verax/detect/expectation-model.js +3 -5
  133. package/src/verax/detect/failure-cause-inference.js +293 -0
  134. package/src/verax/detect/findings-writer.js +126 -166
  135. package/src/verax/detect/flow-detector.js +2 -2
  136. package/src/verax/detect/form-silent-failure.js +98 -0
  137. package/src/verax/detect/index.js +51 -234
  138. package/src/verax/detect/invariants-enforcer.js +147 -0
  139. package/src/verax/detect/journey-stall-detector.js +4 -4
  140. package/src/verax/detect/navigation-silent-failure.js +82 -0
  141. package/src/verax/detect/problem-aggregator.js +361 -0
  142. package/src/verax/detect/route-findings.js +7 -6
  143. package/src/verax/detect/summary-writer.js +477 -0
  144. package/src/verax/detect/test-failure-cause-inference.js +314 -0
  145. package/src/verax/detect/ui-feedback-findings.js +18 -18
  146. package/src/verax/detect/verdict-engine.js +3 -57
  147. package/src/verax/detect/view-switch-correlator.js +2 -2
  148. package/src/verax/flow/flow-engine.js +2 -1
  149. package/src/verax/flow/flow-spec.js +0 -6
  150. package/src/verax/index.js +48 -412
  151. package/src/verax/intel/ts-program.js +1 -0
  152. package/src/verax/intel/vue-navigation-extractor.js +3 -0
  153. package/src/verax/learn/action-contract-extractor.js +67 -682
  154. package/src/verax/learn/ast-contract-extractor.js +1 -1
  155. package/src/verax/learn/flow-extractor.js +1 -0
  156. package/src/verax/learn/project-detector.js +5 -0
  157. package/src/verax/learn/react-router-extractor.js +2 -0
  158. package/src/verax/learn/route-validator.js +1 -4
  159. package/src/verax/learn/source-instrumenter.js +1 -0
  160. package/src/verax/learn/state-extractor.js +2 -1
  161. package/src/verax/learn/static-extractor.js +1 -0
  162. package/src/verax/observe/coverage-gaps.js +132 -0
  163. package/src/verax/observe/expectation-handler.js +126 -0
  164. package/src/verax/observe/incremental-skip.js +46 -0
  165. package/src/verax/observe/index.js +735 -84
  166. package/src/verax/observe/interaction-executor.js +192 -0
  167. package/src/verax/observe/interaction-runner.js +782 -530
  168. package/src/verax/observe/network-firewall.js +86 -0
  169. package/src/verax/observe/observation-builder.js +169 -0
  170. package/src/verax/observe/observe-context.js +1 -1
  171. package/src/verax/observe/observe-helpers.js +2 -1
  172. package/src/verax/observe/observe-runner.js +28 -24
  173. package/src/verax/observe/observers/budget-observer.js +3 -3
  174. package/src/verax/observe/observers/console-observer.js +4 -4
  175. package/src/verax/observe/observers/coverage-observer.js +4 -4
  176. package/src/verax/observe/observers/interaction-observer.js +3 -3
  177. package/src/verax/observe/observers/navigation-observer.js +4 -4
  178. package/src/verax/observe/observers/network-observer.js +4 -4
  179. package/src/verax/observe/observers/safety-observer.js +1 -1
  180. package/src/verax/observe/observers/ui-feedback-observer.js +4 -4
  181. package/src/verax/observe/page-traversal.js +138 -0
  182. package/src/verax/observe/snapshot-ops.js +94 -0
  183. package/src/verax/observe/ui-signal-sensor.js +2 -148
  184. package/src/verax/scan-summary-writer.js +10 -42
  185. package/src/verax/shared/artifact-manager.js +30 -13
  186. package/src/verax/shared/caching.js +1 -0
  187. package/src/verax/shared/expectation-tracker.js +1 -0
  188. package/src/verax/shared/zip-artifacts.js +6 -0
  189. package/src/verax/core/confidence-engine.js.backup +0 -471
  190. package/src/verax/shared/config-loader.js +0 -169
  191. /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
@@ -0,0 +1,342 @@
1
+ /**
2
+ * PHASE 6A: Semantic Determinism Comparison
3
+ *
4
+ * Provides deep semantic comparison of runs with field normalization.
5
+ * Ignores non-deterministic fields (timestamps, IDs, paths).
6
+ */
7
+
8
+ import { readFileSync } from 'fs';
9
+ import { join, normalize } from 'path';
10
+ import { createHash } from 'crypto';
11
+
12
+ /**
13
+ * Normalize non-deterministic fields for comparison
14
+ *
15
+ * @param {any} obj - Object to normalize
16
+ * @param {string} basePath - Base path for path normalization
17
+ * @returns {any} Normalized object
18
+ */
19
+ function normalizeObject(obj, basePath = '') {
20
+ if (obj === null || obj === undefined) {
21
+ return obj;
22
+ }
23
+
24
+ if (Array.isArray(obj)) {
25
+ return obj.map(item => normalizeObject(item, basePath));
26
+ }
27
+
28
+ if (typeof obj !== 'object') {
29
+ return obj;
30
+ }
31
+
32
+ const normalized = {};
33
+ const sortedKeys = Object.keys(obj).sort();
34
+
35
+ for (const key of sortedKeys) {
36
+ const value = obj[key];
37
+
38
+ // Skip non-deterministic fields
39
+ if (isNonDeterministicField(key)) {
40
+ continue;
41
+ }
42
+
43
+ // Normalize paths
44
+ if (isPathField(key) && typeof value === 'string') {
45
+ normalized[key] = normalizePath(value, basePath);
46
+ continue;
47
+ }
48
+
49
+ // Recursively normalize
50
+ normalized[key] = normalizeObject(value, basePath);
51
+ }
52
+
53
+ return normalized;
54
+ }
55
+
56
+ /**
57
+ * Check if field name indicates non-deterministic data
58
+ *
59
+ * @param {string} fieldName - Field name
60
+ * @returns {boolean} True if non-deterministic
61
+ */
62
+ function isNonDeterministicField(fieldName) {
63
+ const nonDeterministicFields = [
64
+ 'timestamp',
65
+ 'createdAt',
66
+ 'updatedAt',
67
+ 'startedAt',
68
+ 'completedAt',
69
+ 'observedAt',
70
+ 'detectedAt',
71
+ 'generatedAt',
72
+ 'verifiedAt',
73
+ 'writtenAt',
74
+ 'learnedAt',
75
+ 'failedAt',
76
+ 'runId',
77
+ 'pid',
78
+ 'duration',
79
+ 'durationMs',
80
+ 'totalMs',
81
+ 'learnMs',
82
+ 'observeMs',
83
+ 'detectMs',
84
+ 'relativeTime',
85
+ 'sequence',
86
+ ];
87
+
88
+ return nonDeterministicFields.includes(fieldName) ||
89
+ fieldName.endsWith('At') ||
90
+ fieldName.endsWith('Time') ||
91
+ fieldName.endsWith('Ms') ||
92
+ fieldName.includes('timestamp') ||
93
+ fieldName.includes('Timestamp');
94
+ }
95
+
96
+ /**
97
+ * Check if field name indicates path data
98
+ *
99
+ * @param {string} fieldName - Field name
100
+ * @returns {boolean} True if path field
101
+ */
102
+ function isPathField(fieldName) {
103
+ return fieldName.endsWith('Path') ||
104
+ fieldName.endsWith('Dir') ||
105
+ fieldName === 'cwd' ||
106
+ fieldName === 'src';
107
+ }
108
+
109
+ /**
110
+ * Normalize path for comparison (make relative to base)
111
+ *
112
+ * @param {string} path - Path to normalize
113
+ * @param {string} basePath - Base path
114
+ * @returns {string} Normalized path
115
+ */
116
+ function normalizePath(path, basePath) {
117
+ if (!path || !basePath) {
118
+ return path;
119
+ }
120
+
121
+ // Normalize separators
122
+ const normalizedPath = normalize(path).replace(/\\/g, '/');
123
+ const normalizedBase = normalize(basePath).replace(/\\/g, '/');
124
+
125
+ // Make relative if starts with base
126
+ if (normalizedPath.startsWith(normalizedBase)) {
127
+ return normalizedPath.substring(normalizedBase.length).replace(/^\//, '');
128
+ }
129
+
130
+ return normalizedPath;
131
+ }
132
+
133
+ /**
134
+ * Compute semantic hash of normalized object
135
+ *
136
+ * @param {Object} obj - Object to hash
137
+ * @returns {string} Semantic hash
138
+ */
139
+ function computeSemanticHash(obj) {
140
+ // Use JSON.stringify with replacer that sorts all object keys recursively
141
+ const sortedReplacer = (key, value) => {
142
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
143
+ return Object.keys(value).sort().reduce((sorted, key) => {
144
+ sorted[key] = value[key];
145
+ return sorted;
146
+ }, {});
147
+ }
148
+ return value;
149
+ };
150
+
151
+ const normalized = JSON.stringify(obj, sortedReplacer);
152
+ // @ts-expect-error - digest returns string
153
+ return createHash('sha256').update(normalized).digest('hex');
154
+ }
155
+
156
+ /**
157
+ * Compare two run summaries semantically
158
+ *
159
+ * @param {Object} summary1 - First run summary
160
+ * @param {Object} summary2 - Second run summary
161
+ * @param {string} basePath - Base path for normalization
162
+ * @returns {{ identical: boolean, differences: Object[] }} Comparison result
163
+ */
164
+ export function compareRunsSemantically(summary1, summary2, basePath = '') {
165
+ const norm1 = normalizeObject(summary1, basePath);
166
+ const norm2 = normalizeObject(summary2, basePath);
167
+
168
+ const hash1 = computeSemanticHash(norm1);
169
+ const hash2 = computeSemanticHash(norm2);
170
+
171
+ if (hash1 === hash2) {
172
+ return { identical: true, differences: [] };
173
+ }
174
+
175
+ // Find differences
176
+ const differences = findDifferences(norm1, norm2, '');
177
+
178
+ return { identical: false, differences };
179
+ }
180
+
181
+ /**
182
+ * Find differences between two normalized objects
183
+ *
184
+ * @param {any} obj1 - First object
185
+ * @param {any} obj2 - Second object
186
+ * @param {string} path - Current path in object tree
187
+ * @returns {Object[]} List of differences
188
+ */
189
+ function findDifferences(obj1, obj2, path) {
190
+ const differences = [];
191
+
192
+ if (typeof obj1 !== typeof obj2) {
193
+ differences.push({
194
+ path,
195
+ type: 'type-mismatch',
196
+ value1: typeof obj1,
197
+ value2: typeof obj2,
198
+ });
199
+ return differences;
200
+ }
201
+
202
+ if (obj1 === null || obj2 === null) {
203
+ if (obj1 !== obj2) {
204
+ differences.push({
205
+ path,
206
+ type: 'null-mismatch',
207
+ value1: obj1,
208
+ value2: obj2,
209
+ });
210
+ }
211
+ return differences;
212
+ }
213
+
214
+ if (typeof obj1 !== 'object') {
215
+ if (obj1 !== obj2) {
216
+ differences.push({
217
+ path,
218
+ type: 'value-mismatch',
219
+ value1: obj1,
220
+ value2: obj2,
221
+ });
222
+ }
223
+ return differences;
224
+ }
225
+
226
+ if (Array.isArray(obj1) !== Array.isArray(obj2)) {
227
+ differences.push({
228
+ path,
229
+ type: 'array-mismatch',
230
+ value1: Array.isArray(obj1),
231
+ value2: Array.isArray(obj2),
232
+ });
233
+ return differences;
234
+ }
235
+
236
+ if (Array.isArray(obj1)) {
237
+ if (obj1.length !== obj2.length) {
238
+ differences.push({
239
+ path,
240
+ type: 'length-mismatch',
241
+ value1: obj1.length,
242
+ value2: obj2.length,
243
+ });
244
+ }
245
+
246
+ const maxLen = Math.max(obj1.length, obj2.length);
247
+ for (let i = 0; i < maxLen; i++) {
248
+ differences.push(...findDifferences(
249
+ obj1[i],
250
+ obj2[i],
251
+ `${path}[${i}]`
252
+ ));
253
+ }
254
+
255
+ return differences;
256
+ }
257
+
258
+ // Object comparison
259
+ const keys1 = Object.keys(obj1).sort();
260
+ const keys2 = Object.keys(obj2).sort();
261
+
262
+ const allKeys = new Set([...keys1, ...keys2]);
263
+
264
+ for (const key of allKeys) {
265
+ const subPath = path ? `${path}.${key}` : key;
266
+
267
+ if (!(key in obj1)) {
268
+ differences.push({
269
+ path: subPath,
270
+ type: 'missing-in-first',
271
+ value2: obj2[key],
272
+ });
273
+ continue;
274
+ }
275
+
276
+ if (!(key in obj2)) {
277
+ differences.push({
278
+ path: subPath,
279
+ type: 'missing-in-second',
280
+ value1: obj1[key],
281
+ });
282
+ continue;
283
+ }
284
+
285
+ differences.push(...findDifferences(obj1[key], obj2[key], subPath));
286
+ }
287
+
288
+ return differences;
289
+ }
290
+
291
+ /**
292
+ * Load and compare two runs
293
+ *
294
+ * @param {string} runDir1 - First run directory
295
+ * @param {string} runDir2 - Second run directory
296
+ * @param {string} basePath - Base path for normalization
297
+ * @returns {{ ok: boolean, identical?: boolean, differences?: Object[], error?: string }} Result
298
+ */
299
+ export function loadAndCompareRuns(runDir1, runDir2, basePath) {
300
+ try {
301
+ const summary1Path = join(runDir1, 'summary.json');
302
+ const summary2Path = join(runDir2, 'summary.json');
303
+
304
+ const content1 = readFileSync(summary1Path, 'utf8');
305
+ const content2 = readFileSync(summary2Path, 'utf8');
306
+
307
+ // @ts-expect-error - readFileSync with encoding returns string
308
+ const summary1 = JSON.parse(content1);
309
+ // @ts-expect-error - readFileSync with encoding returns string
310
+ const summary2 = JSON.parse(content2);
311
+
312
+ const comparison = compareRunsSemantically(summary1, summary2, basePath);
313
+
314
+ return {
315
+ ok: true,
316
+ identical: comparison.identical,
317
+ differences: comparison.differences,
318
+ };
319
+ } catch (error) {
320
+ return {
321
+ ok: false,
322
+ error: error.message,
323
+ };
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Normalize findings for semantic comparison
329
+ *
330
+ * @param {Object[]} findings - Findings array
331
+ * @returns {Object[]} Normalized findings
332
+ */
333
+ export function normalizeFindingsForComparison(findings) {
334
+ return findings
335
+ .map(f => normalizeObject(f, ''))
336
+ .sort((a, b) => {
337
+ // Sort by expectationId for stable comparison
338
+ const idA = a.expectationId || '';
339
+ const idB = b.expectationId || '';
340
+ return idA.localeCompare(idB);
341
+ });
342
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * PHASE 6A: Cryptographic Integrity System
3
+ *
4
+ * Provides SHA256-based integrity verification for all run artifacts.
5
+ * Ensures tamper detection and corruption protection.
6
+ */
7
+
8
+ import { createHash } from 'crypto';
9
+ import { readFileSync, statSync as _statSync, readdirSync } from 'fs';
10
+ import { join, basename as _basename } from 'path';
11
+ import { atomicWriteJson } from '../../../cli/util/atomic-write.js';
12
+
13
+ /**
14
+ * Compute SHA256 hash of file contents
15
+ *
16
+ * @param {string} filePath - Absolute path to file
17
+ * @returns {{ hash: string, size: number, error?: string }} Hash result
18
+ */
19
+ export function computeFileIntegrity(filePath) {
20
+ try {
21
+ const content = readFileSync(filePath);
22
+ const hash = createHash('sha256').update(content).digest('hex');
23
+ const size = content.length;
24
+ // @ts-expect-error - digest returns string
25
+ return { hash, size };
26
+ } catch (error) {
27
+ return { hash: null, size: 0, error: error.message };
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Generate integrity manifest for all artifacts in run directory
33
+ *
34
+ * @param {string} runDir - Run directory path
35
+ * @param {string[]} artifactNames - List of artifact filenames to include
36
+ * @returns {{ manifest: Object, errors: string[] }} Manifest and any errors
37
+ */
38
+ export function generateIntegrityManifest(runDir, artifactNames) {
39
+ const manifest = {
40
+ version: 1,
41
+ generatedAt: new Date().toISOString(),
42
+ runDir,
43
+ artifacts: {},
44
+ };
45
+
46
+ const errors = [];
47
+
48
+ for (const name of artifactNames) {
49
+ const filePath = join(runDir, name);
50
+ const integrity = computeFileIntegrity(filePath);
51
+
52
+ if (integrity.error) {
53
+ errors.push(`Failed to hash ${name}: ${integrity.error}`);
54
+ continue;
55
+ }
56
+
57
+ manifest.artifacts[name] = {
58
+ sha256: integrity.hash,
59
+ size: integrity.size,
60
+ verifiedAt: null, // Set when verified
61
+ };
62
+ }
63
+
64
+ return { manifest, errors };
65
+ }
66
+
67
+ /**
68
+ * Write integrity manifest to run directory
69
+ *
70
+ * @param {string} runDir - Run directory path
71
+ * @param {Object} manifest - Integrity manifest object
72
+ * @returns {{ ok: boolean, path?: string, error?: Error }} Write result
73
+ */
74
+ export function writeIntegrityManifest(runDir, manifest) {
75
+ const manifestPath = join(runDir, 'integrity.manifest.json');
76
+
77
+ try {
78
+ atomicWriteJson(manifestPath, manifest);
79
+ return { ok: true, path: manifestPath };
80
+ } catch (error) {
81
+ return { ok: false, error };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Verify artifact integrity against manifest
87
+ *
88
+ * @param {string} runDir - Run directory path
89
+ * @param {string} artifactName - Artifact filename
90
+ * @param {Object} manifest - Integrity manifest
91
+ * @returns {{ ok: boolean, error?: string, expectedHash?: string, actualHash?: string }} Verification result
92
+ */
93
+ export function verifyArtifactIntegrity(runDir, artifactName, manifest) {
94
+ const artifactPath = join(runDir, artifactName);
95
+
96
+ // Check if artifact exists in manifest
97
+ if (!manifest.artifacts[artifactName]) {
98
+ return {
99
+ ok: false,
100
+ error: `Artifact ${artifactName} not found in integrity manifest`,
101
+ };
102
+ }
103
+
104
+ const expected = manifest.artifacts[artifactName];
105
+ const integrity = computeFileIntegrity(artifactPath);
106
+
107
+ if (integrity.error) {
108
+ return {
109
+ ok: false,
110
+ error: `Failed to read artifact ${artifactName}: ${integrity.error}`,
111
+ };
112
+ }
113
+
114
+ if (integrity.hash !== expected.sha256) {
115
+ return {
116
+ ok: false,
117
+ error: `Integrity violation: ${artifactName} hash mismatch`,
118
+ expectedHash: expected.sha256,
119
+ actualHash: integrity.hash,
120
+ };
121
+ }
122
+
123
+ if (integrity.size !== expected.size) {
124
+ return {
125
+ ok: false,
126
+ error: `Integrity violation: ${artifactName} size mismatch (expected ${expected.size}, got ${integrity.size})`,
127
+ };
128
+ }
129
+
130
+ return { ok: true };
131
+ }
132
+
133
+ /**
134
+ * Load and verify integrity manifest
135
+ *
136
+ * @param {string} runDir - Run directory path
137
+ * @returns {{ ok: boolean, manifest?: Object, error?: string }} Load result
138
+ */
139
+ export function loadIntegrityManifest(runDir) {
140
+ try {
141
+ const manifestPath = join(runDir, 'integrity.manifest.json');
142
+ const content = readFileSync(manifestPath, 'utf8');
143
+ // @ts-expect-error - readFileSync with encoding returns string
144
+ const manifest = JSON.parse(content);
145
+
146
+ if (!manifest.version || !manifest.artifacts) {
147
+ return {
148
+ ok: false,
149
+ error: 'Invalid integrity manifest format',
150
+ };
151
+ }
152
+
153
+ return { ok: true, manifest };
154
+ } catch (error) {
155
+ return {
156
+ ok: false,
157
+ error: `Failed to load integrity manifest: ${error.message}`,
158
+ };
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Verify all artifacts in manifest
164
+ *
165
+ * @param {string} runDir - Run directory path
166
+ * @param {Object} manifest - Integrity manifest
167
+ * @returns {{ ok: boolean, verified: string[], failed: Array<{name: string, error: string}> }} Verification results
168
+ */
169
+ export function verifyAllArtifacts(runDir, manifest) {
170
+ const verified = [];
171
+ const failed = [];
172
+
173
+ for (const artifactName of Object.keys(manifest.artifacts)) {
174
+ const result = verifyArtifactIntegrity(runDir, artifactName, manifest);
175
+
176
+ if (result.ok) {
177
+ verified.push(artifactName);
178
+ } else {
179
+ failed.push({
180
+ name: artifactName,
181
+ error: result.error,
182
+ expectedHash: result.expectedHash,
183
+ actualHash: result.actualHash,
184
+ });
185
+ }
186
+ }
187
+
188
+ return {
189
+ ok: failed.length === 0,
190
+ verified,
191
+ failed,
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Discover all JSON artifacts in run directory
197
+ *
198
+ * @param {string} runDir - Run directory path
199
+ * @returns {string[]} List of artifact filenames
200
+ */
201
+ export function discoverArtifacts(runDir) {
202
+ try {
203
+ const files = readdirSync(runDir);
204
+ return files.filter(f => f.endsWith('.json') && f !== 'integrity.manifest.json');
205
+ } catch (error) {
206
+ return [];
207
+ }
208
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * PHASE 6A: Run Poisoning System
3
+ *
4
+ * Prevents consumption of incomplete or failed runs.
5
+ * Implements .INCOMPLETE marker for run safety.
6
+ */
7
+
8
+ import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'fs';
9
+ import { join } from 'path';
10
+
11
+ /**
12
+ * Create poisoning marker for run
13
+ *
14
+ * @param {string} runDir - Run directory path
15
+ * @param {string} runId - Run identifier
16
+ * @returns {{ ok: boolean, path?: string, error?: Error }} Result
17
+ */
18
+ export function createPoisonMarker(runDir, runId) {
19
+ try {
20
+ const markerPath = join(runDir, '.INCOMPLETE');
21
+ const marker = {
22
+ runId,
23
+ createdAt: new Date().toISOString(),
24
+ pid: process.pid,
25
+ reason: 'Run in progress',
26
+ };
27
+
28
+ writeFileSync(markerPath, JSON.stringify(marker, null, 2), 'utf8');
29
+
30
+ return { ok: true, path: markerPath };
31
+ } catch (error) {
32
+ return { ok: false, error };
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Remove poisoning marker (only on successful completion)
38
+ *
39
+ * @param {string} runDir - Run directory path
40
+ * @returns {{ ok: boolean, error?: Error }} Result
41
+ */
42
+ export function removePoisonMarker(runDir) {
43
+ try {
44
+ const markerPath = join(runDir, '.INCOMPLETE');
45
+
46
+ if (existsSync(markerPath)) {
47
+ unlinkSync(markerPath);
48
+ }
49
+
50
+ return { ok: true };
51
+ } catch (error) {
52
+ return { ok: false, error };
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Check if run is poisoned (incomplete)
58
+ *
59
+ * @param {string} runDir - Run directory path
60
+ * @returns {{ poisoned: boolean, marker?: Object, reason?: string }} Poisoning status
61
+ */
62
+ export function checkPoisonMarker(runDir) {
63
+ try {
64
+ const markerPath = join(runDir, '.INCOMPLETE');
65
+
66
+ if (!existsSync(markerPath)) {
67
+ return { poisoned: false };
68
+ }
69
+
70
+ const content = readFileSync(markerPath, 'utf8');
71
+ // @ts-expect-error - readFileSync with encoding returns string
72
+ const marker = JSON.parse(content);
73
+
74
+ return {
75
+ poisoned: true,
76
+ marker,
77
+ reason: marker.reason || 'Run incomplete',
78
+ };
79
+ } catch (error) {
80
+ // If marker exists but is unreadable, consider it poisoned
81
+ const markerPath = join(runDir, '.INCOMPLETE');
82
+ if (existsSync(markerPath)) {
83
+ return {
84
+ poisoned: true,
85
+ reason: 'Marker unreadable or corrupted',
86
+ };
87
+ }
88
+
89
+ return { poisoned: false };
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Enforce poisoning check - throw if run is poisoned
95
+ *
96
+ * @param {string} runDir - Run directory path
97
+ * @throws {Error} If run is poisoned
98
+ */
99
+ export function enforcePoisonCheck(runDir) {
100
+ const status = checkPoisonMarker(runDir);
101
+
102
+ if (status.poisoned) {
103
+ throw new Error(
104
+ `Cannot read run: RUN_POISONED (${status.reason || 'incomplete'}). ` +
105
+ `Run directory: ${runDir}`
106
+ );
107
+ }
108
+ }