@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,31 +1,142 @@
1
1
  import { atomicWriteJson } from './atomic-write.js';
2
2
  import { resolve } from 'path';
3
3
  import { findingIdFromExpectationId } from './idgen.js';
4
+ import {
5
+ enforceContractsOnFindings,
6
+ FINDING_STATUS,
7
+ CONFIDENCE_LEVEL,
8
+ IMPACT,
9
+ USER_RISK,
10
+ OWNERSHIP,
11
+ } from '../../verax/core/contracts/index.js';
12
+ import { ARTIFACT_REGISTRY, getArtifactVersions } from '../../verax/core/artifacts/registry.js';
4
13
 
5
14
  /**
6
- * Write findings.json artifact with deterministic IDs
15
+ * Write findings.json artifact with deterministic IDs and contract enforcement
7
16
  */
8
17
  export function writeFindingsJson(runDir, findingsData) {
9
- const findingsPath = resolve(runDir, 'findings.json');
10
-
11
- // Add deterministic finding IDs based on expectation IDs
12
- const findingsWithIds = (findingsData.findings || []).map(finding => ({
18
+ const findingsPath = resolve(runDir, ARTIFACT_REGISTRY.findings.filename);
19
+ const normalizedFindings = normalizeFindings(findingsData?.findings || []);
20
+ const enforcement = enforceContractsOnFindings(normalizedFindings);
21
+
22
+ const findingsWithIds = enforcement.valid.map((finding) => ({
13
23
  ...finding,
14
- findingId: findingIdFromExpectationId(finding.id),
24
+ findingId: findingIdFromExpectationId(finding.id || finding.expectationId || ''),
15
25
  }));
16
-
26
+
27
+ const stats = findingsData?.stats || {};
17
28
  const payload = {
29
+ contractVersion: 1,
30
+ artifactVersions: getArtifactVersions(),
18
31
  findings: findingsWithIds,
32
+ total: findingsWithIds.length,
19
33
  stats: {
20
- total: findingsData.stats?.total || 0,
21
- silentFailures: findingsData.stats?.silentFailures || 0,
22
- observed: findingsData.stats?.observed || 0,
23
- coverageGaps: findingsData.stats?.coverageGaps || 0,
24
- unproven: findingsData.stats?.unproven || 0,
25
- informational: findingsData.stats?.informational || 0,
34
+ total: findingsWithIds.length,
35
+ silentFailures: stats.silentFailures || 0,
36
+ observed: stats.observed || 0,
37
+ coverageGaps: stats.coverageGaps || 0,
38
+ unproven: stats.unproven || 0,
39
+ informational: stats.informational || 0,
40
+ },
41
+ detectedAt: findingsData?.detectedAt || new Date().toISOString(),
42
+ enforcement: {
43
+ droppedCount: enforcement.dropped.length,
44
+ downgradedCount: enforcement.downgrades.length,
45
+ downgrades: enforcement.downgrades.map((entry) => ({
46
+ reason: entry.reason,
47
+ originalStatus: entry.original?.status,
48
+ downgradeToStatus: entry.downgraded?.status,
49
+ })),
50
+ dropped: enforcement.dropped.map((entry) => ({
51
+ reason: entry.reason,
52
+ })),
26
53
  },
27
- detectedAt: findingsData.detectedAt || new Date().toISOString(),
28
54
  };
29
-
55
+
30
56
  atomicWriteJson(findingsPath, payload);
57
+ return { path: findingsPath, payload };
58
+ }
59
+
60
+ function normalizeFindings(findings) {
61
+ return findings.map((finding) => normalizeFinding(finding));
62
+ }
63
+
64
+ function normalizeFinding(finding) {
65
+ const status = finding.status || mapClassificationToStatus(finding.classification);
66
+ const evidence = normalizeEvidence(finding.evidence);
67
+ const confidence = normalizeConfidence(finding.confidence);
68
+ const signals = normalizeSignals(finding);
69
+ const interaction = finding.interaction || {
70
+ type: finding.type || 'unknown',
71
+ selector: finding.promise?.selector || finding.promise?.value || finding.source?.file || 'unknown',
72
+ };
73
+
74
+ return {
75
+ ...finding,
76
+ status,
77
+ evidence,
78
+ confidence,
79
+ signals,
80
+ interaction,
81
+ what_happened: finding.what_happened || finding.reason || 'Expectation was exercised during scan.',
82
+ what_was_expected: finding.what_was_expected || finding.promise?.value || 'Expectation derived from source code.',
83
+ what_was_observed: finding.what_was_observed || finding.reason || 'Observation recorded for expectation.',
84
+ why_it_matters: finding.why_it_matters || 'Potential silent failure identified by expectation vs observation.',
85
+ };
86
+ }
87
+
88
+ function mapClassificationToStatus(classification) {
89
+ if (classification === 'observed') return FINDING_STATUS.CONFIRMED;
90
+ if (classification === 'silent-failure') return FINDING_STATUS.SUSPECTED;
91
+ if (classification === 'coverage-gap' || classification === 'unproven') return FINDING_STATUS.SUSPECTED;
92
+ return FINDING_STATUS.INFORMATIONAL;
93
+ }
94
+
95
+ function normalizeConfidence(confidence) {
96
+ if (confidence && typeof confidence === 'object' && confidence.level && confidence.score !== undefined) {
97
+ return confidence;
98
+ }
99
+
100
+ const numeric = typeof confidence === 'number' ? Math.max(0, Math.min(1, confidence)) : 0;
101
+ const score = Math.round(numeric * 100);
102
+ let level = CONFIDENCE_LEVEL.UNPROVEN;
103
+ if (score >= 80) level = CONFIDENCE_LEVEL.HIGH;
104
+ else if (score >= 60) level = CONFIDENCE_LEVEL.MEDIUM;
105
+ else if (score > 0) level = CONFIDENCE_LEVEL.LOW;
106
+
107
+ return { level, score };
108
+ }
109
+
110
+ function normalizeEvidence(evidenceArray) {
111
+ const items = Array.isArray(evidenceArray) ? evidenceArray : [];
112
+ const hasNetworkActivity = items.some((item) => item?.type === 'network-log');
113
+ const hasDomChange = items.some((item) => item?.type === 'screenshot');
114
+
115
+ return {
116
+ type: hasNetworkActivity ? 'network_activity' : undefined,
117
+ hasDomChange,
118
+ hasUrlChange: false,
119
+ hasNetworkActivity,
120
+ hasStateChange: false,
121
+ networkRequests: items.filter((item) => item?.type === 'network-log'),
122
+ before: items.find((item) => item?.type === 'screenshot')?.path,
123
+ after: undefined,
124
+ };
125
+ }
126
+
127
+ function normalizeSignals(finding) {
128
+ if (finding.signals && typeof finding.signals === 'object') {
129
+ return finding.signals;
130
+ }
131
+
132
+ const impact = (finding.impact && IMPACT[finding.impact]) ? finding.impact : IMPACT.MEDIUM;
133
+
134
+ return {
135
+ impact,
136
+ userRisk: USER_RISK.CONFUSES,
137
+ ownership: OWNERSHIP.FRONTEND,
138
+ grouping: {
139
+ expectationType: finding.type || 'unknown',
140
+ },
141
+ };
31
142
  }
@@ -1,18 +1,20 @@
1
1
  import { resolve } from 'path';
2
2
  import { atomicWriteJson } from './atomic-write.js';
3
3
  import { compareExpectations } from './idgen.js';
4
+ import { ARTIFACT_REGISTRY } from '../../verax/core/artifacts/registry.js';
4
5
 
5
6
  /**
6
7
  * Write learn.json artifact
7
8
  * Maintains deterministic ordering for stable output
8
9
  */
9
10
  export function writeLearnJson(runPaths, expectations, skipped) {
10
- const learnJsonPath = resolve(runPaths.baseDir, 'learn.json');
11
+ const learnJsonPath = resolve(runPaths.baseDir, ARTIFACT_REGISTRY.learn.filename);
11
12
 
12
13
  // Sort expectations deterministically for stable output
13
14
  const sortedExpectations = [...expectations].sort(compareExpectations);
14
15
 
15
16
  const learnJson = {
17
+ contractVersion: 1,
16
18
  expectations: sortedExpectations,
17
19
  stats: {
18
20
  totalExpectations: sortedExpectations.length,
@@ -1,6 +1,6 @@
1
1
  import { chromium } from 'playwright';
2
2
  import { writeFileSync, mkdirSync } from 'fs';
3
- import { resolve, join } from 'path';
3
+ import { resolve } from 'path';
4
4
  import { redactHeaders, redactUrl, redactBody, redactConsole, getRedactionCounters } from './redact.js';
5
5
 
6
6
  /**
@@ -60,9 +60,16 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
60
60
  });
61
61
  });
62
62
 
63
- // Navigate to base URL first
63
+ // Navigate to base URL first with explicit timeout
64
64
  try {
65
- await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
65
+ await page.goto(url, {
66
+ waitUntil: 'domcontentloaded', // Use domcontentloaded instead of networkidle for faster timeout
67
+ timeout: 30000
68
+ });
69
+ // Wait for network idle with separate timeout
70
+ await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
71
+ // Network idle timeout is acceptable, continue
72
+ });
66
73
  } catch (error) {
67
74
  // Continue even if initial load fails
68
75
  if (onProgress) {
@@ -96,6 +103,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
96
103
  type: exp.type,
97
104
  promise: exp.promise,
98
105
  source: exp.source,
106
+ attempted: false,
99
107
  observed: false,
100
108
  observedAt: null,
101
109
  evidenceFiles: [],
@@ -107,6 +115,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
107
115
  let evidence = null;
108
116
 
109
117
  if (exp.type === 'navigation') {
118
+ observation.attempted = true; // Mark as attempted
110
119
  result = await observeNavigation(
111
120
  page,
112
121
  exp,
@@ -117,6 +126,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
117
126
  );
118
127
  evidence = result ? `nav_${expNum}_after.png` : null;
119
128
  } else if (exp.type === 'network') {
129
+ observation.attempted = true; // Mark as attempted
120
130
  result = await observeNetwork(page, exp, networkLogs, 5000);
121
131
  if (result) {
122
132
  const evidenceFile = `network_${expNum}.json`;
@@ -135,6 +145,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
135
145
  evidence = null;
136
146
  }
137
147
  } else if (exp.type === 'state') {
148
+ observation.attempted = true; // Mark as attempted
138
149
  result = await observeState(page, exp, evidencePath, expNum);
139
150
  evidence = result ? `state_${expNum}_after.png` : null;
140
151
  }
@@ -187,19 +198,47 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
187
198
  observedAt: new Date().toISOString(),
188
199
  };
189
200
  } finally {
190
- // Clean up
201
+ // Robust cleanup: ensure browser/context/page are closed
202
+ // Remove all event listeners to prevent leaks
191
203
  if (page) {
192
204
  try {
193
- await page.close();
205
+ // Remove all listeners
206
+ page.removeAllListeners();
207
+ // @ts-expect-error - Playwright page.close() doesn't accept timeout option, but we use it for safety
208
+ await page.close({ timeout: 5000 }).catch(() => {});
194
209
  } catch (e) {
195
- // Ignore close errors
210
+ // Ignore close errors but emit warning if onProgress available
211
+ if (onProgress) {
212
+ onProgress({
213
+ event: 'observe:warning',
214
+ message: `Page cleanup warning: ${e.message}`,
215
+ });
216
+ }
196
217
  }
197
218
  }
219
+
220
+ // Close browser context if it exists
198
221
  if (browser) {
199
222
  try {
200
- await browser.close();
223
+ const contexts = browser.contexts();
224
+ for (const context of contexts) {
225
+ try {
226
+ // @ts-expect-error - Playwright context.close() doesn't accept timeout option, but we use it for safety
227
+ await context.close({ timeout: 5000 }).catch(() => {});
228
+ } catch (e) {
229
+ // Ignore context close errors
230
+ }
231
+ }
232
+ // @ts-expect-error - Playwright browser.close() doesn't accept timeout option, but we use it for safety
233
+ await browser.close({ timeout: 5000 }).catch(() => {});
201
234
  } catch (e) {
202
- // Ignore close errors
235
+ // Ignore browser close errors but emit warning if onProgress available
236
+ if (onProgress) {
237
+ onProgress({
238
+ event: 'observe:warning',
239
+ message: `Browser cleanup warning: ${e.message}`,
240
+ });
241
+ }
203
242
  }
204
243
  }
205
244
  }
@@ -242,6 +281,7 @@ async function observeNavigation(page, expectation, baseUrl, visitedUrls, eviden
242
281
  await page.click(`a[href="${element.href}"]`);
243
282
  } catch (e2) {
244
283
  // Try clicking by text content
284
+ // eslint-disable-next-line no-undef
245
285
  const text = await page.evaluate((href) => {
246
286
  const anchors = Array.from(document.querySelectorAll('a'));
247
287
  const found = anchors.find(a => a.getAttribute('href') === href);
@@ -254,14 +294,23 @@ async function observeNavigation(page, expectation, baseUrl, visitedUrls, eviden
254
294
  }
255
295
  }
256
296
 
257
- // Wait for navigation or SPA update
297
+ // Wait for navigation or SPA update with explicit timeout
258
298
  try {
259
- await page.waitForNavigation({ waitUntil: 'networkidle', timeout: 2000 }).catch(() => {});
299
+ await page.waitForNavigation({
300
+ waitUntil: 'domcontentloaded',
301
+ timeout: 5000
302
+ }).catch(() => {
303
+ // Navigation timeout is acceptable for SPAs
304
+ });
305
+ // Wait for network idle with separate timeout
306
+ await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
307
+ // Network idle timeout is acceptable
308
+ });
260
309
  } catch (e) {
261
- // Navigation might not happen
310
+ // Navigation might not happen, continue
262
311
  }
263
312
 
264
- // Wait for potential SPA updates
313
+ // Wait for potential SPA updates (bounded)
265
314
  await page.waitForTimeout(300);
266
315
 
267
316
  // Screenshot after interaction
@@ -304,13 +353,21 @@ async function observeNetwork(page, expectation, networkLogs, timeoutMs) {
304
353
  if (found) {
305
354
  clearInterval(checkTimer);
306
355
  resolve(true);
356
+ return;
307
357
  }
308
358
 
309
359
  if (Date.now() - startTime > timeoutMs) {
310
360
  clearInterval(checkTimer);
311
361
  resolve(false);
362
+ return;
312
363
  }
313
364
  }, 100);
365
+
366
+ // CRITICAL: Unref the interval so it doesn't keep the process alive
367
+ // This allows tests to exit cleanly even if interval is not cleared
368
+ if (checkTimer && checkTimer.unref) {
369
+ checkTimer.unref();
370
+ }
314
371
  });
315
372
  }
316
373
 
@@ -353,14 +410,3 @@ async function observeState(page, expectation, evidencePath, expNum) {
353
410
  }
354
411
  }
355
412
 
356
- /**
357
- * Check if page content changed (for SPA detection)
358
- */
359
- async function checkPageContentChanged(page) {
360
- try {
361
- const bodyText = await page.locator('body').textContent();
362
- return bodyText && bodyText.length > 0;
363
- } catch (error) {
364
- return false;
365
- }
366
- }
@@ -1,13 +1,15 @@
1
1
  import { atomicWriteJson } from './atomic-write.js';
2
2
  import { resolve } from 'path';
3
+ import { ARTIFACT_REGISTRY } from '../../verax/core/artifacts/registry.js';
3
4
 
4
5
  /**
5
6
  * Write observe.json artifact
6
7
  */
7
8
  export function writeObserveJson(runDir, observeData) {
8
- const observePath = resolve(runDir, 'observe.json');
9
+ const observePath = resolve(runDir, ARTIFACT_REGISTRY.observe.filename);
9
10
 
10
11
  const payload = {
12
+ contractVersion: 1,
11
13
  observations: observeData.observations || [],
12
14
  stats: {
13
15
  attempted: observeData.stats?.attempted || 0,
@@ -1,23 +1,15 @@
1
- import { join } from 'path';
1
+ import { join, isAbsolute } from 'path';
2
2
  import { mkdirSync } from 'fs';
3
+ import { buildRunArtifactPaths } from '../../verax/core/artifacts/registry.js';
3
4
 
4
5
  /**
5
6
  * Build run artifact paths
6
7
  */
7
8
  export function getRunPaths(projectRoot, outDir, runId) {
8
- const baseDir = join(projectRoot, outDir, 'runs', runId);
9
-
10
- return {
11
- baseDir,
12
- runStatusJson: join(baseDir, 'run.status.json'),
13
- runMetaJson: join(baseDir, 'run.meta.json'),
14
- summaryJson: join(baseDir, 'summary.json'),
15
- findingsJson: join(baseDir, 'findings.json'),
16
- tracesJsonl: join(baseDir, 'traces.jsonl'),
17
- evidenceDir: join(baseDir, 'evidence'),
18
- learnJson: join(baseDir, 'learn.json'),
19
- observeJson: join(baseDir, 'observe.json'),
20
- };
9
+ const outBase = isAbsolute(outDir) ? outDir : join(projectRoot, outDir);
10
+ const baseDir = join(outBase, 'runs', runId);
11
+
12
+ return buildRunArtifactPaths(baseDir);
21
13
  }
22
14
 
23
15
  /**
@@ -1,12 +1,29 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'fs';
2
2
  import { resolve, dirname } from 'path';
3
+ import { assertExecutionBootstrapAllowed } from './bootstrap-guard.js';
3
4
 
4
5
  /**
5
6
  * Project Discovery Module
6
7
  * Detects framework, router, source root, and dev server configuration
8
+ *
9
+ * @typedef {Object} ProjectProfile
10
+ * @property {string} framework
11
+ * @property {string|null} router
12
+ * @property {string} sourceRoot
13
+ * @property {string} packageManager
14
+ * @property {{dev: string|null, build: string|null, start: string|null}} scripts
15
+ * @property {string} detectedAt
16
+ * @property {string|null} packageJsonPath
17
+ * @property {number} [fileCount] - Optional file count for budget calculation
7
18
  */
8
19
 
20
+ /**
21
+ * @param {string} srcPath
22
+ * @returns {Promise<ProjectProfile>}
23
+ */
9
24
  export async function discoverProject(srcPath) {
25
+ // PHASE 21.6.1: Runtime guard - crash if called during inspection
26
+ assertExecutionBootstrapAllowed('discoverProject');
10
27
  const projectRoot = resolve(srcPath);
11
28
 
12
29
  // Find the nearest package.json
@@ -62,6 +79,12 @@ function findPackageJson(startPath) {
62
79
  return immediatePackage;
63
80
  }
64
81
 
82
+ // For static HTML projects, don't walk up - use the startPath as project root
83
+ // This prevents finding parent package.json files that aren't relevant
84
+ if (hasStaticHtml(currentPath)) {
85
+ return null;
86
+ }
87
+
65
88
  // Then walk up (limit to 5 levels for monorepos, not 10)
66
89
  for (let i = 0; i < 5; i++) {
67
90
  const parentPath = dirname(currentPath);
@@ -1,13 +1,15 @@
1
1
  import { atomicWriteJson } from './atomic-write.js';
2
2
  import { resolve } from 'path';
3
+ import { ARTIFACT_REGISTRY } from '../../verax/core/artifacts/registry.js';
3
4
 
4
5
  /**
5
6
  * Write project profile artifact
6
7
  */
7
8
  export function writeProjectJson(runPaths, projectProfile) {
8
- const projectJsonPath = resolve(runPaths.baseDir, 'project.json');
9
+ const projectJsonPath = resolve(runPaths.baseDir, ARTIFACT_REGISTRY.project.filename);
9
10
 
10
11
  const projectJson = {
12
+ contractVersion: 1,
11
13
  framework: projectProfile.framework,
12
14
  router: projectProfile.router,
13
15
  sourceRoot: projectProfile.sourceRoot,
@@ -105,14 +105,14 @@ export function redactTokensInText(text, counters = { headersRedacted: 0, tokens
105
105
  });
106
106
 
107
107
  // Bearer tokens
108
- output = output.replace(/Bearer\s+([A-Za-z0-9._-]+)/gi, (match, token) => {
108
+ output = output.replace(/Bearer\s+([A-Za-z0-9._-]+)/gi, (_match, _token) => {
109
109
  c.tokensRedacted += 1;
110
110
  return `Bearer ${REDACTED}`;
111
111
  });
112
112
 
113
113
  // JWT-like strings (three base64url-ish segments)
114
114
  // More specific: require uppercase or numbers, not just domain patterns like "api.example.com"
115
- output = output.replace(/[A-Z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, (match) => {
115
+ output = output.replace(/[A-Z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, (_match) => {
116
116
  c.tokensRedacted += 1;
117
117
  return REDACTED;
118
118
  });
@@ -0,0 +1,64 @@
1
+ /**
2
+ * PHASE 21.6.1 — Run Resolver
3
+ *
4
+ * Pure filesystem logic to resolve run IDs.
5
+ * No side effects, no execution dependencies.
6
+ */
7
+
8
+ import { readdirSync, statSync, existsSync } from 'fs';
9
+ import { resolve } from 'path';
10
+
11
+ /**
12
+ * Find the latest run ID from .verax/runs/
13
+ *
14
+ * @param {string} projectDir - Project directory
15
+ * @returns {string|null} Latest run ID or null if no runs found
16
+ */
17
+ export function findLatestRunId(projectDir) {
18
+ const runsDir = resolve(projectDir, '.verax', 'runs');
19
+
20
+ if (!existsSync(runsDir)) {
21
+ return null;
22
+ }
23
+
24
+ try {
25
+ const runs = readdirSync(runsDir, { withFileTypes: true })
26
+ .filter(dirent => dirent.isDirectory())
27
+ .map(dirent => {
28
+ const runPath = resolve(runsDir, dirent.name);
29
+ try {
30
+ const stats = statSync(runPath);
31
+ return {
32
+ name: dirent.name,
33
+ mtimeMs: stats.mtimeMs
34
+ };
35
+ } catch {
36
+ return null;
37
+ }
38
+ })
39
+ .filter(run => run !== null);
40
+
41
+ if (runs.length === 0) {
42
+ return null;
43
+ }
44
+
45
+ // Sort by modification time (descending) and return latest
46
+ runs.sort((a, b) => b.mtimeMs - a.mtimeMs);
47
+ return runs[0].name;
48
+ } catch (error) {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Validate that a run ID exists
55
+ *
56
+ * @param {string} projectDir - Project directory
57
+ * @param {string} runId - Run ID to validate
58
+ * @returns {boolean} Whether run exists
59
+ */
60
+ export function validateRunId(projectDir, runId) {
61
+ const runDir = resolve(projectDir, '.verax', 'runs', runId);
62
+ return existsSync(runDir);
63
+ }
64
+