@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
@@ -21,6 +21,11 @@ function extractStaticStringValue(node) {
21
21
  if (node.type === 'StringLiteral') {
22
22
  return node.value;
23
23
  }
24
+
25
+ // Template literal without interpolation
26
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis.length === 1) {
27
+ return node.quasis[0].value.cooked;
28
+ }
24
29
 
25
30
  // JSX expression: href={'/about'} or href={`/about`}
26
31
  if (node.type === 'JSXExpressionContainer') {
@@ -46,6 +51,22 @@ function extractStaticStringValue(node) {
46
51
  return null;
47
52
  }
48
53
 
54
+ function extractStaticPropValue(propsNode, propNames) {
55
+ if (!propsNode || propsNode.type !== 'ObjectExpression') return { attributeName: null, targetPath: null };
56
+ const names = new Set(propNames);
57
+ for (const prop of propsNode.properties || []) {
58
+ if (prop.type !== 'ObjectProperty') continue;
59
+ const key = prop.key;
60
+ const name = key.type === 'Identifier' ? key.name : (key.type === 'StringLiteral' ? key.value : null);
61
+ if (!name || !names.has(name)) continue;
62
+ const targetPath = extractStaticStringValue(prop.value);
63
+ if (targetPath) {
64
+ return { attributeName: name, targetPath };
65
+ }
66
+ }
67
+ return { attributeName: null, targetPath: null };
68
+ }
69
+
49
70
  /**
50
71
  * Extracts template literal pattern from Babel TemplateLiteral node.
51
72
  * Returns null if template has complex expressions that cannot be normalized.
@@ -176,6 +197,37 @@ function extractContractsFromFile(filePath, fileContent) {
176
197
  // since we cannot reliably tie them to clicked elements
177
198
  CallExpression(path) {
178
199
  const callee = path.node.callee;
200
+
201
+ // React.createElement(Link, { to: '/about' }) or createElement('a', { href: '/about' })
202
+ const isCreateElement = (
203
+ (callee.type === 'MemberExpression' && callee.object.type === 'Identifier' && callee.object.name === 'React' && callee.property.type === 'Identifier' && callee.property.name === 'createElement') ||
204
+ (callee.type === 'Identifier' && callee.name === 'createElement')
205
+ );
206
+
207
+ if (isCreateElement) {
208
+ const [componentArg, propsArg] = path.node.arguments;
209
+ let elementName = null;
210
+ if (componentArg?.type === 'Identifier') {
211
+ elementName = componentArg.name;
212
+ } else if (componentArg?.type === 'StringLiteral') {
213
+ elementName = componentArg.value;
214
+ }
215
+ if (elementName) {
216
+ const { attributeName, targetPath } = extractStaticPropValue(propsArg, ['to', 'href']);
217
+ if (targetPath && !targetPath.startsWith('http://') && !targetPath.startsWith('https://') && !targetPath.startsWith('mailto:') && !targetPath.startsWith('tel:')) {
218
+ const normalized = targetPath.startsWith('/') ? targetPath : '/' + targetPath;
219
+ contracts.push({
220
+ kind: 'NAVIGATION',
221
+ targetPath: normalized,
222
+ sourceFile: filePath,
223
+ element: elementName,
224
+ attribute: attributeName,
225
+ proof: ExpectationProof.PROVEN_EXPECTATION,
226
+ line: path.node.loc?.start.line || null
227
+ });
228
+ }
229
+ }
230
+ }
179
231
 
180
232
  // navigate("/about") - useNavigate hook
181
233
  if (callee.type === 'Identifier' && callee.name === 'navigate') {
@@ -308,7 +360,7 @@ export async function extractASTContracts(projectDir) {
308
360
  * Converts AST contracts to manifest expectations format.
309
361
  * Only includes contracts that can be matched at runtime (excludes imperativeOnly).
310
362
  */
311
- export function contractsToExpectations(contracts, projectType) {
363
+ export function contractsToExpectations(contracts, _projectType) {
312
364
  const expectations = [];
313
365
  const seenPaths = new Set();
314
366
 
@@ -1,9 +1,31 @@
1
1
  import { resolve } from 'path';
2
- import { existsSync } from 'fs';
2
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
3
3
  import { detectProjectType } from './project-detector.js';
4
4
  import { extractRoutes } from './route-extractor.js';
5
5
  import { writeManifest } from './manifest-writer.js';
6
6
 
7
+ /**
8
+ * @typedef {Object} LearnResult
9
+ * @property {number} version
10
+ * @property {string} learnedAt
11
+ * @property {string} projectDir
12
+ * @property {string} projectType
13
+ * @property {Array} routes
14
+ * @property {Array<string>} publicRoutes
15
+ * @property {Array<string>} internalRoutes
16
+ * @property {Array} [staticExpectations]
17
+ * @property {Array} [flows]
18
+ * @property {string} [expectationsStatus]
19
+ * @property {Array} [coverageGaps]
20
+ * @property {Array} notes
21
+ * @property {Object} [learnTruth]
22
+ * @property {string} [manifestPath] - Optional manifest path (added when loaded from file)
23
+ */
24
+
25
+ /**
26
+ * @param {string} projectDir
27
+ * @returns {Promise<LearnResult>}
28
+ */
7
29
  export async function learn(projectDir) {
8
30
  const absoluteProjectDir = resolve(projectDir);
9
31
 
@@ -14,5 +36,17 @@ export async function learn(projectDir) {
14
36
  const projectType = await detectProjectType(absoluteProjectDir);
15
37
  const routes = await extractRoutes(absoluteProjectDir, projectType);
16
38
 
17
- return await writeManifest(absoluteProjectDir, projectType, routes);
39
+ const manifest = await writeManifest(absoluteProjectDir, projectType, routes);
40
+
41
+ // Write manifest to disk and return path
42
+ const veraxDir = resolve(absoluteProjectDir, '.verax');
43
+ mkdirSync(veraxDir, { recursive: true });
44
+
45
+ const manifestPath = resolve(veraxDir, 'project.json');
46
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
47
+
48
+ return {
49
+ ...manifest,
50
+ manifestPath
51
+ };
18
52
  }
@@ -1,5 +1,4 @@
1
- import { resolve } from 'path';
2
- import { writeFileSync, mkdirSync } from 'fs';
1
+ // resolve, writeFileSync, mkdirSync imports removed - currently unused
3
2
  import { extractStaticExpectations } from './static-extractor.js';
4
3
  import { assessLearnTruth } from './truth-assessor.js';
5
4
  import { runCodeIntelligence } from '../intel/index.js';
@@ -8,6 +7,29 @@ import { extractFlows } from './flow-extractor.js';
8
7
  import { createTSProgram } from '../intel/ts-program.js';
9
8
  import { extractVueNavigationPromises } from '../intel/vue-navigation-extractor.js';
10
9
 
10
+ /**
11
+ * @typedef {Object} Manifest
12
+ * @property {number} version
13
+ * @property {string} learnedAt
14
+ * @property {string} projectDir
15
+ * @property {string} projectType
16
+ * @property {Array} routes
17
+ * @property {Array<string>} publicRoutes
18
+ * @property {Array<string>} internalRoutes
19
+ * @property {Array} [staticExpectations]
20
+ * @property {Array} [flows]
21
+ * @property {string} [expectationsStatus]
22
+ * @property {Array} [coverageGaps]
23
+ * @property {Array} notes
24
+ * @property {Object} [learnTruth]
25
+ */
26
+
27
+ /**
28
+ * @param {string} projectDir
29
+ * @param {string} projectType
30
+ * @param {Array} routes
31
+ * @returns {Promise<Manifest>}
32
+ */
11
33
  export async function writeManifest(projectDir, projectType, routes) {
12
34
  const publicRoutes = routes.filter(r => r.public).map(r => r.path);
13
35
  const internalRoutes = routes.filter(r => !r.public).map(r => r.path);
@@ -44,7 +66,7 @@ export async function writeManifest(projectDir, projectType, routes) {
44
66
  const program = createTSProgram(projectDir, { includeJs: true });
45
67
 
46
68
  if (!program.error) {
47
- const vueNavPromises = extractVueNavigationPromises(program, projectDir);
69
+ const vueNavPromises = await extractVueNavigationPromises(program, projectDir);
48
70
 
49
71
  if (vueNavPromises && vueNavPromises.length > 0) {
50
72
  intelExpectations = vueNavPromises.filter(exp => isProvenExpectation(exp));
@@ -132,16 +154,8 @@ export async function writeManifest(projectDir, projectType, routes) {
132
154
  learn: learnTruth
133
155
  });
134
156
 
135
- const manifestDir = resolve(projectDir, '.veraxverax', 'learn');
136
- mkdirSync(manifestDir, { recursive: true });
137
-
138
- const manifestPath = resolve(manifestDir, 'site-manifest.json');
139
- writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
140
-
141
- return {
142
- ...manifest,
143
- manifestPath: manifestPath,
144
- learnTruth: learnTruth
145
- };
157
+ // Note: This function is still used by learn() function
158
+ // Direct usage is deprecated in favor of CLI learn.json writer
159
+ return manifest;
146
160
  }
147
161
 
@@ -1,4 +1,4 @@
1
- import { resolve } from 'path';
1
+ // resolve import removed - currently unused
2
2
  import { extractStaticRoutes } from './static-extractor.js';
3
3
  import { createTSProgram } from '../intel/ts-program.js';
4
4
  import { extractRoutes as extractRoutesAST } from '../intel/route-extractor.js';
@@ -15,7 +15,10 @@ export async function validateRoutes(manifest, baseUrl) {
15
15
  routesReachable: 0,
16
16
  routesUnreachable: 0,
17
17
  details: [],
18
- warnings: []
18
+ warnings: [{
19
+ code: 'INVALID_BASE_URL',
20
+ message: `Cannot parse base URL for validation: ${error.message}`
21
+ }]
19
22
  };
20
23
  }
21
24
 
@@ -99,14 +102,15 @@ export async function validateRoutes(manifest, baseUrl) {
99
102
 
100
103
  const request = response.request();
101
104
 
102
- // Use redirectChain() if available (preferred), otherwise use redirectedFrom()
105
+ // Use redirectChain property if available (Playwright API)
103
106
  let redirectChain = [];
104
- if (typeof request.redirectChain === 'function') {
105
- try {
106
- redirectChain = request.redirectChain();
107
- } catch (e) {
108
- // redirectChain may not be available, fall back to redirectedFrom
109
- }
107
+ // @ts-expect-error - redirectChain exists in Playwright runtime but not in TypeScript types
108
+ if (request.redirectChain && Array.isArray(request.redirectChain)) {
109
+ // @ts-expect-error - redirectChain exists in Playwright runtime but not in TypeScript types
110
+ redirectChain = request.redirectChain;
111
+ } else if (request.redirectedFrom) {
112
+ // Fall back to redirectedFrom if redirectChain not available
113
+ redirectChain = [request.redirectedFrom];
110
114
  }
111
115
 
112
116
  // If redirectChain is empty or not available, build chain from redirectedFrom
@@ -11,7 +11,7 @@ const MAX_FILES_TO_SCAN = 200;
11
11
  * Extracts static string value from call expression arguments.
12
12
  * Returns null if value is dynamic.
13
13
  */
14
- function extractStaticActionName(node) {
14
+ function _extractStaticActionName(node) {
15
15
  if (!node) return null;
16
16
 
17
17
  // String literal: dispatch('increment')
@@ -3,7 +3,7 @@
3
3
  * Detects router.push/replace/navigate calls in inline scripts.
4
4
  */
5
5
 
6
- function extractNavigationExpectations(root, fromPath, file, projectDir) {
6
+ function extractNavigationExpectations(root, fromPath, file, _projectDir) {
7
7
  const expectations = [];
8
8
 
9
9
  // Extract from inline scripts
@@ -3,7 +3,7 @@
3
3
  * Detects preventDefault() calls in onSubmit handlers.
4
4
  */
5
5
 
6
- function extractValidationExpectations(root, fromPath, file, projectDir) {
6
+ function extractValidationExpectations(root, fromPath, file, _projectDir) {
7
7
  const expectations = [];
8
8
 
9
9
  // Extract from inline scripts
@@ -19,7 +19,7 @@ function extractValidationExpectations(root, fromPath, file, projectDir) {
19
19
  // Check if this is in an onSubmit context
20
20
  // Look for function definitions that might be onSubmit handlers
21
21
  const beforeMatch = scriptContent.substring(0, match.index);
22
- const afterMatch = scriptContent.substring(match.index);
22
+ const _afterMatch = scriptContent.substring(match.index);
23
23
 
24
24
  // Check if there's a function definition before this preventDefault
25
25
  const functionMatch = beforeMatch.match(/function\s+(\w+)\s*\([^)]*event[^)]*\)/);
@@ -1,13 +1,13 @@
1
1
  import { glob } from 'glob';
2
- import { resolve, dirname, join, relative } from 'path';
2
+ import { resolve, dirname, relative } from 'path';
3
3
  import { readFileSync, existsSync } from 'fs';
4
4
  import { parse } from 'node-html-parser';
5
5
 
6
6
  const MAX_HTML_FILES = 200;
7
7
 
8
8
  function htmlFileToRoute(file) {
9
- let path = file.replace(/[\\\/]/g, '/');
10
- path = path.replace(/\/index\.html$/, '');
9
+ let path = file.replace(/[\\/]/g, '/');
10
+ path = path.replace(/\/index.html$/, '');
11
11
  path = path.replace(/\.html$/, '');
12
12
  path = path.replace(/^index$/, '');
13
13
 
@@ -159,7 +159,7 @@ function extractFormSubmissionExpectations(root, fromPath, file, routeMap, proje
159
159
  return expectations;
160
160
  }
161
161
 
162
- function extractNetworkExpectations(root, fromPath, file, projectDir) {
162
+ function extractNetworkExpectations(root, fromPath, file, _projectDir) {
163
163
  const expectations = [];
164
164
 
165
165
  // Extract from inline scripts
@@ -286,9 +286,10 @@ export async function extractStaticExpectations(projectDir, routes) {
286
286
  const targetPath = resolveLinkPath(href, file, projectDir);
287
287
  if (!targetPath) continue;
288
288
 
289
- if (!routeMap.has(targetPath)) continue;
290
-
291
- const linkText = link.textContent?.trim() || '';
289
+ // Extract expectation even if target route doesn't exist yet
290
+ // This allows detection of broken links or prevented navigation
291
+ // (The route may not exist, but the link promises navigation)
292
+ const _linkText = link.textContent?.trim() || '';
292
293
  const selectorHint = link.id ? `#${link.id}` : `a[href="${href}"]`;
293
294
 
294
295
  expectations.push({
@@ -9,7 +9,7 @@
9
9
 
10
10
  import ts from 'typescript';
11
11
  import { resolve, relative, dirname, sep, join } from 'path';
12
- import { readFileSync, existsSync, statSync } from 'fs';
12
+ import { existsSync, statSync } from 'fs';
13
13
  import { glob } from 'glob';
14
14
 
15
15
  const MAX_DEPTH = 3;
@@ -45,14 +45,8 @@ export async function resolveActionContracts(rootDir, workspaceRoot) {
45
45
  const checker = program.getTypeChecker();
46
46
  const contracts = [];
47
47
 
48
- for (const sourceFile of program.getSourceFiles()) {
49
- const sourcePath = resolve(sourceFile.fileName);
50
- if (!sourcePath.toLowerCase().startsWith(normalizedRoot.toLowerCase())) continue;
51
- if (sourceFile.isDeclarationFile) continue;
52
-
53
- const importMap = buildImportMap(sourceFile, rootDir, workspaceRoot);
54
-
55
- function visit(node) {
48
+ function createVisitFunction(importMap, sourceFile) {
49
+ return function visit(node) {
56
50
  if (ts.isJsxAttribute(node)) {
57
51
  const name = node.name.getText();
58
52
  if (name !== 'onClick' && name !== 'onSubmit') return;
@@ -64,7 +58,7 @@ export async function resolveActionContracts(rootDir, workspaceRoot) {
64
58
  // We only handle identifier handlers (cross-file capable)
65
59
  if (!ts.isIdentifier(expr)) return;
66
60
 
67
- const handlerName = expr.text;
61
+ const _handlerName = expr.text;
68
62
  const handlerRef = deriveHandlerRef(expr, importMap, sourceFile, workspaceRoot);
69
63
  if (!handlerRef) return;
70
64
 
@@ -112,8 +106,16 @@ export async function resolveActionContracts(rootDir, workspaceRoot) {
112
106
  }
113
107
  }
114
108
  ts.forEachChild(node, visit);
115
- }
109
+ };
110
+ }
116
111
 
112
+ for (const sourceFile of program.getSourceFiles()) {
113
+ const sourcePath = resolve(sourceFile.fileName);
114
+ if (!sourcePath.toLowerCase().startsWith(normalizedRoot.toLowerCase())) continue;
115
+ if (sourceFile.isDeclarationFile) continue;
116
+
117
+ const importMap = buildImportMap(sourceFile, rootDir, workspaceRoot);
118
+ const visit = createVisitFunction(importMap, sourceFile);
117
119
  visit(sourceFile);
118
120
  }
119
121
 
@@ -313,7 +315,7 @@ function analyzeStateCall(node) {
313
315
 
314
316
  // Zustand: store.set(...) or setState(...)
315
317
  if (ts.isPropertyAccessExpression(callee)) {
316
- const obj = callee.expression;
318
+ const _obj = callee.expression;
317
319
  const prop = callee.name;
318
320
  // Common pattern: storeObj.set(...)
319
321
  if (prop.text === 'set') {
@@ -16,12 +16,31 @@ export async function navigateToUrl(page, url, scanBudget = DEFAULT_SCAN_BUDGET)
16
16
  if (url.startsWith('file:') || url.includes('localhost:') || url.includes('127.0.0.1')) {
17
17
  stableWait = 200; // Short wait for local fixtures
18
18
  }
19
- } catch {}
20
- await page.goto(url, { waitUntil: 'networkidle', timeout: scanBudget.initialNavigationTimeoutMs });
19
+ } catch {
20
+ // Ignore config errors
21
+ }
22
+ // Use domcontentloaded first for faster timeout, then wait for networkidle separately
23
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: scanBudget.initialNavigationTimeoutMs });
24
+ await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
25
+ // Network idle timeout is acceptable, continue
26
+ });
21
27
  await page.waitForTimeout(stableWait);
22
28
  }
23
29
 
24
30
  export async function closeBrowser(browser) {
25
- await browser.close();
31
+ try {
32
+ // Close all contexts first
33
+ const contexts = browser.contexts();
34
+ for (const context of contexts) {
35
+ try {
36
+ await context.close({ timeout: 5000 }).catch(() => {});
37
+ } catch (e) {
38
+ // Ignore context close errors
39
+ }
40
+ }
41
+ await browser.close({ timeout: 5000 }).catch(() => {});
42
+ } catch (e) {
43
+ // Ignore browser close errors - best effort cleanup
44
+ }
26
45
  }
27
46
 
@@ -57,7 +57,7 @@ export class ConsoleSensor {
57
57
  };
58
58
 
59
59
  // Capture unhandled promise rejections
60
- const onUnhandledRejection = (promise, reason) => {
60
+ const _onUnhandledRejection = (promise, reason) => {
61
61
  const message = (reason?.toString?.() || String(reason)).slice(0, 200);
62
62
  state.unhandledRejections.push({
63
63
  message: message,
@@ -104,7 +104,7 @@ export class ConsoleSensor {
104
104
  /**
105
105
  * Stop monitoring and return a summary for the window.
106
106
  */
107
- stopWindow(windowId, page) {
107
+ stopWindow(windowId, _page) {
108
108
  const state = this.windows.get(windowId);
109
109
  if (!state) {
110
110
  return this.getEmptySummary();
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Executes every PROVEN expectation from the manifest.
5
5
  * Each expectation must result in: VERIFIED, SILENT_FAILURE, or COVERAGE_GAP.
6
+ * @typedef {import('playwright').Page} Page
6
7
  */
7
8
 
8
9
  import { isProvenExpectation } from '../shared/expectation-prover.js';
@@ -11,7 +12,7 @@ import { hasMeaningfulUrlChange, hasVisibleChange, hasDomChange } from '../detec
11
12
  import { runInteraction } from './interaction-runner.js';
12
13
  import { captureScreenshot } from './evidence-capture.js';
13
14
  import { getBaseOrigin } from './domain-boundary.js';
14
- import { resolve, dirname } from 'path';
15
+ import { resolve } from 'path';
15
16
 
16
17
  /**
17
18
  * Execute a single PROVEN expectation.
@@ -155,7 +155,7 @@ export class FocusSensor {
155
155
  /**
156
156
  * Detect if focus didn't move into modal (expected but didn't happen)
157
157
  */
158
- detectModalFocusFailure(page) {
158
+ detectModalFocusFailure(_page) {
159
159
  // This is checked via modal detection - focus should move to modal
160
160
  // Presence of modal without focus change = failure
161
161
  return false; // Caller will check if modal opened
@@ -158,7 +158,9 @@ export class HumanBehaviorDriver {
158
158
  } else if (url.includes('localhost:') || url.includes('127.0.0.1')) {
159
159
  timeoutMs = 200; // Short wait for local http fixtures
160
160
  }
161
- } catch {}
161
+ } catch {
162
+ // Ignore config errors
163
+ }
162
164
 
163
165
  const waitForUiIdle = async () => {
164
166
  const start = Date.now();
@@ -336,7 +338,10 @@ export class HumanBehaviorDriver {
336
338
 
337
339
  const focusableSelectors = await page.evaluate(() => {
338
340
  const focusables = Array.from(document.querySelectorAll('a[href], button, input, select, textarea, [tabindex], [role="button"], [role="menuitem"], [contenteditable="true"]'))
339
- .filter(el => !el.hasAttribute('disabled') && el.tabIndex >= 0 && el.offsetParent !== null);
341
+ .filter(el => {
342
+ const htmlEl = /** @type {HTMLElement} */ (el);
343
+ return !el.hasAttribute('disabled') && htmlEl.tabIndex >= 0 && htmlEl.offsetParent !== null;
344
+ });
340
345
  const describe = (el) => {
341
346
  if (!el) return 'body';
342
347
  if (el.id) return `#${el.id}`;
@@ -609,7 +614,9 @@ export class HumanBehaviorDriver {
609
614
  page.waitForTimeout(CLICK_TIMEOUT_MS)
610
615
  ]);
611
616
  await this.waitAfterAction(page, 600);
612
- } catch {}
617
+ } catch {
618
+ // Ignore form submission errors
619
+ }
613
620
 
614
621
  const afterUrl = page.url();
615
622
  const afterStorageKeys = await page.evaluate(() => Object.keys(localStorage));
@@ -660,7 +667,9 @@ export class HumanBehaviorDriver {
660
667
  clicked = true;
661
668
  await this.waitAfterAction(page, 400);
662
669
  break;
663
- } catch {}
670
+ } catch {
671
+ // Ignore interaction errors
672
+ }
664
673
  }
665
674
  }
666
675
 
@@ -686,17 +695,23 @@ export class HumanBehaviorDriver {
686
695
  const beforeUrl = page.url();
687
696
  try {
688
697
  await page.goto(url, { waitUntil: 'load', timeout: CLICK_TIMEOUT_MS }).catch(() => null);
689
- } catch {}
698
+ } catch {
699
+ // Ignore navigation errors
700
+ }
690
701
 
691
702
  const afterUrl = page.url();
692
- const blocked = beforeUrl !== afterUrl && (afterUrl.includes('/login') || afterUrl.includes('/signin'));
703
+ const redirectedToLogin = beforeUrl !== afterUrl && (afterUrl.includes('/login') || afterUrl.includes('/signin'));
693
704
  const content = await page.content();
694
705
  const hasAccessDenied = content.includes('401') || content.includes('403') || content.includes('unauthorized') || content.includes('forbidden');
706
+ const isProtected = redirectedToLogin || hasAccessDenied;
695
707
 
696
708
  return {
697
709
  url,
698
- blocked: blocked || hasAccessDenied,
699
- redirectedTo: afterUrl,
710
+ beforeUrl,
711
+ afterUrl,
712
+ isProtected,
713
+ redirectedToLogin,
714
+ hasAccessDenied,
700
715
  httpStatus: hasAccessDenied ? (content.includes('403') ? 403 : 401) : 200
701
716
  };
702
717
  }
@@ -710,7 +725,9 @@ export class HumanBehaviorDriver {
710
725
  const key = window.localStorage.key(i);
711
726
  if (key) result[key] = window.localStorage.getItem(key);
712
727
  }
713
- } catch (e) {}
728
+ } catch {
729
+ // Ignore localStorage access errors
730
+ }
714
731
  return result;
715
732
  });
716
733
 
@@ -721,7 +738,9 @@ export class HumanBehaviorDriver {
721
738
  const key = window.sessionStorage.key(i);
722
739
  if (key) result[key] = window.sessionStorage.getItem(key);
723
740
  }
724
- } catch (e) {}
741
+ } catch {
742
+ // Ignore sessionStorage access errors
743
+ }
725
744
  return result;
726
745
  });
727
746