@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
@@ -3,7 +3,8 @@ import { existsSync, readFileSync } from 'fs';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { dirname } from 'path';
5
5
  import inquirer from 'inquirer';
6
- import { UsageError, DataError, CrashError } from '../util/errors.js';
6
+ import { assertExecutionBootstrapAllowed } from '../util/bootstrap-guard.js';
7
+ import { DataError } from '../util/errors.js';
7
8
  import { generateRunId } from '../util/run-id.js';
8
9
  import { getRunPaths, ensureRunDirectories } from '../util/paths.js';
9
10
  import { atomicWriteJson, atomicWriteText } from '../util/atomic-write.js';
@@ -18,6 +19,8 @@ import { writeObserveJson } from '../util/observe-writer.js';
18
19
  import { detectFindings } from '../util/detection-engine.js';
19
20
  import { writeFindingsJson } from '../util/findings-writer.js';
20
21
  import { writeSummaryJson } from '../util/summary-writer.js';
22
+ import { computeRuntimeBudget, withTimeout } from '../util/runtime-budget.js';
23
+ import { assertHasLocalSource } from '../util/source-requirement.js';
21
24
 
22
25
  const __filename = fileURLToPath(import.meta.url);
23
26
  const __dirname = dirname(__filename);
@@ -28,7 +31,7 @@ function getVersion() {
28
31
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
29
32
  return pkg.version;
30
33
  } catch {
31
- return '0.2.0';
34
+ return '0.3.0';
32
35
  }
33
36
  }
34
37
 
@@ -43,6 +46,8 @@ export async function defaultCommand(options = {}) {
43
46
  url = null,
44
47
  json = false,
45
48
  verbose = false,
49
+ determinism = false,
50
+ determinismRuns = 2,
46
51
  } = options;
47
52
 
48
53
  const projectRoot = resolve(process.cwd());
@@ -52,6 +57,9 @@ export async function defaultCommand(options = {}) {
52
57
  if (!existsSync(srcPath)) {
53
58
  throw new DataError(`Source directory not found: ${srcPath}`);
54
59
  }
60
+
61
+ // Enforce local source availability (no URL-only scans)
62
+ assertHasLocalSource(srcPath);
55
63
 
56
64
  // Create event emitter
57
65
  const events = new RunEventEmitter();
@@ -72,6 +80,83 @@ export async function defaultCommand(options = {}) {
72
80
  });
73
81
  }
74
82
 
83
+ let runId = null;
84
+ /** @type {ReturnType<typeof getRunPaths> | null} */
85
+ let paths = null;
86
+ let startedAt = null;
87
+ let watchdogTimer = null;
88
+ let budget = null;
89
+ let timedOut = false;
90
+
91
+ // Graceful finalization function
92
+ const finalizeOnTimeout = async (reason) => {
93
+ if (timedOut) return; // Prevent double finalization
94
+ timedOut = true;
95
+
96
+ events.stopHeartbeat();
97
+
98
+ // TypeScript narrowing: paths is guaranteed to be non-null here due to control flow
99
+ if (paths && runId && startedAt) {
100
+ try {
101
+ const failedAt = new Date().toISOString();
102
+ atomicWriteJson(paths.runStatusJson, {
103
+ contractVersion: 1,
104
+ artifactVersions: paths.artifactVersions,
105
+ status: 'FAILED',
106
+ runId,
107
+ startedAt,
108
+ failedAt,
109
+ error: reason,
110
+ });
111
+
112
+ atomicWriteJson(paths.runMetaJson, {
113
+ contractVersion: 1,
114
+ artifactVersions: paths.artifactVersions,
115
+ veraxVersion: getVersion(),
116
+ nodeVersion: process.version,
117
+ platform: process.platform,
118
+ cwd: projectRoot,
119
+ command: 'default',
120
+ args: { url: url || null, src },
121
+ url: url || null,
122
+ src: srcPath,
123
+ startedAt,
124
+ completedAt: failedAt,
125
+ error: reason,
126
+ });
127
+
128
+ try {
129
+ writeSummaryJson(paths.summaryJson, {
130
+ runId,
131
+ status: 'FAILED',
132
+ startedAt,
133
+ completedAt: failedAt,
134
+ command: 'default',
135
+ url: url || null,
136
+ notes: `Run timed out: ${reason}`,
137
+ }, {
138
+ expectationsTotal: 0,
139
+ attempted: 0,
140
+ observed: 0,
141
+ silentFailures: 0,
142
+ coverageGaps: 0,
143
+ unproven: 0,
144
+ informational: 0,
145
+ });
146
+ } catch (summaryError) {
147
+ // Ignore summary write errors during timeout handling
148
+ }
149
+ } catch (statusError) {
150
+ // Ignore errors when writing failure status
151
+ }
152
+ }
153
+
154
+ events.emit('error', {
155
+ message: reason,
156
+ type: 'timeout',
157
+ });
158
+ };
159
+
75
160
  try {
76
161
  events.emit('phase:started', {
77
162
  phase: 'Detect Project',
@@ -155,6 +240,9 @@ export async function defaultCommand(options = {}) {
155
240
  console.log(''); // blank line
156
241
  }
157
242
 
243
+ // PHASE 21.6.1: Runtime guard - crash if called during inspection
244
+ assertExecutionBootstrapAllowed('inquirer.prompt');
245
+
158
246
  const answer = await inquirer.prompt([
159
247
  {
160
248
  type: 'input',
@@ -201,12 +289,16 @@ export async function defaultCommand(options = {}) {
201
289
  let startedAt = now.toISOString();
202
290
 
203
291
  atomicWriteJson(paths.runStatusJson, {
292
+ contractVersion: 1,
293
+ artifactVersions: paths.artifactVersions,
204
294
  status: 'RUNNING',
205
295
  runId,
206
296
  startedAt,
207
297
  });
208
298
 
209
299
  atomicWriteJson(paths.runMetaJson, {
300
+ contractVersion: 1,
301
+ artifactVersions: paths.artifactVersions,
210
302
  veraxVersion: getVersion(),
211
303
  nodeVersion: process.version,
212
304
  platform: process.platform,
@@ -231,8 +323,17 @@ export async function defaultCommand(options = {}) {
231
323
  message: 'Analyzing project structure...',
232
324
  });
233
325
 
234
- // Extract expectations
235
- const { expectations, skipped } = await extractExpectations(projectProfile, projectProfile.sourceRoot);
326
+ events.startHeartbeat('Learn', json);
327
+
328
+ let expectations, skipped;
329
+ try {
330
+ // Extract expectations
331
+ const result = await extractExpectations(projectProfile, projectProfile.sourceRoot);
332
+ expectations = result.expectations;
333
+ skipped = result.skipped;
334
+ } finally {
335
+ events.stopHeartbeat();
336
+ }
236
337
 
237
338
  if (!json) {
238
339
  console.log(`Found ${expectations.length} expectations`);
@@ -256,55 +357,109 @@ export async function defaultCommand(options = {}) {
256
357
  });
257
358
  }
258
359
 
360
+ // Compute runtime budget based on expectations count
361
+ budget = computeRuntimeBudget({
362
+ expectationsCount: expectations.length,
363
+ mode: 'default',
364
+ framework: projectProfile.framework,
365
+ fileCount: projectProfile.fileCount || expectations.length,
366
+ });
367
+
368
+ // Set up global watchdog timer
369
+ watchdogTimer = setTimeout(async () => {
370
+ await finalizeOnTimeout(`Global timeout exceeded: ${budget.totalMaxMs}ms`);
371
+ // Exit with code 0 (tool executed, just timed out)
372
+ process.exit(0);
373
+ }, budget.totalMaxMs);
374
+
375
+ // Wrap Learn phase with timeout
376
+ try {
377
+ await withTimeout(
378
+ budget.learnMaxMs,
379
+ Promise.resolve(), // Learn phase already completed
380
+ 'Learn'
381
+ );
382
+ } catch (error) {
383
+ if (error.message.includes('timeout')) {
384
+ await finalizeOnTimeout(`Learn phase timeout: ${budget.learnMaxMs}ms`);
385
+ process.exit(0);
386
+ }
387
+ throw error;
388
+ }
389
+
259
390
  events.emit('phase:completed', {
260
391
  phase: 'Learn',
261
392
  message: 'Project analysis complete',
262
393
  });
263
394
 
264
- // Observe phase
395
+ // Observe phase with timeout
265
396
  events.emit('phase:started', {
266
397
  phase: 'Observe',
267
398
  message: 'Launching browser and observing expectations...',
268
399
  });
269
400
 
401
+ events.startHeartbeat('Observe', json);
402
+
270
403
  let observeData = null;
271
- if (expectations.length > 0) {
272
- try {
273
- observeData = await observeExpectations(
274
- expectations,
275
- resolvedUrl,
276
- paths.evidenceDir,
277
- (progress) => {
278
- events.emit(progress.event, progress);
279
- if (!json && progress.event === 'observe:result') {
280
- const status = progress.observed ? '✓' : '✗';
281
- console.log(` ${status} ${progress.index}/${expectations.length}`);
404
+ try {
405
+ if (expectations.length > 0) {
406
+ try {
407
+ observeData = await withTimeout(
408
+ budget.observeMaxMs,
409
+ observeExpectations(
410
+ expectations,
411
+ resolvedUrl,
412
+ paths.evidenceDir,
413
+ (progress) => {
414
+ events.emit(progress.event, progress);
415
+ if (!json && progress.event === 'observe:result') {
416
+ const status = progress.observed ? '✓' : '✗';
417
+ console.log(` ${status} ${progress.index}/${expectations.length}`);
418
+ }
419
+ }
420
+ ),
421
+ 'Observe'
422
+ );
423
+
424
+ if (!json) {
425
+ console.log(`Observed: ${observeData.stats.observed}/${expectations.length}`);
426
+ }
427
+ } catch (error) {
428
+ if (error.message.includes('timeout')) {
429
+ if (!json) {
430
+ console.error(`Observe error: timeout after ${budget.observeMaxMs}ms`);
282
431
  }
432
+ events.emit('observe:error', {
433
+ message: `Observe phase timeout: ${budget.observeMaxMs}ms`,
434
+ });
435
+ observeData = {
436
+ observations: [],
437
+ stats: { attempted: 0, observed: 0, notObserved: 0 },
438
+ observedAt: new Date().toISOString(),
439
+ };
440
+ } else {
441
+ if (!json) {
442
+ console.error(`Observe error: ${error.message}`);
443
+ }
444
+ events.emit('observe:error', {
445
+ message: error.message,
446
+ });
447
+ observeData = {
448
+ observations: [],
449
+ stats: { attempted: 0, observed: 0, notObserved: 0 },
450
+ observedAt: new Date().toISOString(),
451
+ };
283
452
  }
284
- );
285
-
286
- if (!json) {
287
- console.log(`Observed: ${observeData.stats.observed}/${expectations.length}`);
288
- }
289
- } catch (error) {
290
- if (!json) {
291
- console.error(`Observe error: ${error.message}`);
292
453
  }
293
- events.emit('observe:error', {
294
- message: error.message,
295
- });
454
+ } else {
296
455
  observeData = {
297
456
  observations: [],
298
457
  stats: { attempted: 0, observed: 0, notObserved: 0 },
299
458
  observedAt: new Date().toISOString(),
300
459
  };
301
460
  }
302
- } else {
303
- observeData = {
304
- observations: [],
305
- stats: { attempted: 0, observed: 0, notObserved: 0 },
306
- observedAt: new Date().toISOString(),
307
- };
461
+ } finally {
462
+ events.stopHeartbeat();
308
463
  }
309
464
 
310
465
  events.emit('phase:completed', {
@@ -312,47 +467,71 @@ export async function defaultCommand(options = {}) {
312
467
  message: 'Browser observation complete',
313
468
  });
314
469
 
315
- // Detect phase
470
+ // Detect phase with timeout
316
471
  events.emit('phase:started', {
317
472
  phase: 'Detect',
318
473
  message: 'Analyzing findings and detecting silent failures...',
319
474
  });
320
475
 
476
+ events.startHeartbeat('Detect', json);
477
+
321
478
  // Load learn and observe data for detection
322
479
  let learnData = { expectations: [] };
323
480
  let detectData = null;
324
481
 
325
482
  try {
326
- learnData = {
327
- expectations,
328
- skipped,
329
- };
330
-
331
- detectData = await detectFindings(learnData, observeData, projectRoot, (progress) => {
332
- events.emit(progress.event, progress);
333
- if (!json && progress.event === 'detect:classified') {
334
- const symbol = progress.classification === 'silent-failure' ? '✗' :
335
- progress.classification === 'observed' ? '✓' :
336
- progress.classification === 'coverage-gap' ? '⊘' : '⚠';
337
- console.log(` ${symbol} ${progress.index}/${learnData.expectations.length}`);
483
+ try {
484
+ learnData = {
485
+ expectations,
486
+ skipped,
487
+ };
488
+
489
+ detectData = await withTimeout(
490
+ budget.detectMaxMs,
491
+ detectFindings(learnData, observeData, projectRoot, (progress) => {
492
+ events.emit(progress.event, progress);
493
+ if (!json && progress.event === 'detect:classified') {
494
+ const symbol = progress.classification === 'silent-failure' ? '✗' :
495
+ progress.classification === 'observed' ? '✓' :
496
+ progress.classification === 'coverage-gap' ? '⊘' : '⚠';
497
+ console.log(` ${symbol} ${progress.index}/${learnData.expectations.length}`);
498
+ }
499
+ }),
500
+ 'Detect'
501
+ );
502
+
503
+ if (!json && detectData.stats.silentFailures > 0) {
504
+ console.log(`Silent failures detected: ${detectData.stats.silentFailures}`);
505
+ }
506
+ } catch (error) {
507
+ if (error.message.includes('timeout')) {
508
+ if (!json) {
509
+ console.error(`Detect error: timeout after ${budget.detectMaxMs}ms`);
510
+ }
511
+ events.emit('detect:error', {
512
+ message: `Detect phase timeout: ${budget.detectMaxMs}ms`,
513
+ });
514
+ detectData = {
515
+ findings: [],
516
+ stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
517
+ detectedAt: new Date().toISOString(),
518
+ };
519
+ } else {
520
+ if (!json) {
521
+ console.error(`Detect error: ${error.message}`);
522
+ }
523
+ events.emit('detect:error', {
524
+ message: error.message,
525
+ });
526
+ detectData = {
527
+ findings: [],
528
+ stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
529
+ detectedAt: new Date().toISOString(),
530
+ };
338
531
  }
339
- });
340
-
341
- if (!json && detectData.stats.silentFailures > 0) {
342
- console.log(`Silent failures detected: ${detectData.stats.silentFailures}`);
343
- }
344
- } catch (error) {
345
- if (!json) {
346
- console.error(`Detect error: ${error.message}`);
347
532
  }
348
- events.emit('detect:error', {
349
- message: error.message,
350
- });
351
- detectData = {
352
- findings: [],
353
- stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
354
- detectedAt: new Date().toISOString(),
355
- };
533
+ } finally {
534
+ events.stopHeartbeat();
356
535
  }
357
536
 
358
537
  events.emit('phase:completed', {
@@ -360,35 +539,25 @@ export async function defaultCommand(options = {}) {
360
539
  message: 'Silent failure detection complete',
361
540
  });
362
541
 
542
+ // Clear watchdog timer on successful completion
543
+ if (watchdogTimer) {
544
+ clearTimeout(watchdogTimer);
545
+ watchdogTimer = null;
546
+ }
547
+
363
548
  // Finalize Artifacts
364
549
  events.emit('phase:started', {
365
550
  phase: 'Finalize Artifacts',
366
551
  message: 'Writing run results...',
367
552
  });
368
553
 
369
- const completedAt = new Date().toISOString();
370
-
371
- atomicWriteJson(paths.runStatusJson, {
372
- status: 'COMPLETE',
373
- runId,
374
- startedAt,
375
- completedAt,
376
- });
554
+ events.stopHeartbeat();
377
555
 
378
- atomicWriteJson(paths.runMetaJson, {
379
- veraxVersion: getVersion(),
380
- nodeVersion: process.version,
381
- platform: process.platform,
382
- cwd: projectRoot,
383
- command: 'default',
384
- args: { url: resolvedUrl, src },
385
- url: resolvedUrl,
386
- src: srcPath,
387
- startedAt,
388
- completedAt,
389
- error: null,
390
- });
556
+ const completedAt = new Date().toISOString();
391
557
 
558
+ // Write detect results (or empty if detection failed)
559
+ const findingsResult = writeFindingsJson(paths.baseDir, detectData);
560
+
392
561
  // Write summary with stable digest
393
562
  writeSummaryJson(paths.summaryJson, {
394
563
  runId,
@@ -408,33 +577,11 @@ export async function defaultCommand(options = {}) {
408
577
  informational: detectData.stats?.informational || 0,
409
578
  });
410
579
 
411
- // Write detect results (or empty if detection failed)
412
- writeFindingsJson(paths.baseDir, detectData);
413
-
414
- const traces = [
415
- {
416
- type: 'phase:started',
417
- timestamp: startedAt,
418
- phase: 'Detect Project',
419
- },
420
- {
421
- type: 'phase:completed',
422
- timestamp: new Date().toISOString(),
423
- phase: 'Detect Project',
424
- },
425
- {
426
- type: 'phase:started',
427
- timestamp: new Date().toISOString(),
428
- phase: 'Learn',
429
- },
430
- {
431
- type: 'phase:completed',
432
- timestamp: completedAt,
433
- phase: 'Learn',
434
- },
435
- ];
436
-
437
- const tracesContent = traces.map(t => JSON.stringify(t)).join('\n') + '\n';
580
+ // Write traces (include all events including heartbeats)
581
+ const allEvents = events.getEvents();
582
+ const tracesContent = allEvents
583
+ .map(e => JSON.stringify(e))
584
+ .join('\n') + '\n';
438
585
  atomicWriteText(paths.tracesJsonl, tracesContent);
439
586
 
440
587
  // Write project profile
@@ -445,6 +592,53 @@ export async function defaultCommand(options = {}) {
445
592
 
446
593
  // Write observe results
447
594
  writeObserveJson(paths.baseDir, observeData);
595
+
596
+ // PHASE 6: Verify artifacts after all writers finish
597
+ const { verifyRun } = await import('../../verax/core/artifacts/verifier.js');
598
+ const verification = verifyRun(paths.baseDir, paths.artifactVersions);
599
+
600
+ // Determine final status based on verification
601
+ let finalStatus = 'COMPLETE';
602
+ if (!verification.ok) {
603
+ finalStatus = 'INVALID';
604
+ } else if (verification.warnings.length > 0) {
605
+ finalStatus = 'VALID_WITH_WARNINGS';
606
+ }
607
+
608
+ // Write completed status with contract + enforcement snapshot + verification
609
+ atomicWriteJson(paths.runStatusJson, {
610
+ contractVersion: 1,
611
+ artifactVersions: paths.artifactVersions,
612
+ status: finalStatus,
613
+ runId,
614
+ startedAt,
615
+ completedAt,
616
+ enforcement: findingsResult?.payload?.enforcement || null,
617
+ verification: {
618
+ ok: verification.ok,
619
+ status: finalStatus,
620
+ errorsCount: verification.errors.length,
621
+ warningsCount: verification.warnings.length,
622
+ verifiedAt: verification.verifiedAt
623
+ }
624
+ });
625
+
626
+ // Update metadata with completion time
627
+ atomicWriteJson(paths.runMetaJson, {
628
+ contractVersion: 1,
629
+ artifactVersions: paths.artifactVersions,
630
+ veraxVersion: getVersion(),
631
+ nodeVersion: process.version,
632
+ platform: process.platform,
633
+ cwd: projectRoot,
634
+ command: 'default',
635
+ args: { url: resolvedUrl, src },
636
+ url: resolvedUrl,
637
+ src: srcPath,
638
+ startedAt,
639
+ completedAt,
640
+ error: null,
641
+ });
448
642
 
449
643
  events.emit('phase:completed', {
450
644
  phase: 'Finalize Artifacts',
@@ -456,15 +650,31 @@ export async function defaultCommand(options = {}) {
456
650
  console.log('\nRun complete.');
457
651
  console.log(`Run ID: ${runId}`);
458
652
  console.log(`Artifacts: ${paths.baseDir}`);
653
+
654
+ // PHASE 6: Display verification results
655
+ const { formatVerificationOutput } = await import('../../verax/core/artifacts/verifier.js');
656
+ const verificationOutput = formatVerificationOutput(verification, verbose);
657
+ console.log('');
658
+ console.log(verificationOutput);
459
659
  }
460
660
 
461
661
  return { runId, paths, url: resolvedUrl, success: true };
462
662
  } catch (error) {
663
+ // Clear watchdog timer on error
664
+ if (watchdogTimer) {
665
+ clearTimeout(watchdogTimer);
666
+ watchdogTimer = null;
667
+ }
668
+
669
+ events.stopHeartbeat();
670
+
463
671
  // Mark run as FAILED if we have paths
464
- if (paths && runId && startedAt) {
672
+ if (paths && runId && startedAt && typeof paths === 'object') {
465
673
  try {
466
674
  const failedAt = new Date().toISOString();
467
675
  atomicWriteJson(paths.runStatusJson, {
676
+ contractVersion: 1,
677
+ artifactVersions: paths.artifactVersions,
468
678
  status: 'FAILED',
469
679
  runId,
470
680
  startedAt,
@@ -474,6 +684,8 @@ export async function defaultCommand(options = {}) {
474
684
 
475
685
  // Update metadata
476
686
  atomicWriteJson(paths.runMetaJson, {
687
+ contractVersion: 1,
688
+ artifactVersions: paths.artifactVersions,
477
689
  veraxVersion: getVersion(),
478
690
  nodeVersion: process.version,
479
691
  platform: process.platform,
@@ -92,11 +92,26 @@ export async function doctorCommand(options = {}) {
92
92
 
93
93
  // 4) Headless launch smoke test
94
94
  if (playwright && playwright.chromium && chromiumPath && existsSync(chromiumPath)) {
95
+ /** @type {any} */
96
+ let browser = null;
95
97
  try {
96
- const browser = await playwright.chromium.launch({ headless: true });
97
- const page = await browser.newPage();
98
- await page.goto('about:blank');
99
- await browser.close();
98
+ // Configurable timeout via env (default 5000ms, CI can override to 3000ms)
99
+ const smokeTimeoutMs = parseInt(process.env.VERAX_DOCTOR_SMOKE_TIMEOUT_MS || '5000', 10);
100
+
101
+ // Wrap entire smoke test in hard timeout
102
+ const smokeTestPromise = (async () => {
103
+ browser = await playwright.chromium.launch({ headless: true, timeout: smokeTimeoutMs });
104
+ const page = await browser.newPage();
105
+ await page.goto('about:blank', { timeout: smokeTimeoutMs });
106
+ return true;
107
+ })();
108
+
109
+ // Race against timeout
110
+ const timeoutPromise = new Promise((_, reject) => {
111
+ setTimeout(() => reject(new Error('Smoke test timed out')), smokeTimeoutMs);
112
+ });
113
+
114
+ await Promise.race([smokeTestPromise, timeoutPromise]);
100
115
  addCheck('Headless smoke test', 'pass', 'Chromium launched headless and closed successfully');
101
116
  } catch (error) {
102
117
  addCheck(
@@ -107,6 +122,23 @@ export async function doctorCommand(options = {}) {
107
122
  ? 'Try: npx playwright install --with-deps chromium && launch with --no-sandbox in constrained environments'
108
123
  : 'Reinstall playwright: npx playwright install --with-deps chromium'
109
124
  );
125
+ } finally {
126
+ // CRITICAL: Always close browser to prevent hanging processes
127
+ // Bound close operation too (max 2s)
128
+ if (browser) {
129
+ try {
130
+ const closePromise = browser.close();
131
+ const closeTimeout = new Promise((resolve) => setTimeout(resolve, 2000));
132
+ await Promise.race([closePromise, closeTimeout]);
133
+ } catch (closeError) {
134
+ // Ignore close errors - force kill if needed
135
+ try {
136
+ await browser.close();
137
+ } catch {
138
+ // Final attempt failed, process will clean up
139
+ }
140
+ }
141
+ }
110
142
  }
111
143
  } else {
112
144
  addCheck(