@veraxhq/verax 0.2.0 → 0.3.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 (217) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +15 -5
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +323 -111
  6. package/src/cli/commands/doctor.js +36 -4
  7. package/src/cli/commands/ga.js +243 -0
  8. package/src/cli/commands/gates.js +95 -0
  9. package/src/cli/commands/inspect.js +131 -2
  10. package/src/cli/commands/release-check.js +213 -0
  11. package/src/cli/commands/run.js +498 -103
  12. package/src/cli/commands/security-check.js +211 -0
  13. package/src/cli/commands/truth.js +114 -0
  14. package/src/cli/entry.js +305 -68
  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 +546 -0
  20. package/src/cli/util/ast-network-detector.js +603 -0
  21. package/src/cli/util/ast-usestate-detector.js +602 -0
  22. package/src/cli/util/bootstrap-guard.js +86 -0
  23. package/src/cli/util/detection-engine.js +4 -3
  24. package/src/cli/util/determinism-runner.js +123 -0
  25. package/src/cli/util/determinism-writer.js +129 -0
  26. package/src/cli/util/env-url.js +4 -0
  27. package/src/cli/util/events.js +76 -0
  28. package/src/cli/util/expectation-extractor.js +380 -74
  29. package/src/cli/util/findings-writer.js +126 -15
  30. package/src/cli/util/learn-writer.js +3 -1
  31. package/src/cli/util/observation-engine.js +69 -23
  32. package/src/cli/util/observe-writer.js +3 -1
  33. package/src/cli/util/paths.js +6 -14
  34. package/src/cli/util/project-discovery.js +23 -0
  35. package/src/cli/util/project-writer.js +3 -1
  36. package/src/cli/util/redact.js +2 -2
  37. package/src/cli/util/run-resolver.js +64 -0
  38. package/src/cli/util/runtime-budget.js +147 -0
  39. package/src/cli/util/source-requirement.js +55 -0
  40. package/src/cli/util/summary-writer.js +13 -1
  41. package/src/cli/util/svelte-navigation-detector.js +163 -0
  42. package/src/cli/util/svelte-network-detector.js +80 -0
  43. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  44. package/src/cli/util/svelte-state-detector.js +243 -0
  45. package/src/cli/util/vue-navigation-detector.js +177 -0
  46. package/src/cli/util/vue-sfc-extractor.js +162 -0
  47. package/src/cli/util/vue-state-detector.js +215 -0
  48. package/src/types/global.d.ts +28 -0
  49. package/src/types/ts-ast.d.ts +24 -0
  50. package/src/verax/cli/doctor.js +2 -2
  51. package/src/verax/cli/finding-explainer.js +56 -3
  52. package/src/verax/cli/init.js +1 -1
  53. package/src/verax/cli/url-safety.js +12 -2
  54. package/src/verax/cli/wizard.js +13 -2
  55. package/src/verax/core/artifacts/registry.js +154 -0
  56. package/src/verax/core/artifacts/verifier.js +980 -0
  57. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  58. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  59. package/src/verax/core/budget-engine.js +1 -1
  60. package/src/verax/core/capabilities/gates.js +499 -0
  61. package/src/verax/core/capabilities/registry.js +475 -0
  62. package/src/verax/core/confidence/confidence-compute.js +137 -0
  63. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  64. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  65. package/src/verax/core/confidence/confidence-weights.js +44 -0
  66. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  67. package/src/verax/core/confidence/confidence.loader.js +79 -0
  68. package/src/verax/core/confidence/confidence.schema.js +94 -0
  69. package/src/verax/core/confidence-engine-refactor.js +484 -0
  70. package/src/verax/core/confidence-engine.js +486 -0
  71. package/src/verax/core/confidence-engine.js.backup +471 -0
  72. package/src/verax/core/contracts/index.js +29 -0
  73. package/src/verax/core/contracts/types.js +185 -0
  74. package/src/verax/core/contracts/validators.js +381 -0
  75. package/src/verax/core/decision-snapshot.js +31 -4
  76. package/src/verax/core/decisions/decision.trace.js +276 -0
  77. package/src/verax/core/determinism/contract-writer.js +89 -0
  78. package/src/verax/core/determinism/contract.js +139 -0
  79. package/src/verax/core/determinism/diff.js +364 -0
  80. package/src/verax/core/determinism/engine.js +221 -0
  81. package/src/verax/core/determinism/finding-identity.js +148 -0
  82. package/src/verax/core/determinism/normalize.js +438 -0
  83. package/src/verax/core/determinism/report-writer.js +92 -0
  84. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  85. package/src/verax/core/determinism-model.js +35 -6
  86. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  87. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  88. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  89. package/src/verax/core/evidence-builder.js +487 -0
  90. package/src/verax/core/execution-mode-context.js +77 -0
  91. package/src/verax/core/execution-mode-detector.js +190 -0
  92. package/src/verax/core/failures/exit-codes.js +86 -0
  93. package/src/verax/core/failures/failure-summary.js +76 -0
  94. package/src/verax/core/failures/failure.factory.js +225 -0
  95. package/src/verax/core/failures/failure.ledger.js +132 -0
  96. package/src/verax/core/failures/failure.types.js +196 -0
  97. package/src/verax/core/failures/index.js +10 -0
  98. package/src/verax/core/ga/ga-report-writer.js +43 -0
  99. package/src/verax/core/ga/ga.artifact.js +49 -0
  100. package/src/verax/core/ga/ga.contract.js +434 -0
  101. package/src/verax/core/ga/ga.enforcer.js +86 -0
  102. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  103. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  104. package/src/verax/core/guardrails/policy.loader.js +83 -0
  105. package/src/verax/core/guardrails/policy.schema.js +110 -0
  106. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  107. package/src/verax/core/guardrails-engine.js +505 -0
  108. package/src/verax/core/incremental-store.js +15 -7
  109. package/src/verax/core/observe/run-timeline.js +316 -0
  110. package/src/verax/core/perf/perf.contract.js +186 -0
  111. package/src/verax/core/perf/perf.display.js +65 -0
  112. package/src/verax/core/perf/perf.enforcer.js +91 -0
  113. package/src/verax/core/perf/perf.monitor.js +209 -0
  114. package/src/verax/core/perf/perf.report.js +198 -0
  115. package/src/verax/core/pipeline-tracker.js +238 -0
  116. package/src/verax/core/product-definition.js +127 -0
  117. package/src/verax/core/release/provenance.builder.js +271 -0
  118. package/src/verax/core/release/release-report-writer.js +40 -0
  119. package/src/verax/core/release/release.enforcer.js +159 -0
  120. package/src/verax/core/release/reproducibility.check.js +221 -0
  121. package/src/verax/core/release/sbom.builder.js +283 -0
  122. package/src/verax/core/replay-validator.js +4 -4
  123. package/src/verax/core/replay.js +1 -1
  124. package/src/verax/core/report/cross-index.js +192 -0
  125. package/src/verax/core/report/human-summary.js +222 -0
  126. package/src/verax/core/route-intelligence.js +419 -0
  127. package/src/verax/core/security/secrets.scan.js +326 -0
  128. package/src/verax/core/security/security-report.js +50 -0
  129. package/src/verax/core/security/security.enforcer.js +124 -0
  130. package/src/verax/core/security/supplychain.defaults.json +38 -0
  131. package/src/verax/core/security/supplychain.policy.js +326 -0
  132. package/src/verax/core/security/vuln.scan.js +265 -0
  133. package/src/verax/core/silence-impact.js +1 -1
  134. package/src/verax/core/silence-model.js +9 -7
  135. package/src/verax/core/truth/truth.certificate.js +250 -0
  136. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  137. package/src/verax/detect/comparison.js +8 -3
  138. package/src/verax/detect/confidence-engine.js +645 -57
  139. package/src/verax/detect/confidence-helper.js +33 -0
  140. package/src/verax/detect/detection-engine.js +19 -2
  141. package/src/verax/detect/dynamic-route-findings.js +335 -0
  142. package/src/verax/detect/evidence-index.js +15 -65
  143. package/src/verax/detect/expectation-chain-detector.js +417 -0
  144. package/src/verax/detect/expectation-model.js +56 -3
  145. package/src/verax/detect/explanation-helpers.js +1 -1
  146. package/src/verax/detect/finding-detector.js +2 -2
  147. package/src/verax/detect/findings-writer.js +149 -20
  148. package/src/verax/detect/flow-detector.js +4 -4
  149. package/src/verax/detect/index.js +265 -15
  150. package/src/verax/detect/interactive-findings.js +3 -4
  151. package/src/verax/detect/journey-stall-detector.js +558 -0
  152. package/src/verax/detect/route-findings.js +218 -0
  153. package/src/verax/detect/signal-mapper.js +2 -2
  154. package/src/verax/detect/skip-classifier.js +4 -4
  155. package/src/verax/detect/ui-feedback-findings.js +207 -0
  156. package/src/verax/detect/verdict-engine.js +61 -9
  157. package/src/verax/detect/view-switch-correlator.js +242 -0
  158. package/src/verax/flow/flow-engine.js +3 -2
  159. package/src/verax/flow/flow-spec.js +1 -2
  160. package/src/verax/index.js +413 -33
  161. package/src/verax/intel/effect-detector.js +1 -1
  162. package/src/verax/intel/index.js +2 -2
  163. package/src/verax/intel/route-extractor.js +3 -3
  164. package/src/verax/intel/vue-navigation-extractor.js +81 -18
  165. package/src/verax/intel/vue-router-extractor.js +4 -2
  166. package/src/verax/learn/action-contract-extractor.js +684 -66
  167. package/src/verax/learn/ast-contract-extractor.js +53 -1
  168. package/src/verax/learn/index.js +36 -2
  169. package/src/verax/learn/manifest-writer.js +28 -14
  170. package/src/verax/learn/route-extractor.js +1 -1
  171. package/src/verax/learn/route-validator.js +12 -8
  172. package/src/verax/learn/state-extractor.js +1 -1
  173. package/src/verax/learn/static-extractor-navigation.js +1 -1
  174. package/src/verax/learn/static-extractor-validation.js +2 -2
  175. package/src/verax/learn/static-extractor.js +8 -7
  176. package/src/verax/learn/ts-contract-resolver.js +14 -12
  177. package/src/verax/observe/browser.js +22 -3
  178. package/src/verax/observe/console-sensor.js +2 -2
  179. package/src/verax/observe/expectation-executor.js +2 -1
  180. package/src/verax/observe/focus-sensor.js +1 -1
  181. package/src/verax/observe/human-driver.js +29 -10
  182. package/src/verax/observe/index.js +92 -844
  183. package/src/verax/observe/interaction-discovery.js +27 -15
  184. package/src/verax/observe/interaction-runner.js +31 -14
  185. package/src/verax/observe/loading-sensor.js +6 -0
  186. package/src/verax/observe/navigation-sensor.js +1 -1
  187. package/src/verax/observe/observe-context.js +205 -0
  188. package/src/verax/observe/observe-helpers.js +191 -0
  189. package/src/verax/observe/observe-runner.js +226 -0
  190. package/src/verax/observe/observers/budget-observer.js +185 -0
  191. package/src/verax/observe/observers/console-observer.js +102 -0
  192. package/src/verax/observe/observers/coverage-observer.js +107 -0
  193. package/src/verax/observe/observers/interaction-observer.js +471 -0
  194. package/src/verax/observe/observers/navigation-observer.js +132 -0
  195. package/src/verax/observe/observers/network-observer.js +87 -0
  196. package/src/verax/observe/observers/safety-observer.js +82 -0
  197. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  198. package/src/verax/observe/settle.js +1 -0
  199. package/src/verax/observe/state-sensor.js +8 -4
  200. package/src/verax/observe/state-ui-sensor.js +7 -1
  201. package/src/verax/observe/traces-writer.js +27 -16
  202. package/src/verax/observe/ui-feedback-detector.js +742 -0
  203. package/src/verax/observe/ui-signal-sensor.js +155 -2
  204. package/src/verax/scan-summary-writer.js +46 -9
  205. package/src/verax/shared/artifact-manager.js +9 -6
  206. package/src/verax/shared/budget-profiles.js +2 -2
  207. package/src/verax/shared/caching.js +1 -1
  208. package/src/verax/shared/config-loader.js +1 -2
  209. package/src/verax/shared/css-spinner-rules.js +204 -0
  210. package/src/verax/shared/dynamic-route-utils.js +12 -6
  211. package/src/verax/shared/retry-policy.js +1 -6
  212. package/src/verax/shared/root-artifacts.js +1 -1
  213. package/src/verax/shared/view-switch-rules.js +208 -0
  214. package/src/verax/shared/zip-artifacts.js +1 -0
  215. package/src/verax/validate/context-validator.js +1 -1
  216. package/src/verax/observe/index.js.backup +0 -1
  217. package/src/verax/validate/context-validator.js.bak +0 -0
@@ -1,8 +1,8 @@
1
- import { resolve, join } from 'path';
1
+ import { resolve } from 'path';
2
2
  import { existsSync, readFileSync } from 'fs';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { dirname } from 'path';
5
- import { UsageError, DataError, CrashError } from '../util/errors.js';
5
+ import { UsageError, DataError } from '../util/errors.js';
6
6
  import { generateRunId } from '../util/run-id.js';
7
7
  import { getRunPaths, ensureRunDirectories } from '../util/paths.js';
8
8
  import { atomicWriteJson, atomicWriteText } from '../util/atomic-write.js';
@@ -16,6 +16,10 @@ import { writeObserveJson } from '../util/observe-writer.js';
16
16
  import { detectFindings } from '../util/detection-engine.js';
17
17
  import { writeFindingsJson } from '../util/findings-writer.js';
18
18
  import { writeSummaryJson } from '../util/summary-writer.js';
19
+ import { computeRuntimeBudget, withTimeout } from '../util/runtime-budget.js';
20
+ import { assertHasLocalSource } from '../util/source-requirement.js';
21
+ import { runWithDeterminism } from '../util/determinism-runner.js';
22
+ import { runDeterminismCheck } from '../../verax/core/determinism/engine.js';
19
23
 
20
24
  const __filename = fileURLToPath(import.meta.url);
21
25
  const __dirname = dirname(__filename);
@@ -26,7 +30,7 @@ function getVersion() {
26
30
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
27
31
  return pkg.version;
28
32
  } catch {
29
- return '0.2.0';
33
+ return '0.3.0';
30
34
  }
31
35
  }
32
36
 
@@ -35,17 +39,68 @@ function getVersion() {
35
39
  * Strict, non-interactive CLI mode with explicit flags
36
40
  */
37
41
  export async function runCommand(options) {
42
+ return await runCommandInternal(options);
43
+ }
44
+
45
+ /**
46
+ * Internal run command implementation
47
+ */
48
+ async function runCommandInternal(options) {
38
49
  const {
39
50
  url,
51
+ fixture,
40
52
  src = '.',
41
53
  out = '.verax',
42
54
  json = false,
43
55
  verbose = false,
56
+ determinism = false,
57
+ determinismRuns = 2,
44
58
  } = options;
45
59
 
60
+ // PHASE 25: Support fixture mode for determinism
61
+ let resolvedUrl = url;
62
+ let fixtureId = null;
63
+
64
+ if (fixture && !url) {
65
+ // Extract URL from fixture
66
+ const { resolve } = await import('path');
67
+ const { existsSync, readFileSync } = await import('fs');
68
+ const { fileURLToPath } = await import('url');
69
+ const { dirname } = await import('path');
70
+ const __filename = fileURLToPath(import.meta.url);
71
+ const __dirname = dirname(__filename);
72
+
73
+ const fixturePath = resolve(__dirname, '..', '..', '..', 'test', 'fixtures', 'realistic', fixture);
74
+ if (existsSync(fixturePath)) {
75
+ // Try to read package.json or index.html to extract URL
76
+ const packagePath = resolve(fixturePath, 'package.json');
77
+ const indexPath = resolve(fixturePath, 'index.html');
78
+
79
+ if (existsSync(packagePath)) {
80
+ try {
81
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
82
+ if (pkg.verax && pkg.verax.url) {
83
+ resolvedUrl = pkg.verax.url;
84
+ fixtureId = fixture;
85
+ }
86
+ } catch {
87
+ // Ignore parse errors
88
+ }
89
+ }
90
+
91
+ // If no URL found, use default localhost URL for fixture
92
+ if (!resolvedUrl) {
93
+ resolvedUrl = `http://localhost:5173`; // Default Vite dev server
94
+ fixtureId = fixture;
95
+ }
96
+ } else {
97
+ throw new DataError(`Fixture not found: ${fixture}`);
98
+ }
99
+ }
100
+
46
101
  // Validate required arguments
47
- if (!url) {
48
- throw new UsageError('Missing required argument: --url <url>');
102
+ if (!resolvedUrl) {
103
+ throw new UsageError('Missing required argument: --url <url> or --fixture <fixture>');
49
104
  }
50
105
 
51
106
  const projectRoot = resolve(process.cwd());
@@ -55,12 +110,16 @@ export async function runCommand(options) {
55
110
  if (!existsSync(srcPath)) {
56
111
  throw new DataError(`Source directory not found: ${srcPath}`);
57
112
  }
113
+
114
+ // Enforce local source availability (no URL-only scans)
115
+ assertHasLocalSource(srcPath);
58
116
 
59
117
  // Create event emitter
60
118
  const events = new RunEventEmitter();
61
119
 
62
120
  // Setup event handlers
63
121
  if (json) {
122
+ // In JSON mode, emit events as JSONL (one JSON object per line)
64
123
  events.on('*', (event) => {
65
124
  console.log(JSON.stringify(event));
66
125
  });
@@ -75,6 +134,141 @@ export async function runCommand(options) {
75
134
  let runId = null;
76
135
  let paths = null;
77
136
  let startedAt = null;
137
+ let watchdogTimer = null;
138
+ let budget = null;
139
+ let timedOut = false;
140
+
141
+ // Graceful finalization function
142
+ const finalizeOnTimeout = async (reason) => {
143
+ if (timedOut) return; // Prevent double finalization
144
+ timedOut = true;
145
+
146
+ events.stopHeartbeat();
147
+
148
+ if (paths && runId && startedAt) {
149
+ try {
150
+ const failedAt = new Date().toISOString();
151
+ atomicWriteJson(paths.runStatusJson, {
152
+ contractVersion: 1,
153
+ artifactVersions: paths.artifactVersions,
154
+ status: 'FAILED',
155
+ runId,
156
+ startedAt,
157
+ failedAt,
158
+ error: reason,
159
+ });
160
+
161
+ atomicWriteJson(paths.runMetaJson, {
162
+ contractVersion: 1,
163
+ artifactVersions: paths.artifactVersions,
164
+ veraxVersion: getVersion(),
165
+ nodeVersion: process.version,
166
+ platform: process.platform,
167
+ cwd: projectRoot,
168
+ command: 'run',
169
+ args: { url, src, out },
170
+ url,
171
+ src: srcPath,
172
+ startedAt,
173
+ completedAt: failedAt,
174
+ error: reason,
175
+ });
176
+
177
+ try {
178
+ writeSummaryJson(paths.summaryJson, {
179
+ runId,
180
+ status: 'FAILED',
181
+ startedAt,
182
+ completedAt: failedAt,
183
+ command: 'run',
184
+ url: resolvedUrl,
185
+ notes: `Run timed out: ${reason}`,
186
+ }, {
187
+ expectationsTotal: 0,
188
+ attempted: 0,
189
+ observed: 0,
190
+ silentFailures: 0,
191
+ coverageGaps: 0,
192
+ unproven: 0,
193
+ informational: 0,
194
+ });
195
+ } catch (summaryError) {
196
+ // Ignore summary write errors during timeout handling
197
+ }
198
+ } catch (statusError) {
199
+ // Ignore errors when writing failure status
200
+ }
201
+ }
202
+
203
+ events.emit('error', {
204
+ message: reason,
205
+ type: 'timeout',
206
+ });
207
+ };
208
+
209
+ // PHASE 25: If determinism mode, wrap execution
210
+ if (determinism) {
211
+ const scanFn = async (runConfig) => {
212
+ // Execute a single scan run
213
+ const singleRunId = generateRunId();
214
+ const singlePaths = getRunPaths(projectRoot, out, singleRunId);
215
+ ensureRunDirectories(singlePaths);
216
+
217
+ // Execute scan (reuse existing logic but with single run)
218
+ // This is a simplified version - in production, you'd extract the scan logic
219
+ const { scan } = await import('../../verax/index.js');
220
+ const scanResult = await scan(
221
+ projectRoot,
222
+ resolvedUrl,
223
+ null, // manifestPath
224
+ null, // scanBudgetOverride
225
+ {}, // safetyFlags
226
+ singleRunId
227
+ );
228
+
229
+ return {
230
+ runId: singleRunId,
231
+ artifactPaths: {
232
+ findings: singlePaths.findingsJson,
233
+ runStatus: singlePaths.runStatusJson,
234
+ summary: singlePaths.summaryJson,
235
+ learn: singlePaths.learnJson,
236
+ observe: singlePaths.observeJson,
237
+ runDir: singlePaths.baseDir
238
+ }
239
+ };
240
+ };
241
+
242
+ const determinismResult = await runWithDeterminism(scanFn, {
243
+ runs: determinismRuns,
244
+ out,
245
+ url: resolvedUrl,
246
+ fixture: fixtureId,
247
+ src,
248
+ verbose,
249
+ json
250
+ });
251
+
252
+ // PHASE 25: Output determinism report path
253
+ if (!json) {
254
+ console.log(`\nDeterminism check complete.`);
255
+ console.log(`Verdict: ${determinismResult.verdict}`);
256
+ console.log(`Report: ${determinismResult.reportPath}`);
257
+ } else {
258
+ console.log(JSON.stringify({
259
+ type: 'determinism:complete',
260
+ verdict: determinismResult.verdict,
261
+ reportPath: determinismResult.reportPath,
262
+ summary: determinismResult.summary
263
+ }));
264
+ }
265
+
266
+ return {
267
+ success: determinismResult.verdict === 'DETERMINISTIC' || determinismResult.verdict === 'NON_DETERMINISTIC_EXPECTED',
268
+ verdict: determinismResult.verdict,
269
+ reportPath: determinismResult.reportPath
270
+ };
271
+ }
78
272
 
79
273
  try {
80
274
  // Generate run ID
@@ -128,6 +322,8 @@ export async function runCommand(options) {
128
322
  startedAt = now.toISOString();
129
323
 
130
324
  atomicWriteJson(paths.runStatusJson, {
325
+ contractVersion: 1,
326
+ artifactVersions: paths.artifactVersions,
131
327
  status: 'RUNNING',
132
328
  runId,
133
329
  startedAt,
@@ -135,27 +331,69 @@ export async function runCommand(options) {
135
331
 
136
332
  // Write metadata
137
333
  atomicWriteJson(paths.runMetaJson, {
334
+ contractVersion: 1,
335
+ artifactVersions: paths.artifactVersions,
138
336
  veraxVersion: getVersion(),
139
337
  nodeVersion: process.version,
140
338
  platform: process.platform,
141
339
  cwd: projectRoot,
142
340
  command: 'run',
143
- args: { url, src, out },
144
- url,
341
+ args: { url: resolvedUrl, fixture: fixtureId, src, out },
342
+ url: resolvedUrl,
343
+ fixtureId: fixtureId,
145
344
  src: srcPath,
146
345
  startedAt,
147
346
  completedAt: null,
148
347
  error: null,
149
348
  });
150
349
 
151
- // Simulate learning phase (placeholder)
350
+ // Extract expectations first to compute budget
152
351
  events.emit('phase:started', {
153
352
  phase: 'Learn',
154
353
  message: 'Analyzing project structure...',
155
354
  });
156
355
 
157
- // Extract expectations
158
- const { expectations, skipped } = await extractExpectations(projectProfile, projectProfile.sourceRoot);
356
+ events.startHeartbeat('Learn', json);
357
+
358
+ let expectations, skipped;
359
+ try {
360
+ // Extract expectations (quick operation, no timeout needed here)
361
+ const result = await extractExpectations(projectProfile, projectProfile.sourceRoot);
362
+ expectations = result.expectations;
363
+ skipped = result.skipped;
364
+ } finally {
365
+ events.stopHeartbeat();
366
+ }
367
+
368
+ // Compute runtime budget based on expectations count
369
+ budget = computeRuntimeBudget({
370
+ expectationsCount: expectations.length,
371
+ mode: 'run',
372
+ framework: projectProfile.framework,
373
+ fileCount: projectProfile.fileCount || expectations.length,
374
+ });
375
+
376
+ // Set up global watchdog timer
377
+ watchdogTimer = setTimeout(async () => {
378
+ await finalizeOnTimeout(`Global timeout exceeded: ${budget.totalMaxMs}ms`);
379
+ // Exit with code 0 (tool executed, just timed out)
380
+ process.exit(0);
381
+ }, budget.totalMaxMs);
382
+
383
+ // Wrap Learn phase with timeout
384
+ try {
385
+ await withTimeout(
386
+ budget.learnMaxMs,
387
+ Promise.resolve(), // Learn phase already completed
388
+ 'Learn'
389
+ );
390
+ } catch (error) {
391
+ if (error.message.includes('timeout')) {
392
+ await finalizeOnTimeout(`Learn phase timeout: ${budget.learnMaxMs}ms`);
393
+ process.exit(0);
394
+ }
395
+ throw error;
396
+ }
159
397
 
160
398
  // For now, emit a placeholder trace event
161
399
  events.emit('phase:completed', {
@@ -163,39 +401,60 @@ export async function runCommand(options) {
163
401
  message: 'Project analysis complete',
164
402
  });
165
403
 
166
- // Observe phase
404
+ // Observe phase with timeout
167
405
  events.emit('phase:started', {
168
406
  phase: 'Observe',
169
407
  message: 'Launching browser and observing expectations...',
170
408
  });
171
409
 
410
+ events.startHeartbeat('Observe', json);
411
+
172
412
  let observeData = null;
173
- if (expectations.length > 0) {
174
- try {
175
- observeData = await observeExpectations(
176
- expectations,
177
- url,
178
- paths.evidenceDir,
179
- (progress) => {
180
- events.emit(progress.event, progress);
413
+ try {
414
+ if (expectations.length > 0) {
415
+ try {
416
+ observeData = await withTimeout(
417
+ budget.observeMaxMs,
418
+ observeExpectations(
419
+ expectations,
420
+ url,
421
+ paths.evidenceDir,
422
+ (progress) => {
423
+ events.emit(progress.event, progress);
424
+ }
425
+ ),
426
+ 'Observe'
427
+ );
428
+ } catch (error) {
429
+ if (error.message.includes('timeout')) {
430
+ events.emit('observe:error', {
431
+ message: `Observe phase timeout: ${budget.observeMaxMs}ms`,
432
+ });
433
+ observeData = {
434
+ observations: [],
435
+ stats: { attempted: 0, observed: 0, notObserved: 0 },
436
+ observedAt: new Date().toISOString(),
437
+ };
438
+ } else {
439
+ events.emit('observe:error', {
440
+ message: error.message,
441
+ });
442
+ observeData = {
443
+ observations: [],
444
+ stats: { attempted: 0, observed: 0, notObserved: 0 },
445
+ observedAt: new Date().toISOString(),
446
+ };
181
447
  }
182
- );
183
- } catch (error) {
184
- events.emit('observe:error', {
185
- message: error.message,
186
- });
448
+ }
449
+ } else {
187
450
  observeData = {
188
451
  observations: [],
189
452
  stats: { attempted: 0, observed: 0, notObserved: 0 },
190
453
  observedAt: new Date().toISOString(),
191
454
  };
192
455
  }
193
- } else {
194
- observeData = {
195
- observations: [],
196
- stats: { attempted: 0, observed: 0, notObserved: 0 },
197
- observedAt: new Date().toISOString(),
198
- };
456
+ } finally {
457
+ events.stopHeartbeat();
199
458
  }
200
459
 
201
460
  events.emit('phase:completed', {
@@ -203,32 +462,53 @@ export async function runCommand(options) {
203
462
  message: 'Browser observation complete',
204
463
  });
205
464
 
206
- // Detect phase
465
+ // Detect phase with timeout
207
466
  events.emit('phase:started', {
208
467
  phase: 'Detect',
209
468
  message: 'Analyzing findings and detecting silent failures...',
210
469
  });
211
470
 
471
+ events.startHeartbeat('Detect', json);
472
+
212
473
  let detectData = null;
213
474
  try {
214
- // Use already-extracted expectations
215
- const learnData = {
216
- expectations,
217
- skipped,
218
- };
219
-
220
- detectData = await detectFindings(learnData, observeData, projectRoot, (progress) => {
221
- events.emit(progress.event, progress);
222
- });
223
- } catch (error) {
224
- events.emit('detect:error', {
225
- message: error.message,
226
- });
227
- detectData = {
228
- findings: [],
229
- stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
230
- detectedAt: new Date().toISOString(),
231
- };
475
+ try {
476
+ // Use already-extracted expectations
477
+ const learnData = {
478
+ expectations,
479
+ skipped,
480
+ };
481
+
482
+ detectData = await withTimeout(
483
+ budget.detectMaxMs,
484
+ detectFindings(learnData, observeData, projectRoot, (progress) => {
485
+ events.emit(progress.event, progress);
486
+ }),
487
+ 'Detect'
488
+ );
489
+ } catch (error) {
490
+ if (error.message.includes('timeout')) {
491
+ events.emit('detect:error', {
492
+ message: `Detect phase timeout: ${budget.detectMaxMs}ms`,
493
+ });
494
+ detectData = {
495
+ findings: [],
496
+ stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
497
+ detectedAt: new Date().toISOString(),
498
+ };
499
+ } else {
500
+ events.emit('detect:error', {
501
+ message: error.message,
502
+ });
503
+ detectData = {
504
+ findings: [],
505
+ stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
506
+ detectedAt: new Date().toISOString(),
507
+ };
508
+ }
509
+ }
510
+ } finally {
511
+ events.stopHeartbeat();
232
512
  }
233
513
 
234
514
  events.emit('phase:completed', {
@@ -236,37 +516,39 @@ export async function runCommand(options) {
236
516
  message: 'Silent failure detection complete',
237
517
  });
238
518
 
519
+ // Clear watchdog timer on successful completion
520
+ if (watchdogTimer) {
521
+ clearTimeout(watchdogTimer);
522
+ watchdogTimer = null;
523
+ }
524
+
239
525
  // Emit finalize phase
240
526
  events.emit('phase:started', {
241
527
  phase: 'Finalize Artifacts',
242
528
  message: 'Writing run results...',
243
529
  });
244
530
 
245
- const completedAt = new Date().toISOString();
246
-
247
- // Write completed status
248
- atomicWriteJson(paths.runStatusJson, {
249
- status: 'COMPLETE',
250
- runId,
251
- startedAt,
252
- completedAt,
253
- });
531
+ events.stopHeartbeat();
254
532
 
255
- // Update metadata with completion time
256
- atomicWriteJson(paths.runMetaJson, {
257
- veraxVersion: getVersion(),
258
- nodeVersion: process.version,
259
- platform: process.platform,
260
- cwd: projectRoot,
261
- command: 'run',
262
- args: { url, src, out },
263
- url,
264
- src: srcPath,
265
- startedAt,
266
- completedAt,
267
- error: null,
268
- });
533
+ const completedAt = new Date().toISOString();
269
534
 
535
+ const runDurationMs = completedAt && startedAt ? (Date.parse(completedAt) - Date.parse(startedAt)) : 0;
536
+ const metrics = {
537
+ learnMs: observeData?.timings?.learnMs || 0,
538
+ observeMs: observeData?.timings?.observeMs || observeData?.timings?.totalMs || 0,
539
+ detectMs: detectData?.timings?.detectMs || detectData?.timings?.totalMs || 0,
540
+ totalMs: runDurationMs > 0 ? runDurationMs : (budget?.ms || 0)
541
+ };
542
+ const findingsCounts = detectData?.findingsCounts || {
543
+ HIGH: 0,
544
+ MEDIUM: 0,
545
+ LOW: 0,
546
+ UNKNOWN: 0,
547
+ };
548
+
549
+ // Write detect results (or empty if detection failed)
550
+ const findingsResult = writeFindingsJson(paths.baseDir, detectData);
551
+
270
552
  // Write summary with stable digest
271
553
  writeSummaryJson(paths.summaryJson, {
272
554
  runId,
@@ -276,6 +558,8 @@ export async function runCommand(options) {
276
558
  command: 'run',
277
559
  url,
278
560
  notes: 'Run completed successfully',
561
+ metrics,
562
+ findingsCounts,
279
563
  }, {
280
564
  expectationsTotal: expectations.length,
281
565
  attempted: observeData.stats?.attempted || 0,
@@ -284,36 +568,15 @@ export async function runCommand(options) {
284
568
  coverageGaps: detectData.stats?.coverageGaps || 0,
285
569
  unproven: detectData.stats?.unproven || 0,
286
570
  informational: detectData.stats?.informational || 0,
571
+ ...metrics,
572
+ ...findingsCounts,
287
573
  });
288
574
 
289
- // Write detect results (or empty if detection failed)
290
- writeFindingsJson(paths.baseDir, detectData);
291
-
292
- // Write traces (at least phase events)
293
- const traces = [
294
- {
295
- type: 'phase:started',
296
- timestamp: startedAt,
297
- phase: 'Detect Project',
298
- },
299
- {
300
- type: 'phase:completed',
301
- timestamp: new Date().toISOString(),
302
- phase: 'Detect Project',
303
- },
304
- {
305
- type: 'phase:started',
306
- timestamp: new Date().toISOString(),
307
- phase: 'Learn',
308
- },
309
- {
310
- type: 'phase:completed',
311
- timestamp: completedAt,
312
- phase: 'Learn',
313
- },
314
- ];
315
-
316
- const tracesContent = traces.map(t => JSON.stringify(t)).join('\n') + '\n';
575
+ // Write traces (include all events including heartbeats)
576
+ const allEvents = events.getEvents();
577
+ const tracesContent = allEvents
578
+ .map(e => JSON.stringify(e))
579
+ .join('\n') + '\n';
317
580
  atomicWriteText(paths.tracesJsonl, tracesContent);
318
581
 
319
582
  // Write project profile
@@ -324,26 +587,155 @@ export async function runCommand(options) {
324
587
 
325
588
  // Write observe results
326
589
  writeObserveJson(paths.baseDir, observeData);
590
+
591
+ // PHASE 6: Verify artifacts after all writers finish
592
+ const { verifyRun } = await import('../../verax/core/artifacts/verifier.js');
593
+ const verification = verifyRun(paths.baseDir, paths.artifactVersions);
594
+
595
+ // Determine final status based on verification
596
+ let finalStatus = 'COMPLETE';
597
+ if (!verification.ok) {
598
+ finalStatus = 'INVALID';
599
+ } else if (verification.warnings.length > 0) {
600
+ finalStatus = 'VALID_WITH_WARNINGS';
601
+ }
602
+
603
+ // PHASE 21.2: Compute determinism summary for run.status.json
604
+ let determinismSummary = null;
605
+ try {
606
+ const decisionsPath = resolve(paths.baseDir, 'decisions.json');
607
+ if (existsSync(decisionsPath)) {
608
+ const decisions = JSON.parse(readFileSync(decisionsPath, 'utf-8'));
609
+ const { DecisionRecorder } = await import('../../verax/core/determinism-model.js');
610
+ const recorder = DecisionRecorder.fromExport(decisions);
611
+ const { computeDeterminismVerdict } = await import('../../verax/core/determinism/contract.js');
612
+ const verdict = computeDeterminismVerdict(recorder);
613
+
614
+ determinismSummary = {
615
+ verdict: verdict.verdict, // DETERMINISTIC or NON_DETERMINISTIC
616
+ message: verdict.message,
617
+ adaptiveEventsCount: verdict.adaptiveEvents.length
618
+ };
619
+ }
620
+ } catch (error) {
621
+ // Ignore errors - determinism summary is optional
622
+ }
623
+
624
+ // Write completed status with contract + enforcement snapshot + verification + determinism
625
+ atomicWriteJson(paths.runStatusJson, {
626
+ contractVersion: 1,
627
+ artifactVersions: paths.artifactVersions,
628
+ status: finalStatus,
629
+ runId,
630
+ startedAt,
631
+ completedAt,
632
+ enforcement: findingsResult?.payload?.enforcement || null,
633
+ verification: {
634
+ ok: verification.ok,
635
+ status: finalStatus,
636
+ errorsCount: verification.errors.length,
637
+ warningsCount: verification.warnings.length,
638
+ verifiedAt: verification.verifiedAt
639
+ },
640
+ // PHASE 21.2: Determinism summary
641
+ determinismSummary: determinismSummary
642
+ });
643
+
644
+ // Update metadata with completion time
645
+ atomicWriteJson(paths.runMetaJson, {
646
+ contractVersion: 1,
647
+ artifactVersions: paths.artifactVersions,
648
+ veraxVersion: getVersion(),
649
+ nodeVersion: process.version,
650
+ platform: process.platform,
651
+ cwd: projectRoot,
652
+ command: 'run',
653
+ args: { url: resolvedUrl, fixture: fixtureId, src, out },
654
+ url: resolvedUrl,
655
+ fixtureId: fixtureId,
656
+ src: srcPath,
657
+ startedAt,
658
+ completedAt,
659
+ error: null,
660
+ });
327
661
 
328
662
  events.emit('phase:completed', {
329
663
  phase: 'Finalize Artifacts',
330
664
  message: 'Run artifacts written',
331
665
  });
332
666
 
667
+ // Emit final summary event
668
+ if (json) {
669
+ events.emit('run:complete', {
670
+ runId,
671
+ url,
672
+ command: 'run',
673
+ findingsCounts,
674
+ metrics,
675
+ digest: {
676
+ expectationsTotal: expectations.length,
677
+ attempted: observeData.stats?.attempted || 0,
678
+ observed: observeData.stats?.observed || 0,
679
+ silentFailures: detectData.stats?.silentFailures || 0,
680
+ coverageGaps: detectData.stats?.coverageGaps || 0,
681
+ unproven: detectData.stats?.unproven || 0,
682
+ informational: detectData.stats?.informational || 0,
683
+ }
684
+ });
685
+ }
686
+
333
687
  // Print summary if not JSON mode
334
688
  if (!json) {
335
689
  console.log('\nRun complete.');
336
690
  console.log(`Run ID: ${runId}`);
337
691
  console.log(`Artifacts: ${paths.baseDir}`);
692
+
693
+ // PHASE 21.2: Display determinism truth
694
+ if (determinismSummary) {
695
+ console.log('');
696
+ if (determinismSummary.verdict === 'DETERMINISTIC') {
697
+ console.log('Deterministic: YES');
698
+ } else {
699
+ console.log(`Deterministic: NO (${determinismSummary.message})`);
700
+ if (determinismSummary.adaptiveEventsCount > 0) {
701
+ console.log(` Adaptive events detected: ${determinismSummary.adaptiveEventsCount}`);
702
+ }
703
+ }
704
+ }
705
+
706
+ // PHASE 6: Display verification results
707
+ const { formatVerificationOutput } = await import('../../verax/core/artifacts/verifier.js');
708
+ const verificationOutput = formatVerificationOutput(verification, verbose);
709
+ console.log('');
710
+ console.log(verificationOutput);
711
+
712
+ // PHASE 21.9: Display performance metrics
713
+ const { loadPerformanceReport } = await import('../../verax/core/perf/perf.report.js');
714
+ const { formatPerformanceMetrics } = await import('../../verax/core/perf/perf.display.js');
715
+ const perfReport = loadPerformanceReport(projectRoot, runId);
716
+ if (perfReport) {
717
+ console.log('');
718
+ console.log(formatPerformanceMetrics(perfReport));
719
+ }
338
720
  }
339
721
 
340
722
  return { runId, paths, success: true };
341
723
  } catch (error) {
724
+ // Clear watchdog timer on error
725
+ if (watchdogTimer) {
726
+ clearTimeout(watchdogTimer);
727
+ watchdogTimer = null;
728
+ }
729
+
730
+ events.stopHeartbeat();
731
+
342
732
  // Mark run as FAILED if we have paths
343
733
  if (paths && runId && startedAt) {
344
734
  try {
345
735
  const failedAt = new Date().toISOString();
346
736
  atomicWriteJson(paths.runStatusJson, {
737
+ contractVersion: 1,
738
+ artifactVersions: paths.artifactVersions,
347
739
  status: 'FAILED',
348
740
  runId,
349
741
  startedAt,
@@ -353,13 +745,16 @@ export async function runCommand(options) {
353
745
 
354
746
  // Update metadata
355
747
  atomicWriteJson(paths.runMetaJson, {
748
+ contractVersion: 1,
749
+ artifactVersions: paths.artifactVersions,
356
750
  veraxVersion: getVersion(),
357
751
  nodeVersion: process.version,
358
752
  platform: process.platform,
359
753
  cwd: projectRoot,
360
754
  command: 'run',
361
- args: { url, src, out },
362
- url,
755
+ args: { url: resolvedUrl || url, fixture: fixtureId, src, out },
756
+ url: resolvedUrl || url,
757
+ fixtureId: fixtureId,
363
758
  src: srcPath,
364
759
  startedAt,
365
760
  completedAt: failedAt,
@@ -374,7 +769,7 @@ export async function runCommand(options) {
374
769
  startedAt,
375
770
  completedAt: failedAt,
376
771
  command: 'run',
377
- url,
772
+ url: resolvedUrl || url,
378
773
  notes: `Run failed: ${error.message}`,
379
774
  }, {
380
775
  expectationsTotal: 0,