@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
@@ -0,0 +1,505 @@
1
+ /**
2
+ * PHASE 21.4 — Guardrails Engine (Policy-Driven)
3
+ *
4
+ * Central guardrails engine that prevents false CONFIRMED findings
5
+ * by enforcing policy-driven rules.
6
+ *
7
+ * All rules are mandatory and cannot be disabled.
8
+ */
9
+
10
+ import { loadGuardrailsPolicy, getPolicyReport } from './guardrails/policy.loader.js';
11
+ import { GUARDRAILS_RULE } from './guardrails/policy.defaults.js';
12
+
13
+ // Re-export for backward compatibility
14
+ export { GUARDRAILS_RULE };
15
+
16
+ /**
17
+ * PHASE 17: Guardrails Severity Levels
18
+ */
19
+ export const GUARDRAILS_SEVERITY = {
20
+ BLOCK_CONFIRMED: 'BLOCK_CONFIRMED', // Prevents CONFIRMED status
21
+ DOWNGRADE: 'DOWNGRADE', // Recommends downgrade
22
+ INFORMATIONAL: 'INFORMATIONAL', // Makes finding informational
23
+ DROP: 'DROP', // Recommends dropping finding
24
+ WARNING: 'WARNING', // Warning only, no status change
25
+ };
26
+
27
+ // Global policy cache (loaded once per process)
28
+ let cachedPolicy = null;
29
+
30
+ /**
31
+ * Get guardrails policy (cached)
32
+ *
33
+ * @param {string|null} policyPath - Custom policy path (optional)
34
+ * @param {string} projectDir - Project directory
35
+ * @returns {Object} Guardrails policy
36
+ */
37
+ function getGuardrailsPolicy(policyPath = null, projectDir = null) {
38
+ if (!cachedPolicy) {
39
+ cachedPolicy = loadGuardrailsPolicy(policyPath, projectDir);
40
+ }
41
+ return cachedPolicy;
42
+ }
43
+
44
+ /**
45
+ * PHASE 21.4: Apply guardrails to a finding using policy
46
+ *
47
+ * @param {Object} finding - Finding object
48
+ * @param {Object} context - Context including evidencePackage, signals, confidenceReasons, promise type
49
+ * @param {Object} options - Options { policyPath, projectDir }
50
+ * @returns {Object} { finding: updatedFinding, guardrails: report }
51
+ */
52
+ export function applyGuardrails(finding, context = {}, options = {}) {
53
+ const evidencePackage = context.evidencePackage || finding.evidencePackage || {};
54
+ const signals = context.signals || evidencePackage.signals || {};
55
+ const confidenceReasons = context.confidenceReasons || finding.confidenceReasons || [];
56
+ const promiseType = context.promiseType || finding.expectation?.type || finding.promise?.type || null;
57
+
58
+ // Load policy
59
+ const policy = getGuardrailsPolicy(options.policyPath, options.projectDir);
60
+ const policyReport = getPolicyReport(policy);
61
+
62
+ const appliedRules = [];
63
+ const contradictions = [];
64
+ let recommendedStatus = finding.severity || finding.status || 'SUSPECTED';
65
+ const confidenceAdjustments = [];
66
+ let confidenceDelta = 0;
67
+
68
+ // Apply rules in deterministic order (by rule id)
69
+ const sortedRules = [...policy.rules].sort((a, b) => a.id.localeCompare(b.id));
70
+
71
+ for (const rule of sortedRules) {
72
+ // Check if rule applies to this finding type
73
+ const appliesToFinding = rule.appliesTo.includes('*') ||
74
+ rule.appliesTo.some(cap => finding.type?.includes(cap));
75
+
76
+ if (!appliesToFinding) {
77
+ continue;
78
+ }
79
+
80
+ // Evaluate rule
81
+ const evaluation = evaluateRule(rule, finding, signals, evidencePackage);
82
+
83
+ if (evaluation.applies) {
84
+ appliedRules.push({
85
+ code: rule.id,
86
+ severity: mapActionToSeverity(rule.action),
87
+ message: evaluation.message,
88
+ ruleId: rule.id,
89
+ category: rule.category
90
+ });
91
+
92
+ if (evaluation.contradiction) {
93
+ contradictions.push({
94
+ code: rule.id,
95
+ message: evaluation.message,
96
+ });
97
+ }
98
+
99
+ if (evaluation.recommendedStatus) {
100
+ recommendedStatus = evaluation.recommendedStatus;
101
+ }
102
+
103
+ const delta = rule.confidenceDelta || 0;
104
+ confidenceDelta += delta;
105
+
106
+ if (delta !== 0) {
107
+ confidenceAdjustments.push({
108
+ reason: rule.id,
109
+ delta: delta,
110
+ message: evaluation.message,
111
+ });
112
+ }
113
+ }
114
+ }
115
+
116
+ // Apply confidence adjustments
117
+ let finalConfidence = finding.confidence || 0;
118
+ if (confidenceDelta !== 0) {
119
+ finalConfidence = Math.max(0, Math.min(1, finalConfidence + confidenceDelta));
120
+ }
121
+
122
+ // Build guardrails report with policy metadata
123
+ const guardrailsReport = {
124
+ appliedRules,
125
+ contradictions,
126
+ recommendedStatus,
127
+ confidenceAdjustments,
128
+ confidenceDelta,
129
+ finalDecision: recommendedStatus,
130
+ policyReport: {
131
+ version: policyReport.version,
132
+ source: policyReport.source,
133
+ appliedRuleIds: appliedRules.map(r => r.code)
134
+ }
135
+ };
136
+
137
+ // Update finding
138
+ const updatedFinding = {
139
+ ...finding,
140
+ severity: recommendedStatus,
141
+ status: recommendedStatus, // Also update status for backward compatibility
142
+ confidence: finalConfidence,
143
+ guardrails: guardrailsReport,
144
+ };
145
+
146
+ return {
147
+ finding: updatedFinding,
148
+ guardrails: guardrailsReport,
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Map policy action to severity
154
+ */
155
+ function mapActionToSeverity(action) {
156
+ const mapping = {
157
+ 'BLOCK': GUARDRAILS_SEVERITY.BLOCK_CONFIRMED,
158
+ 'DOWNGRADE': GUARDRAILS_SEVERITY.DOWNGRADE,
159
+ 'INFO': GUARDRAILS_SEVERITY.INFORMATIONAL
160
+ };
161
+ return mapping[action] || GUARDRAILS_SEVERITY.WARNING;
162
+ }
163
+
164
+ /**
165
+ * Evaluate a guardrails rule
166
+ *
167
+ * @param {Object} rule - Policy rule
168
+ * @param {Object} finding - Finding object
169
+ * @param {Object} signals - Sensor signals
170
+ * @param {Object} evidencePackage - Evidence package
171
+ * @returns {Object} { applies, message, contradiction, recommendedStatus }
172
+ */
173
+ function evaluateRule(rule, finding, signals, evidencePackage) {
174
+ const evalType = rule.evaluation.type;
175
+ const isConfirmed = finding.severity === 'CONFIRMED' || finding.status === 'CONFIRMED';
176
+
177
+ switch (evalType) {
178
+ case 'network_success_no_ui':
179
+ return evaluateNetSuccessNoUi(finding, signals, evidencePackage, isConfirmed);
180
+
181
+ case 'analytics_only':
182
+ return evaluateAnalyticsOnly(finding, signals, evidencePackage, isConfirmed);
183
+
184
+ case 'shallow_routing':
185
+ return evaluateShallowRouting(finding, signals, evidencePackage, isConfirmed);
186
+
187
+ case 'ui_feedback_present':
188
+ return evaluateUiFeedbackPresent(finding, signals, evidencePackage, isConfirmed);
189
+
190
+ case 'interaction_blocked':
191
+ return evaluateInteractionBlocked(finding, signals, evidencePackage, isConfirmed);
192
+
193
+ case 'validation_present':
194
+ return evaluateValidationPresent(finding, signals, evidencePackage, isConfirmed);
195
+
196
+ case 'contradict_evidence':
197
+ return evaluateContradictEvidence(finding, signals, evidencePackage, isConfirmed);
198
+
199
+ case 'view_switch_minor_change':
200
+ return evaluateViewSwitchMinorChange(finding, signals, evidencePackage, isConfirmed);
201
+
202
+ case 'view_switch_analytics_only':
203
+ return evaluateViewSwitchAnalyticsOnly(finding, signals, evidencePackage, isConfirmed);
204
+
205
+ case 'view_switch_ambiguous':
206
+ return evaluateViewSwitchAmbiguous(finding, signals, evidencePackage, isConfirmed);
207
+
208
+ default:
209
+ return { applies: false };
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Rule 1: GUARD_NET_SUCCESS_NO_UI
215
+ */
216
+ function evaluateNetSuccessNoUi(finding, signals, evidencePackage, isConfirmed) {
217
+ const networkSignals = signals.network || {};
218
+ const uiSignals = signals.uiSignals || {};
219
+ const uiFeedback = signals.uiFeedback || {};
220
+
221
+ const hasNetworkSuccess = networkSignals.successfulRequests > 0 &&
222
+ networkSignals.failedRequests === 0;
223
+ const hasNoUiChange = !uiSignals.changed &&
224
+ (!uiFeedback.overallUiFeedbackScore || uiFeedback.overallUiFeedbackScore < 0.3);
225
+ const hasNoErrors = !networkSignals.failedRequests &&
226
+ (!signals.console || signals.console.errorCount === 0);
227
+
228
+ const isSilentFailure = finding.type?.includes('silent_failure') ||
229
+ finding.type?.includes('network');
230
+
231
+ if (hasNetworkSuccess && hasNoUiChange && hasNoErrors && isSilentFailure && isConfirmed) {
232
+ return {
233
+ applies: true,
234
+ message: 'Network request succeeded but no UI change observed. This is not a silent failure.',
235
+ contradiction: true,
236
+ recommendedStatus: 'SUSPECTED',
237
+ };
238
+ }
239
+
240
+ return { applies: false };
241
+ }
242
+
243
+ /**
244
+ * Rule 2: GUARD_ANALYTICS_ONLY
245
+ */
246
+ function evaluateAnalyticsOnly(finding, signals, evidencePackage, isConfirmed) {
247
+ const networkSignals = signals.network || {};
248
+ const networkRequests = networkSignals.topFailedUrls ||
249
+ networkSignals.observedRequestUrls ||
250
+ [];
251
+
252
+ const isAnalyticsOnly = networkRequests.some(url => {
253
+ if (!url || typeof url !== 'string') return false;
254
+ const lowerUrl = url.toLowerCase();
255
+ return lowerUrl.includes('/analytics') ||
256
+ lowerUrl.includes('/beacon') ||
257
+ lowerUrl.includes('/tracking') ||
258
+ lowerUrl.includes('/pixel') ||
259
+ lowerUrl.includes('google-analytics') ||
260
+ lowerUrl.includes('segment.io') ||
261
+ lowerUrl.includes('mixpanel');
262
+ });
263
+
264
+ const isNetworkFinding = finding.type?.includes('network') ||
265
+ finding.type?.includes('silent_failure');
266
+
267
+ if (isAnalyticsOnly && isNetworkFinding && isConfirmed && networkRequests.length === 1) {
268
+ return {
269
+ applies: true,
270
+ message: 'Only analytics/beacon requests detected. These are not user promises.',
271
+ contradiction: true,
272
+ recommendedStatus: 'INFORMATIONAL',
273
+ };
274
+ }
275
+
276
+ return { applies: false };
277
+ }
278
+
279
+ /**
280
+ * Rule 3: GUARD_SHALLOW_ROUTING
281
+ */
282
+ function evaluateShallowRouting(finding, signals, evidencePackage, isConfirmed) {
283
+ const navigationSignals = signals.navigation || {};
284
+ const beforeUrl = evidencePackage.before?.url || '';
285
+ const afterUrl = evidencePackage.after?.url || '';
286
+
287
+ const isHashOnly = beforeUrl && afterUrl &&
288
+ beforeUrl.split('#')[0] === afterUrl.split('#')[0] &&
289
+ (beforeUrl.includes('#') || afterUrl.includes('#'));
290
+ const isShallowRouting = navigationSignals.shallowRouting === true &&
291
+ !navigationSignals.urlChanged;
292
+
293
+ const isNavigationFinding = finding.type?.includes('navigation') ||
294
+ finding.type?.includes('route');
295
+
296
+ if ((isHashOnly || isShallowRouting) && isNavigationFinding && isConfirmed) {
297
+ return {
298
+ applies: true,
299
+ message: 'Hash-only or shallow routing detected. Cannot confirm navigation without route intelligence verification.',
300
+ contradiction: true,
301
+ recommendedStatus: 'SUSPECTED',
302
+ };
303
+ }
304
+
305
+ return { applies: false };
306
+ }
307
+
308
+ /**
309
+ * Rule 4: GUARD_UI_FEEDBACK_PRESENT
310
+ */
311
+ function evaluateUiFeedbackPresent(finding, signals, evidencePackage, isConfirmed) {
312
+ const uiFeedback = signals.uiFeedback || {};
313
+ const uiSignals = signals.uiSignals || {};
314
+
315
+ const hasFeedback = (uiFeedback.overallUiFeedbackScore || 0) > 0.5 ||
316
+ uiSignals.hasLoadingIndicator ||
317
+ uiSignals.hasDialog ||
318
+ uiSignals.hasErrorSignal ||
319
+ uiSignals.changed;
320
+
321
+ const isSilentFailure = finding.type?.includes('silent_failure') ||
322
+ finding.type?.includes('feedback_missing');
323
+
324
+ if (hasFeedback && isSilentFailure && isConfirmed) {
325
+ return {
326
+ applies: true,
327
+ message: 'UI feedback is present. This contradicts a silent failure claim.',
328
+ contradiction: true,
329
+ recommendedStatus: 'SUSPECTED',
330
+ };
331
+ }
332
+
333
+ return { applies: false };
334
+ }
335
+
336
+ /**
337
+ * Rule 5: GUARD_INTERACTION_BLOCKED
338
+ */
339
+ function evaluateInteractionBlocked(finding, signals, evidencePackage, isConfirmed) {
340
+ const interaction = finding.interaction || {};
341
+ const action = evidencePackage.action || {};
342
+
343
+ const isDisabled = interaction.disabled === true ||
344
+ action.interaction?.disabled === true ||
345
+ finding.evidence?.interactionBlocked === true;
346
+
347
+ const isSilentFailure = finding.type?.includes('silent_failure');
348
+
349
+ if (isDisabled && isSilentFailure && isConfirmed) {
350
+ return {
351
+ applies: true,
352
+ message: 'Interaction was disabled/blocked. This is expected behavior, not a silent failure.',
353
+ recommendedStatus: 'INFORMATIONAL',
354
+ };
355
+ }
356
+
357
+ return { applies: false };
358
+ }
359
+
360
+ /**
361
+ * Rule 6: GUARD_VALIDATION_PRESENT
362
+ */
363
+ function evaluateValidationPresent(finding, signals, evidencePackage, isConfirmed) {
364
+ const uiSignals = signals.uiSignals || {};
365
+ const uiFeedback = signals.uiFeedback || {};
366
+
367
+ const hasValidationFeedback = uiSignals.hasErrorSignal ||
368
+ uiSignals.hasValidationMessage ||
369
+ (uiFeedback.signals?.validation?.happened === true);
370
+
371
+ const isValidationFailure = finding.type?.includes('validation') ||
372
+ finding.type?.includes('form');
373
+
374
+ if (hasValidationFeedback && isValidationFailure && isConfirmed) {
375
+ return {
376
+ applies: true,
377
+ message: 'Validation feedback is present. This contradicts a validation silent failure claim.',
378
+ contradiction: true,
379
+ recommendedStatus: 'SUSPECTED',
380
+ };
381
+ }
382
+
383
+ return { applies: false };
384
+ }
385
+
386
+ /**
387
+ * Rule 7: GUARD_CONTRADICT_EVIDENCE
388
+ */
389
+ function evaluateContradictEvidence(finding, signals, evidencePackage, isConfirmed) {
390
+ if (!evidencePackage || !evidencePackage.isComplete) {
391
+ const missingFields = evidencePackage.missingEvidence || [];
392
+ if (isConfirmed && missingFields.length > 0) {
393
+ return {
394
+ applies: true,
395
+ message: `Evidence package is incomplete. Missing: ${missingFields.join(', ')}`,
396
+ contradiction: true,
397
+ recommendedStatus: 'SUSPECTED',
398
+ };
399
+ }
400
+ }
401
+
402
+ return { applies: false };
403
+ }
404
+
405
+ /**
406
+ * Rule: GUARD_VIEW_SWITCH_MINOR_CHANGE
407
+ * If URL unchanged and change is minor (e.g. button text change only) -> cannot be CONFIRMED
408
+ */
409
+ function evaluateViewSwitchMinorChange(finding, signals, evidencePackage, isConfirmed) {
410
+ const isViewSwitch = finding.type?.includes('view_switch') ||
411
+ finding.expectation?.kind === 'VIEW_SWITCH_PROMISE';
412
+ const beforeUrl = evidencePackage.before?.url || '';
413
+ const afterUrl = evidencePackage.after?.url || '';
414
+ const urlUnchanged = beforeUrl === afterUrl;
415
+
416
+ const uiSignals = signals.uiSignals || {};
417
+ const uiFeedback = signals.uiFeedback || {};
418
+
419
+ // Check if change is minor (only button text, no structural change)
420
+ const isMinorChange = (
421
+ uiSignals.textChanged === true &&
422
+ !uiSignals.domChanged &&
423
+ !uiSignals.visibleChanged &&
424
+ !uiSignals.ariaChanged &&
425
+ (!uiFeedback.overallUiFeedbackScore || uiFeedback.overallUiFeedbackScore < 0.2)
426
+ );
427
+
428
+ if (isViewSwitch && urlUnchanged && isMinorChange && isConfirmed) {
429
+ return {
430
+ applies: true,
431
+ message: 'URL unchanged and change is minor (e.g. button text only). Cannot confirm view switch.',
432
+ contradiction: true,
433
+ recommendedStatus: 'SUSPECTED',
434
+ };
435
+ }
436
+
437
+ return { applies: false };
438
+ }
439
+
440
+ /**
441
+ * Rule: GUARD_VIEW_SWITCH_ANALYTICS_ONLY
442
+ * If only analytics fired -> ignore
443
+ */
444
+ function evaluateViewSwitchAnalyticsOnly(finding, signals, evidencePackage, isConfirmed) {
445
+ const isViewSwitch = finding.type?.includes('view_switch') ||
446
+ finding.expectation?.kind === 'VIEW_SWITCH_PROMISE';
447
+
448
+ const networkSignals = signals.network || {};
449
+ const networkRequests = networkSignals.topFailedUrls ||
450
+ networkSignals.observedRequestUrls ||
451
+ [];
452
+
453
+ const analyticsOnly = networkRequests.length > 0 && networkRequests.every(url => {
454
+ if (!url || typeof url !== 'string') return false;
455
+ const lowerUrl = url.toLowerCase();
456
+ return lowerUrl.includes('/analytics') ||
457
+ lowerUrl.includes('/beacon') ||
458
+ lowerUrl.includes('/tracking') ||
459
+ lowerUrl.includes('/pixel') ||
460
+ lowerUrl.includes('google-analytics') ||
461
+ lowerUrl.includes('segment.io') ||
462
+ lowerUrl.includes('mixpanel');
463
+ });
464
+
465
+ const uiSignals = signals.uiSignals || {};
466
+ const hasNoUiChange = !uiSignals.changed && !uiSignals.domChanged && !uiSignals.visibleChanged;
467
+
468
+ if (isViewSwitch && analyticsOnly && hasNoUiChange && isConfirmed) {
469
+ return {
470
+ applies: true,
471
+ message: 'Only analytics fired, no UI change. Cannot confirm view switch.',
472
+ contradiction: true,
473
+ recommendedStatus: 'INFORMATIONAL',
474
+ };
475
+ }
476
+
477
+ return { applies: false };
478
+ }
479
+
480
+ /**
481
+ * Rule: GUARD_VIEW_SWITCH_AMBIGUOUS
482
+ * If state change promise exists but UI outcome ambiguous (one signal only) -> SUSPECTED
483
+ */
484
+ function evaluateViewSwitchAmbiguous(finding, signals, evidencePackage, isConfirmed) {
485
+ const isViewSwitch = finding.type?.includes('view_switch') ||
486
+ finding.expectation?.kind === 'VIEW_SWITCH_PROMISE';
487
+ const hasPromise = finding.expectation?.kind === 'VIEW_SWITCH_PROMISE' ||
488
+ finding.promise?.type === 'view_switch';
489
+
490
+ // Check correlation result - if only one signal, it's ambiguous
491
+ const correlation = finding.correlation || {};
492
+ const signalCount = correlation.signals?.length || 0;
493
+ const ambiguousOutcome = signalCount === 1;
494
+
495
+ if (isViewSwitch && hasPromise && ambiguousOutcome && isConfirmed) {
496
+ return {
497
+ applies: true,
498
+ message: 'State change promise exists but UI outcome ambiguous (one signal only). Downgrading to SUSPECTED.',
499
+ contradiction: false,
500
+ recommendedStatus: 'SUSPECTED',
501
+ };
502
+ }
503
+
504
+ return { applies: false };
505
+ }
@@ -9,8 +9,9 @@
9
9
  */
10
10
 
11
11
  import { createHash } from 'crypto';
12
- import { resolve, dirname } from 'path';
12
+ import { resolve } from 'path';
13
13
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
14
+ import { getRunArtifactDir } from './run-id.js';
14
15
 
15
16
  /**
16
17
  * Compute deterministic hash/signature for a route
@@ -39,8 +40,12 @@ export function computeInteractionSignature(interaction, url) {
39
40
  /**
40
41
  * Load previous snapshot if it exists
41
42
  */
42
- export function loadPreviousSnapshot(projectDir) {
43
- const snapshotPath = resolve(projectDir, '.veraxverax', 'incremental-snapshot.json');
43
+ export function loadPreviousSnapshot(projectDir, runId) {
44
+ if (!runId) {
45
+ return null; // No runId, no snapshot
46
+ }
47
+ const runDir = getRunArtifactDir(projectDir, runId);
48
+ const snapshotPath = resolve(runDir, 'incremental-snapshot.json');
44
49
  if (!existsSync(snapshotPath)) {
45
50
  return null;
46
51
  }
@@ -56,10 +61,13 @@ export function loadPreviousSnapshot(projectDir) {
56
61
  /**
57
62
  * Save current snapshot
58
63
  */
59
- export function saveSnapshot(projectDir, snapshot) {
60
- const snapshotDir = resolve(projectDir, '.veraxverax');
61
- mkdirSync(snapshotDir, { recursive: true });
62
- const snapshotPath = resolve(snapshotDir, 'incremental-snapshot.json');
64
+ export function saveSnapshot(projectDir, snapshot, runId) {
65
+ if (!runId) {
66
+ throw new Error('runId is required for saveSnapshot');
67
+ }
68
+ const runDir = getRunArtifactDir(projectDir, runId);
69
+ mkdirSync(runDir, { recursive: true });
70
+ const snapshotPath = resolve(runDir, 'incremental-snapshot.json');
63
71
 
64
72
  writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2));
65
73
  }