@veraxhq/verax 0.3.0 → 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 (191) hide show
  1. package/README.md +28 -20
  2. package/bin/verax.js +11 -18
  3. package/package.json +28 -7
  4. package/src/cli/commands/baseline.js +1 -2
  5. package/src/cli/commands/default.js +72 -81
  6. package/src/cli/commands/doctor.js +29 -0
  7. package/src/cli/commands/ga.js +3 -0
  8. package/src/cli/commands/gates.js +1 -1
  9. package/src/cli/commands/inspect.js +6 -133
  10. package/src/cli/commands/release-check.js +2 -0
  11. package/src/cli/commands/run.js +74 -246
  12. package/src/cli/commands/security-check.js +2 -1
  13. package/src/cli/commands/truth.js +0 -1
  14. package/src/cli/entry.js +82 -309
  15. package/src/cli/util/angular-component-extractor.js +2 -2
  16. package/src/cli/util/angular-navigation-detector.js +2 -2
  17. package/src/cli/util/ast-interactive-detector.js +4 -6
  18. package/src/cli/util/ast-network-detector.js +3 -3
  19. package/src/cli/util/ast-promise-extractor.js +581 -0
  20. package/src/cli/util/ast-usestate-detector.js +3 -3
  21. package/src/cli/util/atomic-write.js +12 -1
  22. package/src/cli/util/console-reporter.js +72 -0
  23. package/src/cli/util/detection-engine.js +105 -41
  24. package/src/cli/util/determinism-runner.js +2 -1
  25. package/src/cli/util/determinism-writer.js +1 -1
  26. package/src/cli/util/digest-engine.js +359 -0
  27. package/src/cli/util/dom-diff.js +226 -0
  28. package/src/cli/util/env-url.js +0 -4
  29. package/src/cli/util/evidence-engine.js +287 -0
  30. package/src/cli/util/expectation-extractor.js +217 -367
  31. package/src/cli/util/findings-writer.js +19 -126
  32. package/src/cli/util/framework-detector.js +572 -0
  33. package/src/cli/util/idgen.js +1 -1
  34. package/src/cli/util/interaction-planner.js +529 -0
  35. package/src/cli/util/learn-writer.js +2 -2
  36. package/src/cli/util/ledger-writer.js +110 -0
  37. package/src/cli/util/monorepo-resolver.js +162 -0
  38. package/src/cli/util/observation-engine.js +127 -278
  39. package/src/cli/util/observe-writer.js +2 -2
  40. package/src/cli/util/paths.js +12 -3
  41. package/src/cli/util/project-discovery.js +284 -3
  42. package/src/cli/util/project-writer.js +2 -2
  43. package/src/cli/util/run-id.js +23 -27
  44. package/src/cli/util/run-result.js +778 -0
  45. package/src/cli/util/selector-resolver.js +235 -0
  46. package/src/cli/util/summary-writer.js +2 -1
  47. package/src/cli/util/svelte-navigation-detector.js +3 -3
  48. package/src/cli/util/svelte-sfc-extractor.js +0 -1
  49. package/src/cli/util/svelte-state-detector.js +1 -2
  50. package/src/cli/util/trust-activation-integration.js +496 -0
  51. package/src/cli/util/trust-activation-wrapper.js +85 -0
  52. package/src/cli/util/trust-integration-hooks.js +164 -0
  53. package/src/cli/util/types.js +153 -0
  54. package/src/cli/util/url-validation.js +40 -0
  55. package/src/cli/util/vue-navigation-detector.js +4 -3
  56. package/src/cli/util/vue-sfc-extractor.js +1 -2
  57. package/src/cli/util/vue-state-detector.js +1 -1
  58. package/src/types/fs-augment.d.ts +23 -0
  59. package/src/types/global.d.ts +137 -0
  60. package/src/types/internal-types.d.ts +35 -0
  61. package/src/verax/cli/finding-explainer.js +3 -56
  62. package/src/verax/cli/init.js +4 -18
  63. package/src/verax/core/action-classifier.js +4 -3
  64. package/src/verax/core/artifacts/registry.js +0 -15
  65. package/src/verax/core/artifacts/verifier.js +18 -8
  66. package/src/verax/core/baseline/baseline.snapshot.js +2 -0
  67. package/src/verax/core/capabilities/gates.js +7 -1
  68. package/src/verax/core/confidence/confidence-compute.js +14 -7
  69. package/src/verax/core/confidence/confidence.loader.js +1 -0
  70. package/src/verax/core/confidence-engine-refactor.js +8 -3
  71. package/src/verax/core/confidence-engine.js +162 -23
  72. package/src/verax/core/contracts/types.js +1 -0
  73. package/src/verax/core/contracts/validators.js +79 -4
  74. package/src/verax/core/decision-snapshot.js +3 -30
  75. package/src/verax/core/decisions/decision.trace.js +2 -0
  76. package/src/verax/core/determinism/contract-writer.js +2 -2
  77. package/src/verax/core/determinism/contract.js +1 -1
  78. package/src/verax/core/determinism/diff.js +42 -1
  79. package/src/verax/core/determinism/engine.js +7 -6
  80. package/src/verax/core/determinism/finding-identity.js +3 -2
  81. package/src/verax/core/determinism/normalize.js +32 -4
  82. package/src/verax/core/determinism/report-writer.js +1 -0
  83. package/src/verax/core/determinism/run-fingerprint.js +7 -2
  84. package/src/verax/core/dynamic-route-intelligence.js +8 -7
  85. package/src/verax/core/evidence/evidence-capture-service.js +1 -0
  86. package/src/verax/core/evidence/evidence-intent-ledger.js +2 -1
  87. package/src/verax/core/evidence-builder.js +2 -2
  88. package/src/verax/core/execution-mode-context.js +1 -1
  89. package/src/verax/core/execution-mode-detector.js +5 -3
  90. package/src/verax/core/failures/exit-codes.js +39 -37
  91. package/src/verax/core/failures/failure-summary.js +1 -1
  92. package/src/verax/core/failures/failure.factory.js +3 -3
  93. package/src/verax/core/failures/failure.ledger.js +3 -2
  94. package/src/verax/core/ga/ga.artifact.js +1 -1
  95. package/src/verax/core/ga/ga.contract.js +3 -2
  96. package/src/verax/core/ga/ga.enforcer.js +1 -0
  97. package/src/verax/core/guardrails/policy.loader.js +1 -0
  98. package/src/verax/core/guardrails/truth-reconciliation.js +1 -1
  99. package/src/verax/core/guardrails-engine.js +2 -2
  100. package/src/verax/core/incremental-store.js +1 -0
  101. package/src/verax/core/integrity/budget.js +138 -0
  102. package/src/verax/core/integrity/determinism.js +342 -0
  103. package/src/verax/core/integrity/integrity.js +208 -0
  104. package/src/verax/core/integrity/poisoning.js +108 -0
  105. package/src/verax/core/integrity/transaction.js +140 -0
  106. package/src/verax/core/observe/run-timeline.js +2 -0
  107. package/src/verax/core/perf/perf.report.js +2 -0
  108. package/src/verax/core/pipeline-tracker.js +5 -0
  109. package/src/verax/core/release/provenance.builder.js +73 -214
  110. package/src/verax/core/release/release.enforcer.js +14 -9
  111. package/src/verax/core/release/reproducibility.check.js +1 -0
  112. package/src/verax/core/release/sbom.builder.js +32 -23
  113. package/src/verax/core/replay-validator.js +2 -0
  114. package/src/verax/core/replay.js +4 -0
  115. package/src/verax/core/report/cross-index.js +6 -3
  116. package/src/verax/core/report/human-summary.js +141 -1
  117. package/src/verax/core/route-intelligence.js +4 -3
  118. package/src/verax/core/run-id.js +6 -3
  119. package/src/verax/core/run-manifest.js +4 -3
  120. package/src/verax/core/security/secrets.scan.js +10 -7
  121. package/src/verax/core/security/security.enforcer.js +4 -0
  122. package/src/verax/core/security/supplychain.policy.js +9 -1
  123. package/src/verax/core/security/vuln.scan.js +2 -2
  124. package/src/verax/core/truth/truth.certificate.js +3 -1
  125. package/src/verax/core/ui-feedback-intelligence.js +12 -46
  126. package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
  127. package/src/verax/detect/confidence-engine.js +100 -660
  128. package/src/verax/detect/confidence-helper.js +1 -0
  129. package/src/verax/detect/detection-engine.js +1 -18
  130. package/src/verax/detect/dynamic-route-findings.js +17 -14
  131. package/src/verax/detect/expectation-chain-detector.js +1 -1
  132. package/src/verax/detect/expectation-model.js +3 -5
  133. package/src/verax/detect/failure-cause-inference.js +293 -0
  134. package/src/verax/detect/findings-writer.js +126 -166
  135. package/src/verax/detect/flow-detector.js +2 -2
  136. package/src/verax/detect/form-silent-failure.js +98 -0
  137. package/src/verax/detect/index.js +51 -234
  138. package/src/verax/detect/invariants-enforcer.js +147 -0
  139. package/src/verax/detect/journey-stall-detector.js +4 -4
  140. package/src/verax/detect/navigation-silent-failure.js +82 -0
  141. package/src/verax/detect/problem-aggregator.js +361 -0
  142. package/src/verax/detect/route-findings.js +7 -6
  143. package/src/verax/detect/summary-writer.js +477 -0
  144. package/src/verax/detect/test-failure-cause-inference.js +314 -0
  145. package/src/verax/detect/ui-feedback-findings.js +18 -18
  146. package/src/verax/detect/verdict-engine.js +3 -57
  147. package/src/verax/detect/view-switch-correlator.js +2 -2
  148. package/src/verax/flow/flow-engine.js +2 -1
  149. package/src/verax/flow/flow-spec.js +0 -6
  150. package/src/verax/index.js +48 -412
  151. package/src/verax/intel/ts-program.js +1 -0
  152. package/src/verax/intel/vue-navigation-extractor.js +3 -0
  153. package/src/verax/learn/action-contract-extractor.js +67 -682
  154. package/src/verax/learn/ast-contract-extractor.js +1 -1
  155. package/src/verax/learn/flow-extractor.js +1 -0
  156. package/src/verax/learn/project-detector.js +5 -0
  157. package/src/verax/learn/react-router-extractor.js +2 -0
  158. package/src/verax/learn/route-validator.js +1 -4
  159. package/src/verax/learn/source-instrumenter.js +1 -0
  160. package/src/verax/learn/state-extractor.js +2 -1
  161. package/src/verax/learn/static-extractor.js +1 -0
  162. package/src/verax/observe/coverage-gaps.js +132 -0
  163. package/src/verax/observe/expectation-handler.js +126 -0
  164. package/src/verax/observe/incremental-skip.js +46 -0
  165. package/src/verax/observe/index.js +735 -84
  166. package/src/verax/observe/interaction-executor.js +192 -0
  167. package/src/verax/observe/interaction-runner.js +782 -530
  168. package/src/verax/observe/network-firewall.js +86 -0
  169. package/src/verax/observe/observation-builder.js +169 -0
  170. package/src/verax/observe/observe-context.js +1 -1
  171. package/src/verax/observe/observe-helpers.js +2 -1
  172. package/src/verax/observe/observe-runner.js +28 -24
  173. package/src/verax/observe/observers/budget-observer.js +3 -3
  174. package/src/verax/observe/observers/console-observer.js +4 -4
  175. package/src/verax/observe/observers/coverage-observer.js +4 -4
  176. package/src/verax/observe/observers/interaction-observer.js +3 -3
  177. package/src/verax/observe/observers/navigation-observer.js +4 -4
  178. package/src/verax/observe/observers/network-observer.js +4 -4
  179. package/src/verax/observe/observers/safety-observer.js +1 -1
  180. package/src/verax/observe/observers/ui-feedback-observer.js +4 -4
  181. package/src/verax/observe/page-traversal.js +138 -0
  182. package/src/verax/observe/snapshot-ops.js +94 -0
  183. package/src/verax/observe/ui-signal-sensor.js +2 -148
  184. package/src/verax/scan-summary-writer.js +10 -42
  185. package/src/verax/shared/artifact-manager.js +30 -13
  186. package/src/verax/shared/caching.js +1 -0
  187. package/src/verax/shared/expectation-tracker.js +1 -0
  188. package/src/verax/shared/zip-artifacts.js +6 -0
  189. package/src/verax/core/confidence-engine.js.backup +0 -471
  190. package/src/verax/shared/config-loader.js +0 -169
  191. /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
@@ -1,21 +1,11 @@
1
1
  import { readFileSync, existsSync } from 'fs';
2
- import { dirname, basename, join } from 'path';
2
+ import { dirname, basename } from 'path';
3
3
  import { expectsNavigation } from './expectation-model.js';
4
4
  import { hasMeaningfulUrlChange, hasVisibleChange, hasDomChange } from './comparison.js';
5
5
  import { writeFindings } from './findings-writer.js';
6
6
  import { getUrlPath } from './evidence-validator.js';
7
7
  import { classifySkipReason, collectSkipReasons } from './skip-classifier.js';
8
8
  import { detectInteractiveFindings } from './interactive-findings.js';
9
- import { detectRouteFindings } from './route-findings.js';
10
- import { detectUIFeedbackFindings } from './ui-feedback-findings.js';
11
- import { detectDynamicRouteFindings } from './dynamic-route-findings.js';
12
- import { addUnifiedConfidence } from './confidence-helper.js';
13
- import { applyGuardrails } from '../core/guardrails-engine.js';
14
- import { finalizeFindingTruth } from '../core/guardrails/truth-reconciliation.js';
15
- import { writeGuardrailsReport } from '../core/guardrails/guardrails-report-writer.js';
16
- import { computeFinalConfidence } from '../core/confidence/confidence-compute.js';
17
- import { enforceConfidenceInvariants } from '../core/confidence/confidence-invariants.js';
18
- import { writeConfidenceReport } from '../core/confidence/confidence-report-writer.js';
19
9
 
20
10
  /**
21
11
  * @param {string} manifestPath
@@ -35,7 +25,9 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
35
25
  const manifestContent = readFileSync(manifestPath, 'utf-8');
36
26
  const tracesContent = readFileSync(tracesPath, 'utf-8');
37
27
 
28
+ // @ts-expect-error - readFileSync with encoding returns string
38
29
  const manifest = JSON.parse(manifestContent);
30
+ // @ts-expect-error - readFileSync with encoding returns string
39
31
  const observation = JSON.parse(tracesContent);
40
32
 
41
33
  const projectDir = manifest.projectDir;
@@ -108,20 +100,20 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
108
100
  selectorHint.includes(interactionSelector) ||
109
101
  interactionSelector.includes(normalizedSelectorHint) ||
110
102
  normalizedSelectorHint === normalizedInteractionSelector) {
111
- if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
103
+ if ((expectation.type === 'navigation' || expectation.type === 'spa_navigation') && (interaction.type === 'link' || interaction.type === 'button')) {
112
104
  matchingExpectations.push(expectation);
113
105
  } else if (expectation.type === 'form_submission' && interaction.type === 'form') {
114
106
  matchingExpectations.push(expectation);
115
107
  }
116
108
  } else {
117
- if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
109
+ if ((expectation.type === 'navigation' || expectation.type === 'spa_navigation') && (interaction.type === 'link' || interaction.type === 'button')) {
118
110
  selectorMismatch = true;
119
111
  } else if (expectation.type === 'form_submission' && interaction.type === 'form') {
120
112
  selectorMismatch = true;
121
113
  }
122
114
  }
123
115
  } else if (!selectorHint && !interactionSelector) {
124
- if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
116
+ if ((expectation.type === 'navigation' || expectation.type === 'spa_navigation') && (interaction.type === 'link' || interaction.type === 'button')) {
125
117
  matchingExpectations.push(expectation);
126
118
  } else if (expectation.type === 'form_submission' && interaction.type === 'form') {
127
119
  matchingExpectations.push(expectation);
@@ -132,9 +124,24 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
132
124
 
133
125
  if (matchingExpectations.length > 1) {
134
126
  multipleMatches = true;
127
+ // VISION TRANSPARENCY: Record ambiguity explicitly (not silent skip)
128
+ if (_silenceTracker) {
129
+ _silenceTracker.record({
130
+ scope: 'expectation',
131
+ reason: 'ambiguous_promise',
132
+ description: `Multiple expectations match interaction "${interaction.label}" (${matchingExpectations.length} candidates). Cannot determine intent without guessing.`,
133
+ context: {
134
+ interaction: { type: interaction.type, selector: interaction.selector, label: interaction.label },
135
+ candidateCount: matchingExpectations.length,
136
+ candidates: matchingExpectations.map(e => e.targetPath)
137
+ },
138
+ impact: 'interaction_not_evaluated',
139
+ outcome: 'UNPROVEN_INTERACTION'
140
+ });
141
+ }
135
142
  } else if (matchingExpectations.length === 1) {
136
143
  expectedTargetPath = matchingExpectations[0].targetPath;
137
- expectationType = matchingExpectations[0].type;
144
+ expectationType = matchingExpectations[0].type === 'spa_navigation' ? 'navigation' : matchingExpectations[0].type;
138
145
  } else if (selectorMismatch) {
139
146
  skips.push({
140
147
  code: 'SELECTOR_MISMATCH',
@@ -179,6 +186,21 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
179
186
  }
180
187
 
181
188
  if (matchingRoutes.length > 1) {
189
+ // VISION TRANSPARENCY: Record ambiguity explicitly
190
+ if (_silenceTracker) {
191
+ _silenceTracker.record({
192
+ scope: 'expectation',
193
+ reason: 'ambiguous_promise',
194
+ description: `Multiple routes match interaction "${interaction.label}" (${matchingRoutes.length} candidates). Conservative approach requires single clear match.`,
195
+ context: {
196
+ interaction: { type: interaction.type, selector: interaction.selector, label: interaction.label },
197
+ candidateCount: matchingRoutes.length,
198
+ candidates: matchingRoutes
199
+ },
200
+ impact: 'interaction_not_evaluated',
201
+ outcome: 'UNPROVEN_INTERACTION'
202
+ });
203
+ }
182
204
  skips.push({
183
205
  code: 'AMBIGUOUS_MATCH',
184
206
  message: 'Multiple expectations could match; conservative approach requires single clear match',
@@ -197,6 +219,7 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
197
219
  }
198
220
 
199
221
  if (multipleMatches) {
222
+ // VISION TRANSPARENCY: Ambiguity already recorded in silence tracker above
200
223
  skips.push({
201
224
  code: 'AMBIGUOUS_MATCH',
202
225
  message: 'Multiple expectations could match; conservative approach requires single clear match',
@@ -231,7 +254,7 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
231
254
 
232
255
  if (expectationType === 'form_submission') {
233
256
  if (normalizedAfter !== normalizedTarget && !hasUrlChange && !hasDomChangeResult) {
234
- const finding = {
257
+ findings.push({
235
258
  type: 'silent_failure',
236
259
  interaction: {
237
260
  type: interaction.type,
@@ -245,27 +268,7 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
245
268
  beforeUrl: beforeUrl,
246
269
  afterUrl: afterUrl
247
270
  }
248
- };
249
-
250
- // PHASE 15: Add unified confidence
251
- const findingWithConfidence = addUnifiedConfidence(finding, {
252
- expectation: { targetPath: expectedTargetPath, type: expectationType },
253
- sensors: trace.sensors || {},
254
- comparisons: {
255
- urlChanged: hasUrlChange,
256
- domChanged: hasDomChangeResult,
257
- },
258
- evidence: {
259
- beforeAfter: {
260
- beforeScreenshot,
261
- afterScreenshot,
262
- beforeUrl,
263
- afterUrl,
264
- },
265
- },
266
271
  });
267
-
268
- findings.push(findingWithConfidence);
269
272
  }
270
273
  } else if (expectationType === 'navigation') {
271
274
  const urlMatchesTarget = normalizedAfter === normalizedTarget;
@@ -273,13 +276,17 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
273
276
 
274
277
  if (!hasEffect) {
275
278
  findings.push({
276
- type: 'silent_failure',
279
+ type: 'navigation_silent_failure',
277
280
  interaction: {
278
281
  type: interaction.type,
279
282
  selector: interaction.selector,
280
283
  label: interaction.label
281
284
  },
282
285
  reason: 'Expected user-visible outcome did not occur',
286
+ what_happened: 'Navigation attempt produced no visible effect',
287
+ what_was_expected: `Navigate to ${normalizedTarget || 'target page'}`,
288
+ what_was_observed: 'URL, DOM, and visuals remained unchanged',
289
+ why_it_matters: 'Users cannot reach the intended destination despite interacting',
283
290
  evidence: {
284
291
  before: beforeScreenshot,
285
292
  after: afterScreenshot,
@@ -291,7 +298,7 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
291
298
  }
292
299
  } else {
293
300
  if (!hasUrlChange && !hasVisibleChangeResult && !hasDomChangeResult) {
294
- const finding = {
301
+ findings.push({
295
302
  type: 'silent_failure',
296
303
  interaction: {
297
304
  type: interaction.type,
@@ -299,34 +306,17 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
299
306
  label: interaction.label
300
307
  },
301
308
  reason: 'Expected user-visible outcome did not occur',
309
+ what_happened: 'User action produced no visible effect',
310
+ what_was_expected: 'Some user-visible change after interaction',
311
+ what_was_observed: 'URL, DOM, and visuals remained unchanged',
312
+ why_it_matters: 'Users cannot complete the intended action',
302
313
  evidence: {
303
314
  before: beforeScreenshot,
304
315
  after: afterScreenshot,
305
316
  beforeUrl: beforeUrl,
306
317
  afterUrl: afterUrl
307
318
  }
308
- };
309
-
310
- // PHASE 15: Add unified confidence
311
- const findingWithConfidence = addUnifiedConfidence(finding, {
312
- expectation: null,
313
- sensors: trace.sensors || {},
314
- comparisons: {
315
- urlChanged: hasUrlChange,
316
- domChanged: hasDomChangeResult,
317
- visibleChanged: hasVisibleChangeResult,
318
- },
319
- evidence: {
320
- beforeAfter: {
321
- beforeScreenshot,
322
- afterScreenshot,
323
- beforeUrl,
324
- afterUrl,
325
- },
326
- },
327
319
  });
328
-
329
- findings.push(findingWithConfidence);
330
320
  }
331
321
  }
332
322
  }
@@ -334,187 +324,16 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
334
324
  // Interactive and accessibility intelligence
335
325
  detectInteractiveFindings(observation.traces, manifest, findings);
336
326
 
337
- // PHASE 12: Route intelligence findings
338
- const routeFindings = detectRouteFindings(observation.traces, manifest, findings);
339
- findings.push(...routeFindings);
340
-
341
- // PHASE 13: UI feedback findings
342
- const uiFeedbackFindings = detectUIFeedbackFindings(observation.traces, manifest, findings);
343
- findings.push(...uiFeedbackFindings);
344
-
345
- // PHASE 14: Dynamic route findings
346
- const dynamicRouteResult = detectDynamicRouteFindings(observation.traces, manifest, findings);
347
- findings.push(...dynamicRouteResult.findings);
348
- // Note: skips are handled separately and should be included in skip summary
349
-
350
- // PHASE 23: Apply guardrails + truth reconciliation (AFTER evidence builder, BEFORE writing artifacts)
351
- const guardrailsSummary = {
352
- totalFindingsProcessed: 0,
353
- preventedConfirmedCount: 0,
354
- downgradedCount: 0,
355
- informationalCount: 0,
356
- };
357
-
358
- const truthDecisions = {}; // Map of findingIdentity -> truthDecision
359
-
360
- const findingsWithGuardrails = findings.map(finding => {
361
- guardrailsSummary.totalFindingsProcessed++;
362
-
363
- // Capture initial confidence before guardrails
364
- const initialConfidence = finding.confidence || 0;
365
- const initialConfidenceLevel = finding.confidenceLevel ||
366
- (initialConfidence >= 0.8 ? 'HIGH' : initialConfidence >= 0.5 ? 'MEDIUM' : initialConfidence >= 0.2 ? 'LOW' : 'UNPROVEN');
367
-
368
- // Build context for guardrails
369
- const context = {
370
- evidencePackage: finding.evidencePackage,
371
- signals: finding.evidencePackage?.signals || finding.evidence || {},
372
- confidenceReasons: finding.confidenceReasons || [],
373
- promiseType: finding.expectation?.type || finding.promise?.type || null,
374
- };
375
-
376
- // Apply guardrails
377
- const guardrailsResult = applyGuardrails(finding, context);
378
-
379
- // Finalize truth (reconcile confidence with guardrails outcome)
380
- const { finalFinding, truthDecision } = finalizeFindingTruth(
381
- guardrailsResult.finding,
382
- guardrailsResult,
383
- {
384
- initialConfidence,
385
- initialConfidenceLevel
386
- }
387
- );
388
-
389
- // Store truth decision for report
390
- const findingIdentity = finalFinding.findingId || finalFinding.id || `finding-${findings.indexOf(finding)}`;
391
- truthDecisions[findingIdentity] = truthDecision;
392
-
393
- // Track guardrails impact
394
- const originalSeverity = finding.severity || 'SUSPECTED';
395
- const finalSeverity = truthDecision.finalStatus;
396
-
397
- if (finalSeverity !== originalSeverity && originalSeverity === 'CONFIRMED') {
398
- guardrailsSummary.preventedConfirmedCount++;
399
- }
400
- if (finalSeverity === 'SUSPECTED' && originalSeverity === 'CONFIRMED') {
401
- guardrailsSummary.downgradedCount++;
402
- }
403
- if (finalSeverity === 'INFORMATIONAL') {
404
- guardrailsSummary.informationalCount++;
405
- }
406
-
407
- return finalFinding;
408
- });
409
-
410
- // Replace findings with guardrails-applied + truth-reconciled findings
411
- findings.length = 0;
412
- findings.push(...findingsWithGuardrails);
413
-
414
- // PHASE 24: Apply confidence invariants and compute final confidence
415
- const confidenceData = {}; // Map of findingIdentity -> confidence computation result
416
-
417
- // Load evidence intent if available
418
- let evidenceIntentLedger = null;
327
+ // Infer canonical run directory from tracesPath when available
419
328
  let runDir = null;
420
329
  try {
421
330
  runDir = dirname(tracesPath);
422
- const evidenceIntentPath = join(runDir, 'evidence.intent.json');
423
- if (existsSync(evidenceIntentPath)) {
424
- try {
425
- const intentContent = readFileSync(evidenceIntentPath, 'utf-8');
426
- evidenceIntentLedger = JSON.parse(intentContent);
427
- } catch {
428
- // Ignore parse errors
429
- }
430
- }
431
331
  } catch {
432
332
  // Ignore path parsing errors
433
333
  }
434
-
435
- const findingsWithConfidence = findings.map(finding => {
436
- const findingIdentity = finding.findingId || finding.id || `finding-${findings.indexOf(finding)}`;
437
-
438
- // Get evidence intent entry for this finding
439
- const evidenceIntentEntry = evidenceIntentLedger?.entries?.find(e => e.findingIdentity === findingIdentity) || null;
440
-
441
- // Get guardrails outcome
442
- const guardrailsOutcome = finding.guardrails || truthDecisions[findingIdentity] || null;
443
-
444
- // Compute final confidence
445
- const confidenceResult = computeFinalConfidence({
446
- findingType: finding.type || 'unknown',
447
- rawSignals: finding.evidencePackage?.signals || finding.evidence || {},
448
- evidenceIntent: evidenceIntentEntry,
449
- guardrailsOutcome,
450
- truthStatus: finding.severity || finding.status || 'SUSPECTED',
451
- expectation: finding.expectation || null,
452
- sensors: finding.evidencePackage?.signals || finding.evidence || {},
453
- comparisons: {},
454
- evidence: finding.evidence || {},
455
- options: {
456
- verificationStatus: null // Will be set by verifier
457
- }
458
- });
459
-
460
- // Store confidence data for report
461
- confidenceData[findingIdentity] = confidenceResult;
462
-
463
- // Enforce invariants
464
- const invariantResult = enforceConfidenceInvariants(finding, {
465
- expectationProof: confidenceResult.expectationProof,
466
- verificationStatus: confidenceResult.verificationStatus,
467
- guardrailsOutcome
468
- });
469
-
470
- // Update finding with final confidence
471
- const finalFinding = {
472
- ...invariantResult.finding,
473
- confidence: confidenceResult.confidenceAfter,
474
- confidenceLevel: confidenceResult.confidenceLevel,
475
- confidenceReasons: confidenceResult.explanation
476
- };
477
-
478
- return finalFinding;
479
- });
480
-
481
- // Replace findings with confidence-enforced findings
482
- findings.length = 0;
483
- findings.push(...findingsWithConfidence);
484
-
485
- // Infer canonical run directory from tracesPath when available
486
- if (!runDir) {
487
- try {
488
- runDir = dirname(tracesPath);
489
- } catch {
490
- // Ignore path parsing errors
491
- }
492
- }
493
334
 
494
335
  const findingsResult = writeFindings(projectDir, observation.url, findings, [], runDir);
495
336
 
496
- // PHASE 23: Write guardrails report
497
- let guardrailsReportPath = null;
498
- if (runDir) {
499
- try {
500
- guardrailsReportPath = writeGuardrailsReport(runDir, findings, truthDecisions);
501
- } catch (error) {
502
- // Log but don't fail the run
503
- console.error('Failed to write guardrails report:', error.message);
504
- }
505
- }
506
-
507
- // PHASE 24: Write confidence report
508
- let confidenceReportPath = null;
509
- if (runDir) {
510
- try {
511
- confidenceReportPath = writeConfidenceReport(runDir, findings, confidenceData);
512
- } catch (error) {
513
- // Log but don't fail the run
514
- console.error('Failed to write confidence report:', error.message);
515
- }
516
- }
517
-
518
337
  const skipSummary = collectSkipReasons(skips);
519
338
 
520
339
  const detectTruth = {
@@ -526,8 +345,6 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
526
345
 
527
346
  return {
528
347
  ...findingsResult,
529
- detectTruth: detectTruth,
530
- guardrailsSummary: guardrailsSummary, // PHASE 17: Include guardrails summary
531
- guardrailsReportPath: guardrailsReportPath // PHASE 23: Include guardrails report path
348
+ detectTruth: detectTruth
532
349
  };
533
350
  }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Phase 4: Final Output Invariants (Trust Lock)
3
+ *
4
+ * Strict gate applied only to user-facing outputs (REPORT.json, SUMMARY.md, console).
5
+ * Findings violating any invariant are dropped silently. Purity and determinism are required.
6
+ */
7
+
8
+ const KNOWN_IMPACTS = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO', 'UNKNOWN'];
9
+ const VALID_STATUSES = ['CONFIRMED', 'SUSPECTED', 'INFORMATIONAL'];
10
+
11
+ function isValidEvidence(evidence) {
12
+ if (!evidence || typeof evidence !== 'object') return false;
13
+ const keys = Object.keys(evidence);
14
+ if (keys.length === 0) return false;
15
+
16
+ const signals = Object.values(evidence).filter(v => {
17
+ if (v === true || v === false) return true;
18
+ if (typeof v === 'number' && v !== 0) return true;
19
+ if (typeof v === 'string' && v.length > 0) return true;
20
+ if (Array.isArray(v)) return v.length > 0; // arrays must contain at least one element
21
+ if (v && typeof v === 'object') return Object.keys(v).length > 0;
22
+ return false;
23
+ });
24
+
25
+ return signals.length > 0;
26
+ }
27
+
28
+ function isValidConfidence(confidence) {
29
+ if (confidence === 0) return true;
30
+ if (!confidence) return false;
31
+
32
+ if (typeof confidence === 'number') {
33
+ return confidence >= 0 && confidence <= 1;
34
+ }
35
+
36
+ if (typeof confidence === 'object') {
37
+ const hasLevel = (typeof confidence.level === 'string' && confidence.level.length > 0) || typeof confidence.level === 'number';
38
+ const hasScore = typeof confidence.score === 'number';
39
+ return hasLevel || hasScore;
40
+ }
41
+
42
+ return false;
43
+ }
44
+
45
+ function isValidPromise(promise) {
46
+ if (!promise || typeof promise !== 'object') return false;
47
+
48
+ const { kind, value, type, expected, actual, expected_signal } = promise;
49
+
50
+ const kindValueValid = typeof kind === 'string' && kind.length > 0 && typeof value === 'string' && value.length > 0;
51
+ const typeValid = typeof type === 'string' && type.length > 0;
52
+ const expectationValid = (typeof expected === 'string' && expected.length > 0) ||
53
+ (typeof actual === 'string' && actual.length > 0) ||
54
+ (typeof expected_signal === 'string' && expected_signal.length > 0);
55
+
56
+ return kindValueValid || (typeValid && expectationValid);
57
+ }
58
+
59
+ function isInternalErrorFlag(finding) {
60
+ const internalMarkers = [
61
+ 'INTERNAL_ERROR',
62
+ 'internal-error',
63
+ 'internalError',
64
+ 'TIMEOUT_ERROR',
65
+ 'CRASH',
66
+ 'FATAL',
67
+ 'BROWSER_CRASH'
68
+ ];
69
+
70
+ if (internalMarkers.some(marker =>
71
+ finding.reason?.includes?.(marker) ||
72
+ finding.errorMessage?.includes?.(marker) ||
73
+ finding.errorStack?.includes?.(marker)
74
+ )) {
75
+ return true;
76
+ }
77
+
78
+ if (finding.reason?.toLowerCase?.().includes('internal error')) {
79
+ return true;
80
+ }
81
+
82
+ return false;
83
+ }
84
+
85
+ function isValidCause(cause, evidence) {
86
+ if (!cause || typeof cause !== 'object') return false;
87
+ if (!cause.id || !cause.statement) return false;
88
+
89
+ if (Array.isArray(cause.evidence_refs) && cause.evidence_refs.length > 0) {
90
+ if (!evidence || typeof evidence !== 'object') return false;
91
+ return cause.evidence_refs.every(ref => typeof ref === 'string' && ref in evidence);
92
+ }
93
+
94
+ return true;
95
+ }
96
+
97
+ export function filterCausesWithEvidence(evidence, causes) {
98
+ if (!Array.isArray(causes)) return [];
99
+ return causes.filter(cause => isValidCause(cause, evidence));
100
+ }
101
+
102
+ function enforceFinalInvariant(finding) {
103
+ if (!finding) return null;
104
+
105
+ if (!isValidEvidence(finding.evidence)) return null;
106
+ if (!isValidPromise(finding.promise)) return null;
107
+ if (!isValidConfidence(finding.confidence)) return null;
108
+ if (finding.impact && !KNOWN_IMPACTS.includes(finding.impact)) return null;
109
+ if (isInternalErrorFlag(finding)) return null;
110
+ if (!finding.id || typeof finding.id !== 'string' || finding.id.length === 0) return null;
111
+ if (finding.status && !VALID_STATUSES.includes(finding.status)) return null;
112
+
113
+ const causes = filterCausesWithEvidence(finding.evidence, finding.causes);
114
+
115
+ return { ...finding, causes };
116
+ }
117
+
118
+ export function deduplicateFindings(findings) {
119
+ const seen = new Set();
120
+ const deduped = [];
121
+
122
+ for (const finding of findings) {
123
+ const promiseKey = finding.promise ? JSON.stringify(finding.promise) : 'nopromise';
124
+ const key = `${finding.id || 'unknown'}|${finding.location || 'unknown'}|${promiseKey}`;
125
+ if (seen.has(key)) continue;
126
+ seen.add(key);
127
+ deduped.push(finding);
128
+ }
129
+
130
+ return deduped;
131
+ }
132
+
133
+ export function enforceFinalInvariants(findings) {
134
+ if (!Array.isArray(findings)) return [];
135
+
136
+ const validFindings = findings
137
+ .map(enforceFinalInvariant)
138
+ .filter(Boolean);
139
+
140
+ const deduped = deduplicateFindings(validFindings);
141
+
142
+ return deduped.sort((a, b) => {
143
+ const aId = (a.id || '').toString();
144
+ const bId = (b.id || '').toString();
145
+ return aId.localeCompare(bId);
146
+ });
147
+ }
@@ -86,7 +86,7 @@ export class JourneyStallDetector {
86
86
  // 2. Next trace shows successful navigation (end of journey segment)
87
87
  // 3. We're at the end of traces
88
88
  const isLastTrace = i === traces.length - 1;
89
- const nextTrace = !isLastTrace ? traces[i + 1] : null;
89
+ const _nextTrace = !isLastTrace ? traces[i + 1] : null;
90
90
  const navigationOccurred = trace.sensors?.navigation?.urlChanged === true;
91
91
 
92
92
  if (
@@ -138,7 +138,7 @@ export class JourneyStallDetector {
138
138
  * Detect if there's a stall between two consecutive steps
139
139
  * @private
140
140
  */
141
- _detectStallBetweenSteps(current, next, stepIndex, sequence) {
141
+ _detectStallBetweenSteps(current, next, stepIndex, _sequence) {
142
142
  const reasons = [];
143
143
  const evidence = [];
144
144
 
@@ -427,7 +427,7 @@ export class JourneyStallDetector {
427
427
  * Calculate severity level for stall
428
428
  * @private
429
429
  */
430
- _calculateSeverity(reasons, current, next) {
430
+ _calculateSeverity(reasons, _current, _next) {
431
431
  let score = 0;
432
432
 
433
433
  // Multiple reasons = higher severity
@@ -459,7 +459,7 @@ export class JourneyStallDetector {
459
459
  * @private
460
460
  */
461
461
  _generateStallFinding(sequence, stallPoints) {
462
- const firstTrace = sequence[0].trace;
462
+ const _firstTrace = sequence[0].trace;
463
463
  const lastTrace = sequence[sequence.length - 1].trace;
464
464
 
465
465
  const id = `journey-stall-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Navigation Silent Failure Detection
3
+ *
4
+ * Detects navigation actions where:
5
+ * - URL changes to expected route
6
+ * - AND no meaningful DOM change occurs (loading state never resolves)
7
+ * - AND expected anchor (h1, title, route container) does not appear
8
+ *
9
+ * CONFIDENCE: HIGH (URL + DOM evidence)
10
+ * PARTIAL SUPPORT: Relies on heuristic anchor detection
11
+ */
12
+
13
+ import { hasMeaningfulUrlChange, hasDomChange } from './comparison.js';
14
+ import { enrichFindingWithExplanations } from './finding-detector.js';
15
+
16
+ export function detectNavigationSilentFailures(traces, manifest, findings) {
17
+ // Parameters:
18
+ // traces - array of interaction traces from observation
19
+ // manifest - project manifest (contains expectations)
20
+ // findings - array to append new findings to (mutated in-place)
21
+
22
+ for (const trace of traces) {
23
+ const interaction = trace.interaction || {};
24
+
25
+ // Only analyze navigation interactions
26
+ if (interaction.type !== 'navigation' && interaction.type !== 'link') {
27
+ continue;
28
+ }
29
+
30
+ const beforeUrl = trace.before?.url || trace.beforeUrl || '';
31
+ const afterUrl = trace.after?.url || trace.afterUrl || '';
32
+ const beforePage = trace.page?.beforeTitle || trace.before?.title || '';
33
+ const afterPage = trace.page?.afterTitle || trace.after?.title || '';
34
+
35
+ const urlChanged = hasMeaningfulUrlChange(beforeUrl, afterUrl);
36
+ const domChanged = hasDomChange(trace);
37
+
38
+ // Detection logic:
39
+ // URL changed (navigation happened) but:
40
+ // 1. DOM signature didn't change (content not rendered)
41
+ // 2. Page title is the same or empty (no new page loaded)
42
+ if (urlChanged && !domChanged && beforePage === afterPage) {
43
+ const evidence = {
44
+ before: trace.before?.screenshot || trace.beforeScreenshot || '',
45
+ after: trace.after?.screenshot || trace.afterScreenshot || '',
46
+ beforeUrl,
47
+ afterUrl,
48
+ beforePageTitle: beforePage,
49
+ afterPageTitle: afterPage,
50
+ domChanged,
51
+ urlChanged,
52
+ reason: 'URL changed but content did not render (likely loading state)'
53
+ };
54
+
55
+ const finding = {
56
+ type: 'navigation_silent_failure',
57
+ description: `Navigation to ${afterUrl} succeeded but content did not render`,
58
+ summary: `URL updated to route but component content remains unrendered (likely stuck in loading state)`,
59
+ explanation: `The navigation route changed from ${beforeUrl} to ${afterUrl}, but the DOM signature remained identical and page title unchanged. This suggests the component is stuck in a loading state.`,
60
+ evidence,
61
+ confidence: {
62
+ level: 0.85, // HIGH - URL + DOM evidence
63
+ reasons: [
64
+ 'URL changed to expected route',
65
+ 'DOM signature unchanged (content not rendered)',
66
+ 'Page title unchanged'
67
+ ]
68
+ },
69
+ promise: {
70
+ type: 'navigation',
71
+ expected: `Navigate to ${afterUrl} and render content`,
72
+ actual: 'Navigation succeeded but content did not render'
73
+ }
74
+ };
75
+
76
+ // Enrich with explanations
77
+ enrichFindingWithExplanations(finding, trace);
78
+ findings.push(finding);
79
+ }
80
+ }
81
+ }
82
+