@veraxhq/verax 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +10 -6
  2. package/bin/verax.js +11 -11
  3. package/package.json +29 -8
  4. package/src/cli/commands/baseline.js +103 -0
  5. package/src/cli/commands/default.js +51 -6
  6. package/src/cli/commands/doctor.js +29 -0
  7. package/src/cli/commands/ga.js +246 -0
  8. package/src/cli/commands/gates.js +95 -0
  9. package/src/cli/commands/inspect.js +4 -2
  10. package/src/cli/commands/release-check.js +215 -0
  11. package/src/cli/commands/run.js +45 -6
  12. package/src/cli/commands/security-check.js +212 -0
  13. package/src/cli/commands/truth.js +113 -0
  14. package/src/cli/entry.js +30 -20
  15. package/src/cli/util/angular-component-extractor.js +179 -0
  16. package/src/cli/util/angular-navigation-detector.js +141 -0
  17. package/src/cli/util/angular-network-detector.js +161 -0
  18. package/src/cli/util/angular-state-detector.js +162 -0
  19. package/src/cli/util/ast-interactive-detector.js +544 -0
  20. package/src/cli/util/ast-network-detector.js +603 -0
  21. package/src/cli/util/ast-promise-extractor.js +581 -0
  22. package/src/cli/util/ast-usestate-detector.js +602 -0
  23. package/src/cli/util/atomic-write.js +12 -1
  24. package/src/cli/util/bootstrap-guard.js +86 -0
  25. package/src/cli/util/console-reporter.js +72 -0
  26. package/src/cli/util/detection-engine.js +105 -41
  27. package/src/cli/util/determinism-runner.js +124 -0
  28. package/src/cli/util/determinism-writer.js +129 -0
  29. package/src/cli/util/digest-engine.js +359 -0
  30. package/src/cli/util/dom-diff.js +226 -0
  31. package/src/cli/util/evidence-engine.js +287 -0
  32. package/src/cli/util/expectation-extractor.js +151 -5
  33. package/src/cli/util/findings-writer.js +3 -0
  34. package/src/cli/util/framework-detector.js +572 -0
  35. package/src/cli/util/idgen.js +1 -1
  36. package/src/cli/util/interaction-planner.js +529 -0
  37. package/src/cli/util/learn-writer.js +2 -0
  38. package/src/cli/util/ledger-writer.js +110 -0
  39. package/src/cli/util/monorepo-resolver.js +162 -0
  40. package/src/cli/util/observation-engine.js +127 -278
  41. package/src/cli/util/observe-writer.js +2 -0
  42. package/src/cli/util/project-discovery.js +284 -0
  43. package/src/cli/util/project-writer.js +2 -0
  44. package/src/cli/util/run-id.js +23 -27
  45. package/src/cli/util/run-resolver.js +64 -0
  46. package/src/cli/util/run-result.js +778 -0
  47. package/src/cli/util/selector-resolver.js +235 -0
  48. package/src/cli/util/source-requirement.js +55 -0
  49. package/src/cli/util/summary-writer.js +2 -0
  50. package/src/cli/util/svelte-navigation-detector.js +163 -0
  51. package/src/cli/util/svelte-network-detector.js +80 -0
  52. package/src/cli/util/svelte-sfc-extractor.js +146 -0
  53. package/src/cli/util/svelte-state-detector.js +242 -0
  54. package/src/cli/util/trust-activation-integration.js +496 -0
  55. package/src/cli/util/trust-activation-wrapper.js +85 -0
  56. package/src/cli/util/trust-integration-hooks.js +164 -0
  57. package/src/cli/util/types.js +153 -0
  58. package/src/cli/util/url-validation.js +40 -0
  59. package/src/cli/util/vue-navigation-detector.js +178 -0
  60. package/src/cli/util/vue-sfc-extractor.js +161 -0
  61. package/src/cli/util/vue-state-detector.js +215 -0
  62. package/src/types/fs-augment.d.ts +23 -0
  63. package/src/types/global.d.ts +137 -0
  64. package/src/types/internal-types.d.ts +35 -0
  65. package/src/verax/cli/init.js +4 -18
  66. package/src/verax/core/action-classifier.js +4 -3
  67. package/src/verax/core/artifacts/registry.js +139 -0
  68. package/src/verax/core/artifacts/verifier.js +990 -0
  69. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  70. package/src/verax/core/baseline/baseline.snapshot.js +233 -0
  71. package/src/verax/core/capabilities/gates.js +505 -0
  72. package/src/verax/core/capabilities/registry.js +475 -0
  73. package/src/verax/core/confidence/confidence-compute.js +144 -0
  74. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  75. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  76. package/src/verax/core/confidence/confidence-weights.js +44 -0
  77. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  78. package/src/verax/core/confidence/confidence.loader.js +80 -0
  79. package/src/verax/core/confidence/confidence.schema.js +94 -0
  80. package/src/verax/core/confidence-engine-refactor.js +489 -0
  81. package/src/verax/core/confidence-engine.js +625 -0
  82. package/src/verax/core/contracts/index.js +29 -0
  83. package/src/verax/core/contracts/types.js +186 -0
  84. package/src/verax/core/contracts/validators.js +456 -0
  85. package/src/verax/core/decisions/decision.trace.js +278 -0
  86. package/src/verax/core/determinism/contract-writer.js +89 -0
  87. package/src/verax/core/determinism/contract.js +139 -0
  88. package/src/verax/core/determinism/diff.js +405 -0
  89. package/src/verax/core/determinism/engine.js +222 -0
  90. package/src/verax/core/determinism/finding-identity.js +149 -0
  91. package/src/verax/core/determinism/normalize.js +466 -0
  92. package/src/verax/core/determinism/report-writer.js +93 -0
  93. package/src/verax/core/determinism/run-fingerprint.js +123 -0
  94. package/src/verax/core/dynamic-route-intelligence.js +529 -0
  95. package/src/verax/core/evidence/evidence-capture-service.js +308 -0
  96. package/src/verax/core/evidence/evidence-intent-ledger.js +166 -0
  97. package/src/verax/core/evidence-builder.js +487 -0
  98. package/src/verax/core/execution-mode-context.js +77 -0
  99. package/src/verax/core/execution-mode-detector.js +192 -0
  100. package/src/verax/core/failures/exit-codes.js +88 -0
  101. package/src/verax/core/failures/failure-summary.js +76 -0
  102. package/src/verax/core/failures/failure.factory.js +225 -0
  103. package/src/verax/core/failures/failure.ledger.js +133 -0
  104. package/src/verax/core/failures/failure.types.js +196 -0
  105. package/src/verax/core/failures/index.js +10 -0
  106. package/src/verax/core/ga/ga-report-writer.js +43 -0
  107. package/src/verax/core/ga/ga.artifact.js +49 -0
  108. package/src/verax/core/ga/ga.contract.js +435 -0
  109. package/src/verax/core/ga/ga.enforcer.js +87 -0
  110. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  111. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  112. package/src/verax/core/guardrails/policy.loader.js +84 -0
  113. package/src/verax/core/guardrails/policy.schema.js +110 -0
  114. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  115. package/src/verax/core/guardrails-engine.js +505 -0
  116. package/src/verax/core/incremental-store.js +1 -0
  117. package/src/verax/core/integrity/budget.js +138 -0
  118. package/src/verax/core/integrity/determinism.js +342 -0
  119. package/src/verax/core/integrity/integrity.js +208 -0
  120. package/src/verax/core/integrity/poisoning.js +108 -0
  121. package/src/verax/core/integrity/transaction.js +140 -0
  122. package/src/verax/core/observe/run-timeline.js +318 -0
  123. package/src/verax/core/perf/perf.contract.js +186 -0
  124. package/src/verax/core/perf/perf.display.js +65 -0
  125. package/src/verax/core/perf/perf.enforcer.js +91 -0
  126. package/src/verax/core/perf/perf.monitor.js +209 -0
  127. package/src/verax/core/perf/perf.report.js +200 -0
  128. package/src/verax/core/pipeline-tracker.js +243 -0
  129. package/src/verax/core/product-definition.js +127 -0
  130. package/src/verax/core/release/provenance.builder.js +130 -0
  131. package/src/verax/core/release/release-report-writer.js +40 -0
  132. package/src/verax/core/release/release.enforcer.js +164 -0
  133. package/src/verax/core/release/reproducibility.check.js +222 -0
  134. package/src/verax/core/release/sbom.builder.js +292 -0
  135. package/src/verax/core/replay-validator.js +2 -0
  136. package/src/verax/core/replay.js +4 -0
  137. package/src/verax/core/report/cross-index.js +195 -0
  138. package/src/verax/core/report/human-summary.js +362 -0
  139. package/src/verax/core/route-intelligence.js +420 -0
  140. package/src/verax/core/run-id.js +6 -3
  141. package/src/verax/core/run-manifest.js +4 -3
  142. package/src/verax/core/security/secrets.scan.js +329 -0
  143. package/src/verax/core/security/security-report.js +50 -0
  144. package/src/verax/core/security/security.enforcer.js +128 -0
  145. package/src/verax/core/security/supplychain.defaults.json +38 -0
  146. package/src/verax/core/security/supplychain.policy.js +334 -0
  147. package/src/verax/core/security/vuln.scan.js +265 -0
  148. package/src/verax/core/truth/truth.certificate.js +252 -0
  149. package/src/verax/core/ui-feedback-intelligence.js +481 -0
  150. package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
  151. package/src/verax/detect/confidence-engine.js +62 -34
  152. package/src/verax/detect/confidence-helper.js +34 -0
  153. package/src/verax/detect/dynamic-route-findings.js +338 -0
  154. package/src/verax/detect/expectation-chain-detector.js +417 -0
  155. package/src/verax/detect/expectation-model.js +2 -2
  156. package/src/verax/detect/failure-cause-inference.js +293 -0
  157. package/src/verax/detect/findings-writer.js +131 -35
  158. package/src/verax/detect/flow-detector.js +2 -2
  159. package/src/verax/detect/form-silent-failure.js +98 -0
  160. package/src/verax/detect/index.js +46 -5
  161. package/src/verax/detect/invariants-enforcer.js +147 -0
  162. package/src/verax/detect/journey-stall-detector.js +558 -0
  163. package/src/verax/detect/navigation-silent-failure.js +82 -0
  164. package/src/verax/detect/problem-aggregator.js +361 -0
  165. package/src/verax/detect/route-findings.js +219 -0
  166. package/src/verax/detect/summary-writer.js +477 -0
  167. package/src/verax/detect/test-failure-cause-inference.js +314 -0
  168. package/src/verax/detect/ui-feedback-findings.js +207 -0
  169. package/src/verax/detect/view-switch-correlator.js +242 -0
  170. package/src/verax/flow/flow-engine.js +2 -1
  171. package/src/verax/flow/flow-spec.js +0 -6
  172. package/src/verax/index.js +4 -0
  173. package/src/verax/intel/ts-program.js +1 -0
  174. package/src/verax/intel/vue-navigation-extractor.js +3 -0
  175. package/src/verax/learn/action-contract-extractor.js +3 -0
  176. package/src/verax/learn/ast-contract-extractor.js +1 -1
  177. package/src/verax/learn/flow-extractor.js +1 -0
  178. package/src/verax/learn/project-detector.js +5 -0
  179. package/src/verax/learn/react-router-extractor.js +2 -0
  180. package/src/verax/learn/source-instrumenter.js +1 -0
  181. package/src/verax/learn/state-extractor.js +2 -1
  182. package/src/verax/learn/static-extractor.js +1 -0
  183. package/src/verax/observe/coverage-gaps.js +132 -0
  184. package/src/verax/observe/expectation-handler.js +126 -0
  185. package/src/verax/observe/incremental-skip.js +46 -0
  186. package/src/verax/observe/index.js +51 -155
  187. package/src/verax/observe/interaction-executor.js +192 -0
  188. package/src/verax/observe/interaction-runner.js +782 -513
  189. package/src/verax/observe/network-firewall.js +86 -0
  190. package/src/verax/observe/observation-builder.js +169 -0
  191. package/src/verax/observe/observe-context.js +205 -0
  192. package/src/verax/observe/observe-helpers.js +192 -0
  193. package/src/verax/observe/observe-runner.js +230 -0
  194. package/src/verax/observe/observers/budget-observer.js +185 -0
  195. package/src/verax/observe/observers/console-observer.js +102 -0
  196. package/src/verax/observe/observers/coverage-observer.js +107 -0
  197. package/src/verax/observe/observers/interaction-observer.js +471 -0
  198. package/src/verax/observe/observers/navigation-observer.js +132 -0
  199. package/src/verax/observe/observers/network-observer.js +87 -0
  200. package/src/verax/observe/observers/safety-observer.js +82 -0
  201. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  202. package/src/verax/observe/page-traversal.js +138 -0
  203. package/src/verax/observe/snapshot-ops.js +94 -0
  204. package/src/verax/observe/ui-feedback-detector.js +742 -0
  205. package/src/verax/scan-summary-writer.js +2 -0
  206. package/src/verax/shared/artifact-manager.js +25 -5
  207. package/src/verax/shared/caching.js +1 -0
  208. package/src/verax/shared/css-spinner-rules.js +204 -0
  209. package/src/verax/shared/expectation-tracker.js +1 -0
  210. package/src/verax/shared/view-switch-rules.js +208 -0
  211. package/src/verax/shared/zip-artifacts.js +6 -0
  212. package/src/verax/shared/config-loader.js +0 -169
  213. /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * PHASE H6 - Monorepo Resolution Module
3
+ *
4
+ * Deterministically selects the correct app root in a monorepo.
5
+ * Records all decision logic in metadata for auditability.
6
+ *
7
+ * Algorithm:
8
+ * 1. Scan workspace for app root candidates
9
+ * 2. For single candidate: return it
10
+ * 3. For multiple: apply deterministic tie-breaks
11
+ * 4. Record decision trail in run.meta.json
12
+ */
13
+
14
+ import { existsSync, readFileSync } from 'fs';
15
+ import { resolve, relative } from 'path';
16
+ import { findAppRootCandidates, detectFramework as _detectFramework } from './framework-detector.js';
17
+
18
+ /**
19
+ * Detect if path contains monorepo workspace markers
20
+ */
21
+ function isMonorepo(rootPath) {
22
+ // Check for workspace configuration files
23
+ const workspaceMarkers = [
24
+ 'pnpm-workspace.yaml',
25
+ 'lerna.json',
26
+ 'nx.json',
27
+ 'turbo.json',
28
+ // package.json with workspaces field
29
+ ];
30
+
31
+ for (const marker of workspaceMarkers) {
32
+ if (existsSync(resolve(rootPath, marker))) {
33
+ return true;
34
+ }
35
+ }
36
+
37
+ // Check package.json for workspaces
38
+ try {
39
+ const pkgPath = resolve(rootPath, 'package.json');
40
+ if (existsSync(pkgPath)) {
41
+ // @ts-expect-error - readFileSync with encoding returns string
42
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
43
+ if (pkg.workspaces || pkg.packages) {
44
+ return true;
45
+ }
46
+ }
47
+ } catch {
48
+ // ignore
49
+ }
50
+
51
+ return false;
52
+ }
53
+
54
+ /**
55
+ * Detect package manager used in workspace
56
+ */
57
+ function detectPackageManager(rootPath) {
58
+ const markers = [
59
+ { file: 'pnpm-lock.yaml', manager: 'pnpm' },
60
+ { file: 'yarn.lock', manager: 'yarn' },
61
+ { file: 'package-lock.json', manager: 'npm' },
62
+ { file: 'bun.lockb', manager: 'bun' },
63
+ ];
64
+
65
+ for (const { file, manager } of markers) {
66
+ if (existsSync(resolve(rootPath, file))) {
67
+ return manager;
68
+ }
69
+ }
70
+
71
+ return 'npm'; // default
72
+ }
73
+
74
+ /**
75
+ * Resolve app root in a monorepo or single-app workspace
76
+ * Returns: { appRoot, isMonorepo, decision, candidates, evidence }
77
+ */
78
+ export function resolveAppRoot(workspaceRoot) {
79
+ const isMonorepoWorkspace = isMonorepo(workspaceRoot);
80
+ const packageManager = detectPackageManager(workspaceRoot);
81
+
82
+ const result = {
83
+ appRoot: workspaceRoot, // default
84
+ isMonorepo: isMonorepoWorkspace,
85
+ packageManager,
86
+ decision: null,
87
+ candidates: [],
88
+ evidence: [],
89
+ };
90
+
91
+ if (!isMonorepoWorkspace) {
92
+ result.evidence.push('No monorepo markers found; treating as single-app workspace');
93
+ result.decision = 'single-app-default';
94
+ return result;
95
+ }
96
+
97
+ // Scan for app candidates
98
+ const candidates = findAppRootCandidates(workspaceRoot);
99
+ result.candidates = candidates.map(c => ({
100
+ path: relative(workspaceRoot, c.path),
101
+ framework: c.framework,
102
+ confidence: c.confidence,
103
+ hasDevScript: c.hasDevScript,
104
+ }));
105
+
106
+ result.evidence.push(`Monorepo workspace detected (${packageManager})`);
107
+ result.evidence.push(`Found ${candidates.length} app candidates`);
108
+
109
+ if (candidates.length === 0) {
110
+ result.evidence.push('No app candidates found; using workspace root');
111
+ result.decision = 'no-candidates-fallback-root';
112
+ return result;
113
+ }
114
+
115
+ if (candidates.length === 1) {
116
+ result.appRoot = candidates[0].path;
117
+ result.evidence.push(`Single candidate found: ${relative(workspaceRoot, candidates[0].path)}`);
118
+ result.decision = 'single-candidate';
119
+ return result;
120
+ }
121
+
122
+ // Multiple candidates: apply deterministic tie-breaking
123
+ // Already sorted by findAppRootCandidates:
124
+ // 1. Highest framework confidence
125
+ // 2. Has dev script
126
+ // 3. Shallowest depth
127
+ // 4. Alphabetical path
128
+
129
+ const best = candidates[0];
130
+ const tieBreakers = [];
131
+
132
+ // Reason code
133
+ if (best.confidence > candidates[1].confidence) {
134
+ tieBreakers.push(`highest-framework-confidence:${best.framework}-${best.confidence}%`);
135
+ }
136
+ if (best.hasDevScript && !candidates[1].hasDevScript) {
137
+ tieBreakers.push('has-dev-script');
138
+ }
139
+ if (best.depth < candidates[1].depth) {
140
+ tieBreakers.push('shallowest-depth');
141
+ }
142
+ if (best.path < candidates[1].path) {
143
+ tieBreakers.push('alphabetical-first');
144
+ }
145
+
146
+ result.appRoot = best.path;
147
+ result.evidence.push(
148
+ `Multiple candidates (${candidates.length}); selected: ${relative(workspaceRoot, best.path)}`
149
+ );
150
+ result.evidence.push(`Tie-breakers applied: ${tieBreakers.join(' > ')}`);
151
+ result.decision = 'monorepo-tie-break';
152
+
153
+ return result;
154
+ }
155
+
156
+ /**
157
+ * Export for testing
158
+ */
159
+ export const _internal = {
160
+ isMonorepo,
161
+ detectPackageManager,
162
+ };
@@ -1,20 +1,63 @@
1
1
  import { chromium } from 'playwright';
2
- import { writeFileSync, mkdirSync } from 'fs';
3
- import { resolve } from 'path';
2
+ import { writeFileSync as _writeFileSync, mkdirSync as _mkdirSync } from 'fs';
3
+ import { resolve as _resolve } from 'path';
4
4
  import { redactHeaders, redactUrl, redactBody, redactConsole, getRedactionCounters } from './redact.js';
5
+ import { InteractionPlanner } from './interaction-planner.js';
6
+ import { computeDigest } from './digest-engine.js';
5
7
 
6
8
  /**
7
- * Real Browser Observation Engine
8
- * Monitors expectations from learn.json using actual Playwright browser
9
+ * PHASE H3/M3 - Real Browser Observation Engine
10
+ * Uses Interaction Planner to execute promises with evidence capture
11
+ * H5: Added read-only safety mode by default
9
12
  */
10
13
 
11
- export async function observeExpectations(expectations, url, evidencePath, onProgress) {
14
+ export async function observeExpectations(expectations, url, evidencePath, onProgress, _options = {}) {
15
+ // TEST MODE FAST PATH: In VERAX_TEST_MODE, skip browser work entirely for determinism and speed.
16
+ if (process.env.VERAX_TEST_MODE === '1') {
17
+ const observations = (expectations || []).map((exp, idx) => ({
18
+ id: exp.id,
19
+ expectationId: exp.id,
20
+ type: exp.type,
21
+ category: exp.category,
22
+ promise: exp.promise,
23
+ source: exp.source,
24
+ attempted: false,
25
+ observed: false,
26
+ reason: 'test-mode-skip',
27
+ observedAt: new Date().toISOString(),
28
+ evidenceFiles: [],
29
+ signals: {},
30
+ action: null,
31
+ cause: null,
32
+ index: idx + 1,
33
+ }));
34
+
35
+ const digest = computeDigest(expectations || [], observations, {
36
+ framework: 'unknown',
37
+ url,
38
+ version: '1.0',
39
+ });
40
+
41
+ return {
42
+ observations,
43
+ stats: {
44
+ attempted: 0,
45
+ observed: 0,
46
+ notObserved: 0,
47
+ blockedWrites: 0,
48
+ },
49
+ blockedWrites: [],
50
+ digest,
51
+ redaction: getRedactionCounters({ headersRedacted: 0, tokensRedacted: 0 }),
52
+ observedAt: new Date().toISOString(),
53
+ };
54
+ }
55
+
12
56
  const observations = [];
13
- let observed = 0;
14
- let notObserved = 0;
15
57
  const redactionCounters = { headersRedacted: 0, tokensRedacted: 0 };
16
58
  let browser = null;
17
59
  let page = null;
60
+ let planner = null;
18
61
 
19
62
  try {
20
63
  // Launch browser
@@ -27,10 +70,32 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
27
70
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
28
71
  });
29
72
 
30
- // Set up network and console monitoring
31
- const networkLogs = [];
32
- const consoleLogs = [];
73
+ // Create interaction planner (read-only mode enforced)
74
+ planner = new InteractionPlanner(page, evidencePath, {});
33
75
 
76
+ // Set up network monitoring (H5: with write-blocking)
77
+ const networkRecordingStartTime = Date.now();
78
+
79
+ // H5: Route handler for blocking mutating requests (MUST be before page.on)
80
+ await page.route('**/*', async (route) => {
81
+ const request = route.request();
82
+ const method = request.method();
83
+
84
+ if (planner.shouldBlockRequest(method)) {
85
+ const redactedUrl = redactUrl(request.url(), redactionCounters);
86
+ planner.blockedRequests.push({
87
+ url: redactedUrl,
88
+ method: method,
89
+ reason: 'write-blocked-read-only-mode',
90
+ timestamp: new Date().toISOString(),
91
+ });
92
+ await route.abort('blockedbyclient');
93
+ return;
94
+ }
95
+
96
+ await route.continue();
97
+ });
98
+
34
99
  page.on('request', (request) => {
35
100
  const redactedHeaders = redactHeaders(request.headers(), redactionCounters);
36
101
  const redactedUrl = redactUrl(request.url(), redactionCounters);
@@ -42,36 +107,37 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
42
107
  redactedBody = null;
43
108
  }
44
109
 
45
- networkLogs.push({
110
+ const event = {
46
111
  url: redactedUrl,
47
112
  method: request.method(),
48
113
  headers: redactedHeaders,
49
114
  body: redactedBody,
50
115
  timestamp: new Date().toISOString(),
51
- });
116
+ relativeMs: Date.now() - networkRecordingStartTime,
117
+ };
118
+
119
+ planner.recordNetworkEvent(event);
52
120
  });
53
121
 
54
122
  page.on('console', (msg) => {
55
- const redactedText = redactConsole(msg.text(), redactionCounters);
56
- consoleLogs.push({
57
- type: msg.type(),
58
- text: redactedText,
59
- timestamp: new Date().toISOString(),
60
- });
123
+ if (msg.type() === 'error' || msg.type() === 'warning') {
124
+ const redactedText = redactConsole(msg.text(), redactionCounters);
125
+ planner.recordConsoleEvent({
126
+ type: msg.type(),
127
+ text: redactedText,
128
+ timestamp: new Date().toISOString(),
129
+ });
130
+ }
61
131
  });
62
132
 
63
- // Navigate to base URL first with explicit timeout
133
+ // Navigate to base URL
64
134
  try {
65
135
  await page.goto(url, {
66
- waitUntil: 'domcontentloaded', // Use domcontentloaded instead of networkidle for faster timeout
136
+ waitUntil: 'domcontentloaded',
67
137
  timeout: 30000
68
138
  });
69
- // Wait for network idle with separate timeout
70
- await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
71
- // Network idle timeout is acceptable, continue
72
- });
139
+ await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
73
140
  } catch (error) {
74
- // Continue even if initial load fails
75
141
  if (onProgress) {
76
142
  onProgress({
77
143
  event: 'observe:warning',
@@ -80,10 +146,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
80
146
  }
81
147
  }
82
148
 
83
- // Track visited URLs for navigation observations
84
- const visitedUrls = new Set([url]);
85
-
86
- // Process each expectation
149
+ // Execute each promise via interaction planner
87
150
  for (let i = 0; i < expectations.length; i++) {
88
151
  const exp = expectations[i];
89
152
  const expNum = i + 1;
@@ -94,120 +157,76 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
94
157
  index: expNum,
95
158
  total: expectations.length,
96
159
  type: exp.type,
160
+ category: exp.category,
97
161
  promise: exp.promise,
98
162
  });
99
163
  }
100
164
 
165
+ // Execute the promise
166
+ const attempt = await planner.executeSinglePromise(exp, expNum);
167
+
168
+ // Convert attempt to observation
101
169
  const observation = {
102
170
  id: exp.id,
103
171
  type: exp.type,
172
+ category: exp.category,
104
173
  promise: exp.promise,
105
174
  source: exp.source,
106
- attempted: false,
107
- observed: false,
108
- observedAt: null,
109
- evidenceFiles: [],
110
- reason: null,
175
+ attempted: attempt.attempted,
176
+ observed: attempt.signals ? Object.values(attempt.signals).some(v => v === true) : false,
177
+ action: attempt.action,
178
+ reason: attempt.reason,
179
+ observedAt: new Date().toISOString(),
180
+ evidenceFiles: attempt.evidence?.files || [],
181
+ signals: attempt.signals,
111
182
  };
112
183
 
113
- try {
114
- let result = false;
115
- let evidence = null;
116
-
117
- if (exp.type === 'navigation') {
118
- observation.attempted = true; // Mark as attempted
119
- result = await observeNavigation(
120
- page,
121
- exp,
122
- url,
123
- visitedUrls,
124
- evidencePath,
125
- expNum
126
- );
127
- evidence = result ? `nav_${expNum}_after.png` : null;
128
- } else if (exp.type === 'network') {
129
- observation.attempted = true; // Mark as attempted
130
- result = await observeNetwork(page, exp, networkLogs, 5000);
131
- if (result) {
132
- const evidenceFile = `network_${expNum}.json`;
133
- try {
134
- mkdirSync(evidencePath, { recursive: true });
135
- const targetUrl = exp.promise.value;
136
- const relevant = networkLogs.filter((log) =>
137
- log.url === targetUrl || log.url.includes(targetUrl) || targetUrl.includes(log.url)
138
- );
139
- writeFileSync(resolve(evidencePath, evidenceFile), JSON.stringify(relevant, null, 2), 'utf-8');
140
- } catch {
141
- // best effort
142
- }
143
- evidence = evidenceFile;
144
- } else {
145
- evidence = null;
146
- }
147
- } else if (exp.type === 'state') {
148
- observation.attempted = true; // Mark as attempted
149
- result = await observeState(page, exp, evidencePath, expNum);
150
- evidence = result ? `state_${expNum}_after.png` : null;
151
- }
152
-
153
- if (result) {
154
- observation.observed = true;
155
- observation.observedAt = new Date().toISOString();
156
- if (evidence) observation.evidenceFiles.push(evidence);
157
- observed++;
158
- } else {
159
- observation.reason = 'No matching event observed';
160
- notObserved++;
161
- }
162
- } catch (error) {
163
- observation.reason = `Error: ${error.message}`;
164
- notObserved++;
165
- }
166
-
167
184
  observations.push(observation);
168
185
 
169
186
  if (onProgress) {
170
187
  onProgress({
171
188
  event: 'observe:result',
172
189
  index: expNum,
190
+ attempted: observation.attempted,
173
191
  observed: observation.observed,
174
192
  reason: observation.reason,
175
193
  });
176
194
  }
177
195
  }
178
196
 
179
- // Persist shared evidence
180
- try {
181
- mkdirSync(evidencePath, { recursive: true });
182
- const networkPath = resolve(evidencePath, 'network_logs.json');
183
- writeFileSync(networkPath, JSON.stringify(networkLogs, null, 2), 'utf-8');
184
- const consolePath = resolve(evidencePath, 'console_logs.json');
185
- writeFileSync(consolePath, JSON.stringify(consoleLogs, null, 2), 'utf-8');
186
- } catch {
187
- // Best effort; do not throw
188
- }
197
+ // Count results
198
+ const observed = observations.filter(o => o.observed).length;
199
+ const attempted = observations.filter(o => o.attempted).length;
200
+ const notObserved = attempted - observed;
201
+
202
+ // H5: Compute deterministic digest for reproducibility proof
203
+ const digest = computeDigest(expectations, observations, {
204
+ framework: 'unknown', // Will be populated by caller
205
+ url,
206
+ version: '1.0',
207
+ });
189
208
 
190
209
  return {
191
210
  observations,
192
211
  stats: {
193
- attempted: expectations.length,
212
+ attempted,
194
213
  observed,
195
214
  notObserved,
215
+ blockedWrites: planner.getBlockedRequests().length, // H5: Include write-blocking stats
196
216
  },
217
+ blockedWrites: planner.getBlockedRequests(), // H5: Include details of blocked writes
218
+ digest, // H5: Deterministic digest
197
219
  redaction: getRedactionCounters(redactionCounters),
198
220
  observedAt: new Date().toISOString(),
199
221
  };
222
+
200
223
  } finally {
201
- // Robust cleanup: ensure browser/context/page are closed
202
- // Remove all event listeners to prevent leaks
224
+ // Cleanup
203
225
  if (page) {
204
226
  try {
205
- // Remove all listeners
206
227
  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(() => {});
228
+ await page.close().catch(() => {});
209
229
  } catch (e) {
210
- // Ignore close errors but emit warning if onProgress available
211
230
  if (onProgress) {
212
231
  onProgress({
213
232
  event: 'observe:warning',
@@ -217,22 +236,18 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
217
236
  }
218
237
  }
219
238
 
220
- // Close browser context if it exists
221
239
  if (browser) {
222
240
  try {
223
241
  const contexts = browser.contexts();
224
242
  for (const context of contexts) {
225
243
  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(() => {});
244
+ await context.close().catch(() => {});
228
245
  } catch (e) {
229
- // Ignore context close errors
246
+ // Ignore
230
247
  }
231
248
  }
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(() => {});
249
+ await browser.close().catch(() => {});
234
250
  } catch (e) {
235
- // Ignore browser close errors but emit warning if onProgress available
236
251
  if (onProgress) {
237
252
  onProgress({
238
253
  event: 'observe:warning',
@@ -244,169 +259,3 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
244
259
  }
245
260
  }
246
261
 
247
- /**
248
- * Observe navigation expectation
249
- * Attempts to find and click element, observes URL/SPA changes
250
- */
251
- async function observeNavigation(page, expectation, baseUrl, visitedUrls, evidencePath, expNum) {
252
- const targetPath = expectation.promise.value;
253
-
254
- try {
255
- // Screenshot before interaction
256
- const beforePath = resolve(evidencePath, `nav_${expNum}_before.png`);
257
- await page.screenshot({ path: beforePath }).catch(() => {});
258
-
259
- // Find element by searching all anchor tags
260
- const element = await page.evaluate((path) => {
261
- const anchors = Array.from(document.querySelectorAll('a'));
262
- const found = anchors.find(a => {
263
- const href = a.getAttribute('href');
264
- return href === path || href.includes(path);
265
- });
266
- return found ? { tag: 'a', href: found.getAttribute('href') } : null;
267
- }, targetPath);
268
-
269
- if (!element) {
270
- return false;
271
- }
272
-
273
- const urlBefore = page.url();
274
- const contentBefore = await page.content();
275
-
276
- // Click the element - try multiple approaches
277
- try {
278
- await page.locator(`a[href="${element.href}"]`).click({ timeout: 3000 });
279
- } catch (e) {
280
- try {
281
- await page.click(`a[href="${element.href}"]`);
282
- } catch (e2) {
283
- // Try clicking by text content
284
- // eslint-disable-next-line no-undef
285
- const text = await page.evaluate((href) => {
286
- const anchors = Array.from(document.querySelectorAll('a'));
287
- const found = anchors.find(a => a.getAttribute('href') === href);
288
- return found ? found.textContent : null;
289
- }, element.href);
290
-
291
- if (text) {
292
- await page.click(`a:has-text("${text}")`).catch(() => {});
293
- }
294
- }
295
- }
296
-
297
- // Wait for navigation or SPA update with explicit timeout
298
- try {
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
- });
309
- } catch (e) {
310
- // Navigation might not happen, continue
311
- }
312
-
313
- // Wait for potential SPA updates (bounded)
314
- await page.waitForTimeout(300);
315
-
316
- // Screenshot after interaction
317
- const afterPath = resolve(evidencePath, `nav_${expNum}_after.png`);
318
- await page.screenshot({ path: afterPath }).catch(() => {});
319
-
320
- const urlAfter = page.url();
321
- const contentAfter = await page.content();
322
-
323
- // Check if URL changed or content changed
324
- if (urlBefore !== urlAfter || contentBefore !== contentAfter) {
325
- visitedUrls.add(urlAfter);
326
- return true;
327
- }
328
-
329
- return false;
330
- } catch (error) {
331
- return false;
332
- }
333
- }
334
-
335
- /**
336
- * Observe network expectation
337
- * Checks if matching request was made
338
- */
339
- async function observeNetwork(page, expectation, networkLogs, timeoutMs) {
340
- const targetUrl = expectation.promise.value;
341
- const startTime = Date.now();
342
-
343
- return new Promise((resolve) => {
344
- const checkTimer = setInterval(() => {
345
- const found = networkLogs.some((log) => {
346
- return (
347
- log.url === targetUrl ||
348
- log.url.includes(targetUrl) ||
349
- targetUrl.includes(log.url)
350
- );
351
- });
352
-
353
- if (found) {
354
- clearInterval(checkTimer);
355
- resolve(true);
356
- return;
357
- }
358
-
359
- if (Date.now() - startTime > timeoutMs) {
360
- clearInterval(checkTimer);
361
- resolve(false);
362
- return;
363
- }
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
- }
371
- });
372
- }
373
-
374
- /**
375
- * Observe state expectation
376
- * Detects DOM changes or loading indicators
377
- */
378
- async function observeState(page, expectation, evidencePath, expNum) {
379
- try {
380
- // Screenshot before
381
- const beforePath = resolve(evidencePath, `state_${expNum}_before.png`);
382
- await page.screenshot({ path: beforePath });
383
-
384
- const htmlBefore = await page.content();
385
-
386
- // Wait briefly for potential state changes
387
- await page.waitForTimeout(2000);
388
-
389
- const htmlAfter = await page.content();
390
-
391
- // Screenshot after
392
- const afterPath = resolve(evidencePath, `state_${expNum}_after.png`);
393
- await page.screenshot({ path: afterPath });
394
-
395
- // Check if DOM changed
396
- if (htmlBefore !== htmlAfter) {
397
- return true;
398
- }
399
-
400
- // Check for common state indicators (loading, error, success messages)
401
- const hasStateIndicators =
402
- (await page.$('.loading')) ||
403
- (await page.$('[role="status"]')) ||
404
- (await page.$('.toast')) ||
405
- (await page.$('[aria-live]'));
406
-
407
- return !!hasStateIndicators;
408
- } catch (error) {
409
- return false;
410
- }
411
- }
412
-
@@ -1,5 +1,6 @@
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
@@ -8,6 +9,7 @@ export function writeObserveJson(runDir, observeData) {
8
9
  const observePath = resolve(runDir, 'observe.json');
9
10
 
10
11
  const payload = {
12
+ contractVersion: ARTIFACT_REGISTRY.observe.contractVersion,
11
13
  observations: observeData.observations || [],
12
14
  stats: {
13
15
  attempted: observeData.stats?.attempted || 0,