@veraxhq/verax 0.2.1 → 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 (213) hide show
  1. package/README.md +10 -6
  2. package/bin/verax.js +11 -11
  3. package/package.json +29 -8
  4. package/src/cli/commands/baseline.js +103 -0
  5. package/src/cli/commands/default.js +51 -6
  6. package/src/cli/commands/doctor.js +29 -0
  7. package/src/cli/commands/ga.js +246 -0
  8. package/src/cli/commands/gates.js +95 -0
  9. package/src/cli/commands/inspect.js +4 -2
  10. package/src/cli/commands/release-check.js +215 -0
  11. package/src/cli/commands/run.js +45 -6
  12. package/src/cli/commands/security-check.js +212 -0
  13. package/src/cli/commands/truth.js +113 -0
  14. package/src/cli/entry.js +30 -20
  15. package/src/cli/util/angular-component-extractor.js +179 -0
  16. package/src/cli/util/angular-navigation-detector.js +141 -0
  17. package/src/cli/util/angular-network-detector.js +161 -0
  18. package/src/cli/util/angular-state-detector.js +162 -0
  19. package/src/cli/util/ast-interactive-detector.js +544 -0
  20. package/src/cli/util/ast-network-detector.js +603 -0
  21. package/src/cli/util/ast-promise-extractor.js +581 -0
  22. package/src/cli/util/ast-usestate-detector.js +602 -0
  23. package/src/cli/util/atomic-write.js +12 -1
  24. package/src/cli/util/bootstrap-guard.js +86 -0
  25. package/src/cli/util/console-reporter.js +72 -0
  26. package/src/cli/util/detection-engine.js +105 -41
  27. package/src/cli/util/determinism-runner.js +124 -0
  28. package/src/cli/util/determinism-writer.js +129 -0
  29. package/src/cli/util/digest-engine.js +359 -0
  30. package/src/cli/util/dom-diff.js +226 -0
  31. package/src/cli/util/evidence-engine.js +287 -0
  32. package/src/cli/util/expectation-extractor.js +151 -5
  33. package/src/cli/util/findings-writer.js +3 -0
  34. package/src/cli/util/framework-detector.js +572 -0
  35. package/src/cli/util/idgen.js +1 -1
  36. package/src/cli/util/interaction-planner.js +529 -0
  37. package/src/cli/util/learn-writer.js +2 -0
  38. package/src/cli/util/ledger-writer.js +110 -0
  39. package/src/cli/util/monorepo-resolver.js +162 -0
  40. package/src/cli/util/observation-engine.js +127 -278
  41. package/src/cli/util/observe-writer.js +2 -0
  42. package/src/cli/util/project-discovery.js +284 -0
  43. package/src/cli/util/project-writer.js +2 -0
  44. package/src/cli/util/run-id.js +23 -27
  45. package/src/cli/util/run-resolver.js +64 -0
  46. package/src/cli/util/run-result.js +778 -0
  47. package/src/cli/util/selector-resolver.js +235 -0
  48. package/src/cli/util/source-requirement.js +55 -0
  49. package/src/cli/util/summary-writer.js +2 -0
  50. package/src/cli/util/svelte-navigation-detector.js +163 -0
  51. package/src/cli/util/svelte-network-detector.js +80 -0
  52. package/src/cli/util/svelte-sfc-extractor.js +146 -0
  53. package/src/cli/util/svelte-state-detector.js +242 -0
  54. package/src/cli/util/trust-activation-integration.js +496 -0
  55. package/src/cli/util/trust-activation-wrapper.js +85 -0
  56. package/src/cli/util/trust-integration-hooks.js +164 -0
  57. package/src/cli/util/types.js +153 -0
  58. package/src/cli/util/url-validation.js +40 -0
  59. package/src/cli/util/vue-navigation-detector.js +178 -0
  60. package/src/cli/util/vue-sfc-extractor.js +161 -0
  61. package/src/cli/util/vue-state-detector.js +215 -0
  62. package/src/types/fs-augment.d.ts +23 -0
  63. package/src/types/global.d.ts +137 -0
  64. package/src/types/internal-types.d.ts +35 -0
  65. package/src/verax/cli/init.js +4 -18
  66. package/src/verax/core/action-classifier.js +4 -3
  67. package/src/verax/core/artifacts/registry.js +139 -0
  68. package/src/verax/core/artifacts/verifier.js +990 -0
  69. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  70. package/src/verax/core/baseline/baseline.snapshot.js +233 -0
  71. package/src/verax/core/capabilities/gates.js +505 -0
  72. package/src/verax/core/capabilities/registry.js +475 -0
  73. package/src/verax/core/confidence/confidence-compute.js +144 -0
  74. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  75. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  76. package/src/verax/core/confidence/confidence-weights.js +44 -0
  77. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  78. package/src/verax/core/confidence/confidence.loader.js +80 -0
  79. package/src/verax/core/confidence/confidence.schema.js +94 -0
  80. package/src/verax/core/confidence-engine-refactor.js +489 -0
  81. package/src/verax/core/confidence-engine.js +625 -0
  82. package/src/verax/core/contracts/index.js +29 -0
  83. package/src/verax/core/contracts/types.js +186 -0
  84. package/src/verax/core/contracts/validators.js +456 -0
  85. package/src/verax/core/decisions/decision.trace.js +278 -0
  86. package/src/verax/core/determinism/contract-writer.js +89 -0
  87. package/src/verax/core/determinism/contract.js +139 -0
  88. package/src/verax/core/determinism/diff.js +405 -0
  89. package/src/verax/core/determinism/engine.js +222 -0
  90. package/src/verax/core/determinism/finding-identity.js +149 -0
  91. package/src/verax/core/determinism/normalize.js +466 -0
  92. package/src/verax/core/determinism/report-writer.js +93 -0
  93. package/src/verax/core/determinism/run-fingerprint.js +123 -0
  94. package/src/verax/core/dynamic-route-intelligence.js +529 -0
  95. package/src/verax/core/evidence/evidence-capture-service.js +308 -0
  96. package/src/verax/core/evidence/evidence-intent-ledger.js +166 -0
  97. package/src/verax/core/evidence-builder.js +487 -0
  98. package/src/verax/core/execution-mode-context.js +77 -0
  99. package/src/verax/core/execution-mode-detector.js +192 -0
  100. package/src/verax/core/failures/exit-codes.js +88 -0
  101. package/src/verax/core/failures/failure-summary.js +76 -0
  102. package/src/verax/core/failures/failure.factory.js +225 -0
  103. package/src/verax/core/failures/failure.ledger.js +133 -0
  104. package/src/verax/core/failures/failure.types.js +196 -0
  105. package/src/verax/core/failures/index.js +10 -0
  106. package/src/verax/core/ga/ga-report-writer.js +43 -0
  107. package/src/verax/core/ga/ga.artifact.js +49 -0
  108. package/src/verax/core/ga/ga.contract.js +435 -0
  109. package/src/verax/core/ga/ga.enforcer.js +87 -0
  110. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  111. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  112. package/src/verax/core/guardrails/policy.loader.js +84 -0
  113. package/src/verax/core/guardrails/policy.schema.js +110 -0
  114. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  115. package/src/verax/core/guardrails-engine.js +505 -0
  116. package/src/verax/core/incremental-store.js +1 -0
  117. package/src/verax/core/integrity/budget.js +138 -0
  118. package/src/verax/core/integrity/determinism.js +342 -0
  119. package/src/verax/core/integrity/integrity.js +208 -0
  120. package/src/verax/core/integrity/poisoning.js +108 -0
  121. package/src/verax/core/integrity/transaction.js +140 -0
  122. package/src/verax/core/observe/run-timeline.js +318 -0
  123. package/src/verax/core/perf/perf.contract.js +186 -0
  124. package/src/verax/core/perf/perf.display.js +65 -0
  125. package/src/verax/core/perf/perf.enforcer.js +91 -0
  126. package/src/verax/core/perf/perf.monitor.js +209 -0
  127. package/src/verax/core/perf/perf.report.js +200 -0
  128. package/src/verax/core/pipeline-tracker.js +243 -0
  129. package/src/verax/core/product-definition.js +127 -0
  130. package/src/verax/core/release/provenance.builder.js +130 -0
  131. package/src/verax/core/release/release-report-writer.js +40 -0
  132. package/src/verax/core/release/release.enforcer.js +164 -0
  133. package/src/verax/core/release/reproducibility.check.js +222 -0
  134. package/src/verax/core/release/sbom.builder.js +292 -0
  135. package/src/verax/core/replay-validator.js +2 -0
  136. package/src/verax/core/replay.js +4 -0
  137. package/src/verax/core/report/cross-index.js +195 -0
  138. package/src/verax/core/report/human-summary.js +362 -0
  139. package/src/verax/core/route-intelligence.js +420 -0
  140. package/src/verax/core/run-id.js +6 -3
  141. package/src/verax/core/run-manifest.js +4 -3
  142. package/src/verax/core/security/secrets.scan.js +329 -0
  143. package/src/verax/core/security/security-report.js +50 -0
  144. package/src/verax/core/security/security.enforcer.js +128 -0
  145. package/src/verax/core/security/supplychain.defaults.json +38 -0
  146. package/src/verax/core/security/supplychain.policy.js +334 -0
  147. package/src/verax/core/security/vuln.scan.js +265 -0
  148. package/src/verax/core/truth/truth.certificate.js +252 -0
  149. package/src/verax/core/ui-feedback-intelligence.js +481 -0
  150. package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
  151. package/src/verax/detect/confidence-engine.js +62 -34
  152. package/src/verax/detect/confidence-helper.js +34 -0
  153. package/src/verax/detect/dynamic-route-findings.js +338 -0
  154. package/src/verax/detect/expectation-chain-detector.js +417 -0
  155. package/src/verax/detect/expectation-model.js +2 -2
  156. package/src/verax/detect/failure-cause-inference.js +293 -0
  157. package/src/verax/detect/findings-writer.js +131 -35
  158. package/src/verax/detect/flow-detector.js +2 -2
  159. package/src/verax/detect/form-silent-failure.js +98 -0
  160. package/src/verax/detect/index.js +46 -5
  161. package/src/verax/detect/invariants-enforcer.js +147 -0
  162. package/src/verax/detect/journey-stall-detector.js +558 -0
  163. package/src/verax/detect/navigation-silent-failure.js +82 -0
  164. package/src/verax/detect/problem-aggregator.js +361 -0
  165. package/src/verax/detect/route-findings.js +219 -0
  166. package/src/verax/detect/summary-writer.js +477 -0
  167. package/src/verax/detect/test-failure-cause-inference.js +314 -0
  168. package/src/verax/detect/ui-feedback-findings.js +207 -0
  169. package/src/verax/detect/view-switch-correlator.js +242 -0
  170. package/src/verax/flow/flow-engine.js +2 -1
  171. package/src/verax/flow/flow-spec.js +0 -6
  172. package/src/verax/index.js +4 -0
  173. package/src/verax/intel/ts-program.js +1 -0
  174. package/src/verax/intel/vue-navigation-extractor.js +3 -0
  175. package/src/verax/learn/action-contract-extractor.js +3 -0
  176. package/src/verax/learn/ast-contract-extractor.js +1 -1
  177. package/src/verax/learn/flow-extractor.js +1 -0
  178. package/src/verax/learn/project-detector.js +5 -0
  179. package/src/verax/learn/react-router-extractor.js +2 -0
  180. package/src/verax/learn/source-instrumenter.js +1 -0
  181. package/src/verax/learn/state-extractor.js +2 -1
  182. package/src/verax/learn/static-extractor.js +1 -0
  183. package/src/verax/observe/coverage-gaps.js +132 -0
  184. package/src/verax/observe/expectation-handler.js +126 -0
  185. package/src/verax/observe/incremental-skip.js +46 -0
  186. package/src/verax/observe/index.js +51 -155
  187. package/src/verax/observe/interaction-executor.js +192 -0
  188. package/src/verax/observe/interaction-runner.js +782 -513
  189. package/src/verax/observe/network-firewall.js +86 -0
  190. package/src/verax/observe/observation-builder.js +169 -0
  191. package/src/verax/observe/observe-context.js +205 -0
  192. package/src/verax/observe/observe-helpers.js +192 -0
  193. package/src/verax/observe/observe-runner.js +230 -0
  194. package/src/verax/observe/observers/budget-observer.js +185 -0
  195. package/src/verax/observe/observers/console-observer.js +102 -0
  196. package/src/verax/observe/observers/coverage-observer.js +107 -0
  197. package/src/verax/observe/observers/interaction-observer.js +471 -0
  198. package/src/verax/observe/observers/navigation-observer.js +132 -0
  199. package/src/verax/observe/observers/network-observer.js +87 -0
  200. package/src/verax/observe/observers/safety-observer.js +82 -0
  201. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  202. package/src/verax/observe/page-traversal.js +138 -0
  203. package/src/verax/observe/snapshot-ops.js +94 -0
  204. package/src/verax/observe/ui-feedback-detector.js +742 -0
  205. package/src/verax/scan-summary-writer.js +2 -0
  206. package/src/verax/shared/artifact-manager.js +25 -5
  207. package/src/verax/shared/caching.js +1 -0
  208. package/src/verax/shared/css-spinner-rules.js +204 -0
  209. package/src/verax/shared/expectation-tracker.js +1 -0
  210. package/src/verax/shared/view-switch-rules.js +208 -0
  211. package/src/verax/shared/zip-artifacts.js +6 -0
  212. package/src/verax/shared/config-loader.js +0 -169
  213. /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
@@ -0,0 +1,496 @@
1
+ /**
2
+ * Phase 6A: Artifact Staging, Integrity, & Atomicity
3
+ *
4
+ * Provides production-ready artifact management:
5
+ * - Poison markers to detect incomplete runs
6
+ * - Staging directories for atomic writes
7
+ * - Integrity verification with checksums
8
+ * - Atomic commit of verified artifacts
9
+ * - Rollback with ledger on failures
10
+ *
11
+ * Integration points:
12
+ * 1. initPhase6A() - Called at scan START
13
+ * 2. redirectArtifactWrites() - Intercepts ALL artifact writes
14
+ * 3. completePhase6A() - Called on successful completion
15
+ * 4. rollbackPhase6A() - Called on ANY error
16
+ * 5. checkPoisonMarker() - Called before reading previous runs
17
+ */
18
+
19
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync as _rmSync, renameSync, readdirSync, unlinkSync, statSync } from 'fs';
20
+ import { join, dirname as _dirname } from 'path';
21
+ import { createHash } from 'crypto';
22
+
23
+ /**
24
+ * Initialize Phase 6A on scan START
25
+ *
26
+ * Actions:
27
+ * 1. Create staging directory
28
+ * 2. Create poison marker
29
+ *
30
+ * @param {string} artifactDir - Artifact directory
31
+ * @returns {Promise<{ success: boolean, poisonMarkerPath?: string, stagingDir?: string, error?: Error }>}
32
+ */
33
+ export async function initPhase6A(artifactDir) {
34
+ try {
35
+ // Create staging directory if it doesn't exist
36
+ const stagingDir = getStagingPath(artifactDir, '').replace(/\/$/, '');
37
+ mkdirSync(stagingDir, { recursive: true });
38
+
39
+ // Create poison marker indicating scan in progress
40
+ const poisonMarkerPath = getPoisonMarkerPath(artifactDir);
41
+ const poisonContent = JSON.stringify({
42
+ timestamp: new Date().toISOString(),
43
+ version: '1.0',
44
+ status: 'in-progress',
45
+ }, null, 2);
46
+
47
+ writeFileSync(poisonMarkerPath, poisonContent, 'utf-8');
48
+
49
+ return {
50
+ success: true,
51
+ poisonMarkerPath,
52
+ stagingDir,
53
+ };
54
+ } catch (error) {
55
+ return { success: false, error };
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Redirect artifact write to staging directory
61
+ *
62
+ * This function is called for EVERY artifact write.
63
+ * It routes the write to the staging directory instead of the final location.
64
+ *
65
+ * @param {string} artifactDir - Artifact directory
66
+ * @param {string} filename - Artifact filename (e.g., 'summary.json')
67
+ * @returns {string} Path to staging artifact
68
+ */
69
+ export function redirectArtifactWrites(artifactDir, filename) {
70
+ // Validate artifact name
71
+ validateArtifactPath(artifactDir, filename);
72
+
73
+ // Return staging path
74
+ return getStagingPath(artifactDir, filename);
75
+ }
76
+
77
+ /**
78
+ * Validate artifact path (whitelist approach)
79
+ *
80
+ * @param {string} artifactDir - Artifact directory
81
+ * @param {string} filename - Filename to validate
82
+ * @throws {Error} If filename is not a valid artifact
83
+ */
84
+ export function validateArtifactPath(artifactDir, filename) {
85
+ const validArtifacts = [
86
+ 'summary.json',
87
+ 'findings.json',
88
+ 'ledger.json',
89
+ 'observations.json',
90
+ 'report.html',
91
+ 'learn.json',
92
+ 'manifest.json',
93
+ 'observations-legacy.json',
94
+ 'observations-legacy-formatted.json',
95
+ ];
96
+
97
+ if (!validArtifacts.includes(filename)) {
98
+ throw new Error(`Invalid artifact: ${filename}. Must be one of: ${validArtifacts.join(', ')}`);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Complete Phase 6A on successful run
104
+ *
105
+ * Actions:
106
+ * 1. Generate integrity manifest
107
+ * 2. Verify all artifacts
108
+ * 3. Atomically commit staging to final
109
+ * 4. Remove poison marker
110
+ *
111
+ * @param {string} artifactDir - Artifact directory
112
+ * @returns {Promise<{ success: boolean, verification?: any, error?: Error }>}
113
+ */
114
+ export async function completePhase6A(artifactDir) {
115
+ try {
116
+ const stagingDir = getStagingPath(artifactDir, '').replace(/\/$/, '');
117
+
118
+ // Check if staging directory exists
119
+ if (!existsSync(stagingDir)) {
120
+ return {
121
+ success: false,
122
+ error: new Error('Staging directory does not exist'),
123
+ };
124
+ }
125
+
126
+ // Discover artifacts in staging
127
+ const allFiles = readdirSync(stagingDir);
128
+ const artifacts = allFiles.filter(f => f.endsWith('.json') && f !== 'integrity.manifest.json');
129
+
130
+ if (artifacts.length === 0) {
131
+ return {
132
+ success: false,
133
+ error: new Error('No artifacts found in staging directory'),
134
+ };
135
+ }
136
+
137
+ // Generate integrity manifest
138
+ const manifestResult = generateIntegrityManifest(stagingDir, artifacts);
139
+ if (manifestResult.errors.length > 0) {
140
+ return {
141
+ success: false,
142
+ error: new Error(`Failed to generate integrity manifest: ${manifestResult.errors.join(', ')}`),
143
+ };
144
+ }
145
+
146
+ // Write manifest to staging
147
+ const writeResult = writeIntegrityManifest(stagingDir, manifestResult);
148
+ if (!writeResult.ok) {
149
+ return {
150
+ success: false,
151
+ error: writeResult.error,
152
+ };
153
+ }
154
+
155
+ // Verify all artifacts
156
+ const verification = verifyAllArtifacts(stagingDir, manifestResult);
157
+
158
+ if (!verification.ok) {
159
+ return {
160
+ success: false,
161
+ verification,
162
+ error: new Error(`Artifact integrity verification failed: ${verification.failed.map(f => f.name).join(', ')}`),
163
+ };
164
+ }
165
+
166
+ // Commit staging directory atomically
167
+ await commitStagingDir(artifactDir);
168
+
169
+ // Remove poison marker only after successful commit
170
+ removePoisonMarker(artifactDir);
171
+
172
+ return {
173
+ success: true,
174
+ verification,
175
+ };
176
+ } catch (error) {
177
+ return { success: false, error };
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Rollback Phase 6A on error
183
+ *
184
+ * Actions:
185
+ * 1. Write ledger entry with error details
186
+ * 2. Clean staging artifacts
187
+ * 3. KEEP poison marker (prevents retry)
188
+ *
189
+ * @param {string} artifactDir - Artifact directory
190
+ * @param {Error} error - Error that occurred
191
+ * @returns {Promise<{ success: boolean, ledgerEntry?: any, error?: Error }>}
192
+ */
193
+ export async function rollbackPhase6A(artifactDir, error) {
194
+ try {
195
+ // Create ledger entry
196
+ const ledgerEntry = createLedgerEntry('error', error);
197
+
198
+ // Append to ledger
199
+ const ledgerPath = join(artifactDir, 'ledger.json');
200
+ let ledger = [];
201
+
202
+ if (existsSync(ledgerPath)) {
203
+ const content = readFileSync(ledgerPath, 'utf-8');
204
+ try {
205
+ // @ts-expect-error - readFileSync with encoding returns string
206
+ ledger = JSON.parse(content);
207
+ } catch (e) {
208
+ // If ledger is corrupted, start fresh
209
+ ledger = [];
210
+ }
211
+ }
212
+
213
+ ledger.push(ledgerEntry);
214
+ writeFileSync(ledgerPath, JSON.stringify(ledger, null, 2), 'utf-8');
215
+
216
+ // Clean staging artifacts but KEEP poison marker
217
+ const stagingDir = getStagingPath(artifactDir, '').replace(/\/$/, '');
218
+ if (existsSync(stagingDir)) {
219
+ const files = readdirSync(stagingDir);
220
+ for (const file of files) {
221
+ const filePath = join(stagingDir, file);
222
+ try {
223
+ if (statSync(filePath).isFile()) {
224
+ unlinkSync(filePath);
225
+ }
226
+ } catch (e) {
227
+ // Ignore file deletion errors
228
+ }
229
+ }
230
+ }
231
+
232
+ return {
233
+ success: true,
234
+ ledgerEntry,
235
+ };
236
+ } catch (error) {
237
+ return { success: false, error };
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Check for poison marker indicating incomplete run
243
+ *
244
+ * This should be called BEFORE reading previous run results to detect
245
+ * incomplete or corrupted previous runs.
246
+ *
247
+ * @param {string} artifactDir - Artifact directory
248
+ * @returns {{ hasPoisonMarker: boolean, entry?: any }}
249
+ */
250
+ export function checkPoisonMarker(artifactDir) {
251
+ const poisonPath = getPoisonMarkerPath(artifactDir);
252
+
253
+ if (!existsSync(poisonPath)) {
254
+ return { hasPoisonMarker: false };
255
+ }
256
+
257
+ try {
258
+ const content = readFileSync(poisonPath, 'utf-8');
259
+ // @ts-expect-error - readFileSync with encoding returns string
260
+ const entry = JSON.parse(content);
261
+ return { hasPoisonMarker: true, entry };
262
+ } catch (e) {
263
+ return { hasPoisonMarker: true };
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Create ledger entry for a run event
269
+ *
270
+ * @param {string} status - 'success', 'error', or 'partial'
271
+ * @param {Error} error - Error object (if status is 'error')
272
+ * @param {object} metadata - Additional metadata
273
+ * @returns {object} Ledger entry
274
+ */
275
+ export function createLedgerEntry(status, error, metadata = {}) {
276
+ return {
277
+ timestamp: new Date().toISOString(),
278
+ status,
279
+ error: error ? error.message : undefined,
280
+ stack: error ? error.stack : undefined,
281
+ metadata,
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Get poison marker path
287
+ *
288
+ * @param {string} artifactDir - Artifact directory
289
+ * @returns {string} Path to poison marker
290
+ */
291
+ export function getPoisonMarkerPath(artifactDir) {
292
+ return join(artifactDir, '.poison-marker.json');
293
+ }
294
+
295
+ /**
296
+ * Get staging directory path for artifacts
297
+ *
298
+ * @param {string} artifactDir - Artifact directory
299
+ * @param {string} filename - Optional filename to append
300
+ * @returns {string} Path to staging location
301
+ */
302
+ export function getStagingPath(artifactDir, filename) {
303
+ const stagingDir = join(artifactDir, '.staging');
304
+ if (filename) {
305
+ return join(stagingDir, filename);
306
+ }
307
+ return stagingDir + '/'; // Return with trailing slash for consistency
308
+ }
309
+
310
+ /**
311
+ * Generate integrity manifest for all artifacts
312
+ *
313
+ * Creates SHA256 checksums for each artifact to detect corruption.
314
+ *
315
+ * @param {string} stagingDir - Staging directory
316
+ * @param {string[]} artifacts - List of artifact filenames
317
+ * @returns {{ checksums: object, generatedAt: string, errors: string[] }}
318
+ */
319
+ export function generateIntegrityManifest(stagingDir, artifacts) {
320
+ const checksums = {};
321
+ const errors = [];
322
+
323
+ for (const artifact of artifacts) {
324
+ try {
325
+ const filePath = join(stagingDir, artifact);
326
+ if (!existsSync(filePath)) {
327
+ errors.push(`Artifact not found: ${artifact}`);
328
+ continue;
329
+ }
330
+
331
+ const content = readFileSync(filePath, 'utf-8');
332
+ if (typeof content !== 'string') {
333
+ errors.push(`Failed to read ${artifact}: content is not a string`);
334
+ continue;
335
+ }
336
+
337
+ const hash = createHash('sha256').update(content).digest('hex');
338
+ checksums[artifact] = hash;
339
+ } catch (error) {
340
+ errors.push(`Failed to hash ${artifact}: ${error.message}`);
341
+ }
342
+ }
343
+
344
+ return {
345
+ checksums,
346
+ generatedAt: new Date().toISOString(),
347
+ errors,
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Write integrity manifest to staging directory
353
+ *
354
+ * @param {string} stagingDir - Staging directory
355
+ * @param {object} manifest - Manifest object
356
+ * @returns {{ ok: boolean, error?: Error }}
357
+ */
358
+ export function writeIntegrityManifest(stagingDir, manifest) {
359
+ try {
360
+ if (!manifest || typeof manifest !== 'object') {
361
+ return { ok: false, error: new Error('Invalid manifest object') };
362
+ }
363
+
364
+ const manifestPath = join(stagingDir, 'integrity.manifest.json');
365
+ const manifestContent = JSON.stringify(manifest, null, 2);
366
+
367
+ if (typeof manifestContent !== 'string') {
368
+ return { ok: false, error: new Error('Failed to serialize manifest') };
369
+ }
370
+
371
+ writeFileSync(manifestPath, manifestContent, 'utf-8');
372
+ return { ok: true };
373
+ } catch (error) {
374
+ return { ok: false, error };
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Verify all artifacts match their checksums in the manifest
380
+ *
381
+ * @param {string} stagingDir - Staging directory
382
+ * @param {object} manifest - Manifest object with checksums
383
+ * @returns {{ ok: boolean, verified: any[], failed: any[] }}
384
+ */
385
+ export function verifyAllArtifacts(stagingDir, manifest) {
386
+ const verified = [];
387
+ const failed = [];
388
+
389
+ for (const [filename, expectedHash] of Object.entries(manifest.checksums)) {
390
+ try {
391
+ const filePath = join(stagingDir, filename);
392
+ if (!existsSync(filePath)) {
393
+ failed.push({
394
+ name: filename,
395
+ reason: 'File not found',
396
+ });
397
+ continue;
398
+ }
399
+
400
+ const content = readFileSync(filePath, 'utf-8');
401
+ const actualHash = createHash('sha256').update(content).digest('hex');
402
+
403
+ if (actualHash === expectedHash) {
404
+ verified.push({ name: filename });
405
+ } else {
406
+ failed.push({
407
+ name: filename,
408
+ reason: 'Checksum mismatch',
409
+ expected: expectedHash,
410
+ actual: actualHash,
411
+ });
412
+ }
413
+ } catch (error) {
414
+ failed.push({
415
+ name: filename,
416
+ reason: `Verification failed: ${error.message}`,
417
+ });
418
+ }
419
+ }
420
+
421
+ return {
422
+ ok: failed.length === 0,
423
+ verified,
424
+ failed,
425
+ };
426
+ }
427
+
428
+ /**
429
+ * Atomically commit staging directory to final location
430
+ *
431
+ * Uses atomic rename operations to prevent partial writes.
432
+ *
433
+ * @param {string} artifactDir - Artifact directory
434
+ * @returns {Promise<void>}
435
+ */
436
+ export async function commitStagingDir(artifactDir) {
437
+ const stagingDir = getStagingPath(artifactDir, '').replace(/\/$/, '');
438
+
439
+ if (!existsSync(stagingDir)) {
440
+ throw new Error('Staging directory does not exist');
441
+ }
442
+
443
+ // Get all files in staging
444
+ const files = readdirSync(stagingDir);
445
+
446
+ // Move each file from staging to final location (atomic per-file)
447
+ for (const file of files) {
448
+ const stagingPath = join(stagingDir, file);
449
+ const finalPath = join(artifactDir, file);
450
+
451
+ // Only move actual artifact files, not internal manifest
452
+ if (file === 'integrity.manifest.json') {
453
+ // Manifest stays in staging for verification purposes
454
+ continue;
455
+ }
456
+
457
+ if (existsSync(stagingPath)) {
458
+ // Atomic rename: staging -> final
459
+ renameSync(stagingPath, finalPath);
460
+ }
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Remove poison marker after successful completion
466
+ *
467
+ * @param {string} artifactDir - Artifact directory
468
+ */
469
+ export function removePoisonMarker(artifactDir) {
470
+ const poisonPath = getPoisonMarkerPath(artifactDir);
471
+ try {
472
+ if (existsSync(poisonPath)) {
473
+ unlinkSync(poisonPath);
474
+ }
475
+ } catch (error) {
476
+ // Ignore errors removing poison marker
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Discover artifacts in a directory
482
+ *
483
+ * @param {string} dir - Directory to scan
484
+ * @returns {string[]} Array of artifact filenames
485
+ */
486
+ export function discoverArtifacts(dir) {
487
+ if (!existsSync(dir)) {
488
+ return [];
489
+ }
490
+
491
+ try {
492
+ return readdirSync(dir).filter(f => f.endsWith('.json'));
493
+ } catch (error) {
494
+ return [];
495
+ }
496
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Phase 6A Integration Wrapper for Run Command
3
+ *
4
+ * Wraps the run command to add:
5
+ * - Poison markers to detect incomplete runs
6
+ * - Artifact staging for atomic writes
7
+ * - Integrity verification
8
+ * - Atomic commits
9
+ * - Rollback on failure
10
+ */
11
+
12
+ import { initPhase6A, completePhase6A, rollbackPhase6A, checkPoisonMarker, getStagingPath, redirectArtifactWrites } from './trust-activation-integration.js';
13
+
14
+ /**
15
+ * Wrap run execution with Phase 6A artifact management
16
+ *
17
+ * @param {Function} runFn - Async function that executes the run
18
+ * @param {string} artifactDir - Artifact directory for run
19
+ * @returns {Promise<any>} Result from runFn
20
+ */
21
+ export async function withPhase6A(runFn, artifactDir) {
22
+ // Check for incomplete previous run
23
+ const poisonCheck = checkPoisonMarker(artifactDir);
24
+ if (poisonCheck.hasPoisonMarker) {
25
+ console.warn('⚠️ WARNING: Incomplete previous run detected (poison marker present)');
26
+ console.warn(' This run may be building on corrupted or incomplete artifacts');
27
+ }
28
+
29
+ // Initialize Phase 6A
30
+ const initResult = await initPhase6A(artifactDir);
31
+ if (!initResult.success) {
32
+ throw new Error(`Phase 6A initialization failed: ${initResult.error.message}`);
33
+ }
34
+
35
+ try {
36
+ // Execute the run function with artifact staging
37
+ const result = await runFn();
38
+
39
+ // Complete Phase 6A on success
40
+ const completeResult = await completePhase6A(artifactDir);
41
+ if (!completeResult.success) {
42
+ throw new Error(`Phase 6A completion failed: ${completeResult.error.message}`);
43
+ }
44
+
45
+ return {
46
+ ...result,
47
+ phase6a: {
48
+ success: true,
49
+ verification: completeResult.verification,
50
+ },
51
+ };
52
+ } catch (error) {
53
+ // Rollback on error
54
+ const rollbackResult = await rollbackPhase6A(artifactDir, error);
55
+ if (!rollbackResult.success) {
56
+ console.error(`Phase 6A rollback failed: ${rollbackResult.error.message}`);
57
+ }
58
+
59
+ // Re-throw the original error
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Create a path redirector for artifact writes
66
+ *
67
+ * This function returns a redirector that can be passed to artifact writers
68
+ * to automatically route writes to staging.
69
+ *
70
+ * @param {string} artifactDir - Artifact directory
71
+ * @returns {Function} Redirector function
72
+ */
73
+ export function createArtifactPathRedirector(artifactDir) {
74
+ return (filename) => redirectArtifactWrites(artifactDir, filename);
75
+ }
76
+
77
+ /**
78
+ * Get staging directory path for a run
79
+ *
80
+ * @param {string} runDir - Run directory (e.g., .verax/runs/<runId>)
81
+ * @returns {string} Staging directory path
82
+ */
83
+ export function getStagingDirectory(runDir) {
84
+ return getStagingPath(runDir, '').replace(/\/$/, '');
85
+ }