@veraxhq/verax 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +10 -6
  2. package/bin/verax.js +11 -11
  3. package/package.json +29 -8
  4. package/src/cli/commands/baseline.js +103 -0
  5. package/src/cli/commands/default.js +51 -6
  6. package/src/cli/commands/doctor.js +29 -0
  7. package/src/cli/commands/ga.js +246 -0
  8. package/src/cli/commands/gates.js +95 -0
  9. package/src/cli/commands/inspect.js +4 -2
  10. package/src/cli/commands/release-check.js +215 -0
  11. package/src/cli/commands/run.js +45 -6
  12. package/src/cli/commands/security-check.js +212 -0
  13. package/src/cli/commands/truth.js +113 -0
  14. package/src/cli/entry.js +30 -20
  15. package/src/cli/util/angular-component-extractor.js +179 -0
  16. package/src/cli/util/angular-navigation-detector.js +141 -0
  17. package/src/cli/util/angular-network-detector.js +161 -0
  18. package/src/cli/util/angular-state-detector.js +162 -0
  19. package/src/cli/util/ast-interactive-detector.js +544 -0
  20. package/src/cli/util/ast-network-detector.js +603 -0
  21. package/src/cli/util/ast-promise-extractor.js +581 -0
  22. package/src/cli/util/ast-usestate-detector.js +602 -0
  23. package/src/cli/util/atomic-write.js +12 -1
  24. package/src/cli/util/bootstrap-guard.js +86 -0
  25. package/src/cli/util/console-reporter.js +72 -0
  26. package/src/cli/util/detection-engine.js +105 -41
  27. package/src/cli/util/determinism-runner.js +124 -0
  28. package/src/cli/util/determinism-writer.js +129 -0
  29. package/src/cli/util/digest-engine.js +359 -0
  30. package/src/cli/util/dom-diff.js +226 -0
  31. package/src/cli/util/evidence-engine.js +287 -0
  32. package/src/cli/util/expectation-extractor.js +151 -5
  33. package/src/cli/util/findings-writer.js +3 -0
  34. package/src/cli/util/framework-detector.js +572 -0
  35. package/src/cli/util/idgen.js +1 -1
  36. package/src/cli/util/interaction-planner.js +529 -0
  37. package/src/cli/util/learn-writer.js +2 -0
  38. package/src/cli/util/ledger-writer.js +110 -0
  39. package/src/cli/util/monorepo-resolver.js +162 -0
  40. package/src/cli/util/observation-engine.js +127 -278
  41. package/src/cli/util/observe-writer.js +2 -0
  42. package/src/cli/util/project-discovery.js +284 -0
  43. package/src/cli/util/project-writer.js +2 -0
  44. package/src/cli/util/run-id.js +23 -27
  45. package/src/cli/util/run-resolver.js +64 -0
  46. package/src/cli/util/run-result.js +778 -0
  47. package/src/cli/util/selector-resolver.js +235 -0
  48. package/src/cli/util/source-requirement.js +55 -0
  49. package/src/cli/util/summary-writer.js +2 -0
  50. package/src/cli/util/svelte-navigation-detector.js +163 -0
  51. package/src/cli/util/svelte-network-detector.js +80 -0
  52. package/src/cli/util/svelte-sfc-extractor.js +146 -0
  53. package/src/cli/util/svelte-state-detector.js +242 -0
  54. package/src/cli/util/trust-activation-integration.js +496 -0
  55. package/src/cli/util/trust-activation-wrapper.js +85 -0
  56. package/src/cli/util/trust-integration-hooks.js +164 -0
  57. package/src/cli/util/types.js +153 -0
  58. package/src/cli/util/url-validation.js +40 -0
  59. package/src/cli/util/vue-navigation-detector.js +178 -0
  60. package/src/cli/util/vue-sfc-extractor.js +161 -0
  61. package/src/cli/util/vue-state-detector.js +215 -0
  62. package/src/types/fs-augment.d.ts +23 -0
  63. package/src/types/global.d.ts +137 -0
  64. package/src/types/internal-types.d.ts +35 -0
  65. package/src/verax/cli/init.js +4 -18
  66. package/src/verax/core/action-classifier.js +4 -3
  67. package/src/verax/core/artifacts/registry.js +139 -0
  68. package/src/verax/core/artifacts/verifier.js +990 -0
  69. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  70. package/src/verax/core/baseline/baseline.snapshot.js +233 -0
  71. package/src/verax/core/capabilities/gates.js +505 -0
  72. package/src/verax/core/capabilities/registry.js +475 -0
  73. package/src/verax/core/confidence/confidence-compute.js +144 -0
  74. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  75. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  76. package/src/verax/core/confidence/confidence-weights.js +44 -0
  77. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  78. package/src/verax/core/confidence/confidence.loader.js +80 -0
  79. package/src/verax/core/confidence/confidence.schema.js +94 -0
  80. package/src/verax/core/confidence-engine-refactor.js +489 -0
  81. package/src/verax/core/confidence-engine.js +625 -0
  82. package/src/verax/core/contracts/index.js +29 -0
  83. package/src/verax/core/contracts/types.js +186 -0
  84. package/src/verax/core/contracts/validators.js +456 -0
  85. package/src/verax/core/decisions/decision.trace.js +278 -0
  86. package/src/verax/core/determinism/contract-writer.js +89 -0
  87. package/src/verax/core/determinism/contract.js +139 -0
  88. package/src/verax/core/determinism/diff.js +405 -0
  89. package/src/verax/core/determinism/engine.js +222 -0
  90. package/src/verax/core/determinism/finding-identity.js +149 -0
  91. package/src/verax/core/determinism/normalize.js +466 -0
  92. package/src/verax/core/determinism/report-writer.js +93 -0
  93. package/src/verax/core/determinism/run-fingerprint.js +123 -0
  94. package/src/verax/core/dynamic-route-intelligence.js +529 -0
  95. package/src/verax/core/evidence/evidence-capture-service.js +308 -0
  96. package/src/verax/core/evidence/evidence-intent-ledger.js +166 -0
  97. package/src/verax/core/evidence-builder.js +487 -0
  98. package/src/verax/core/execution-mode-context.js +77 -0
  99. package/src/verax/core/execution-mode-detector.js +192 -0
  100. package/src/verax/core/failures/exit-codes.js +88 -0
  101. package/src/verax/core/failures/failure-summary.js +76 -0
  102. package/src/verax/core/failures/failure.factory.js +225 -0
  103. package/src/verax/core/failures/failure.ledger.js +133 -0
  104. package/src/verax/core/failures/failure.types.js +196 -0
  105. package/src/verax/core/failures/index.js +10 -0
  106. package/src/verax/core/ga/ga-report-writer.js +43 -0
  107. package/src/verax/core/ga/ga.artifact.js +49 -0
  108. package/src/verax/core/ga/ga.contract.js +435 -0
  109. package/src/verax/core/ga/ga.enforcer.js +87 -0
  110. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  111. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  112. package/src/verax/core/guardrails/policy.loader.js +84 -0
  113. package/src/verax/core/guardrails/policy.schema.js +110 -0
  114. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  115. package/src/verax/core/guardrails-engine.js +505 -0
  116. package/src/verax/core/incremental-store.js +1 -0
  117. package/src/verax/core/integrity/budget.js +138 -0
  118. package/src/verax/core/integrity/determinism.js +342 -0
  119. package/src/verax/core/integrity/integrity.js +208 -0
  120. package/src/verax/core/integrity/poisoning.js +108 -0
  121. package/src/verax/core/integrity/transaction.js +140 -0
  122. package/src/verax/core/observe/run-timeline.js +318 -0
  123. package/src/verax/core/perf/perf.contract.js +186 -0
  124. package/src/verax/core/perf/perf.display.js +65 -0
  125. package/src/verax/core/perf/perf.enforcer.js +91 -0
  126. package/src/verax/core/perf/perf.monitor.js +209 -0
  127. package/src/verax/core/perf/perf.report.js +200 -0
  128. package/src/verax/core/pipeline-tracker.js +243 -0
  129. package/src/verax/core/product-definition.js +127 -0
  130. package/src/verax/core/release/provenance.builder.js +130 -0
  131. package/src/verax/core/release/release-report-writer.js +40 -0
  132. package/src/verax/core/release/release.enforcer.js +164 -0
  133. package/src/verax/core/release/reproducibility.check.js +222 -0
  134. package/src/verax/core/release/sbom.builder.js +292 -0
  135. package/src/verax/core/replay-validator.js +2 -0
  136. package/src/verax/core/replay.js +4 -0
  137. package/src/verax/core/report/cross-index.js +195 -0
  138. package/src/verax/core/report/human-summary.js +362 -0
  139. package/src/verax/core/route-intelligence.js +420 -0
  140. package/src/verax/core/run-id.js +6 -3
  141. package/src/verax/core/run-manifest.js +4 -3
  142. package/src/verax/core/security/secrets.scan.js +329 -0
  143. package/src/verax/core/security/security-report.js +50 -0
  144. package/src/verax/core/security/security.enforcer.js +128 -0
  145. package/src/verax/core/security/supplychain.defaults.json +38 -0
  146. package/src/verax/core/security/supplychain.policy.js +334 -0
  147. package/src/verax/core/security/vuln.scan.js +265 -0
  148. package/src/verax/core/truth/truth.certificate.js +252 -0
  149. package/src/verax/core/ui-feedback-intelligence.js +481 -0
  150. package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
  151. package/src/verax/detect/confidence-engine.js +62 -34
  152. package/src/verax/detect/confidence-helper.js +34 -0
  153. package/src/verax/detect/dynamic-route-findings.js +338 -0
  154. package/src/verax/detect/expectation-chain-detector.js +417 -0
  155. package/src/verax/detect/expectation-model.js +2 -2
  156. package/src/verax/detect/failure-cause-inference.js +293 -0
  157. package/src/verax/detect/findings-writer.js +131 -35
  158. package/src/verax/detect/flow-detector.js +2 -2
  159. package/src/verax/detect/form-silent-failure.js +98 -0
  160. package/src/verax/detect/index.js +46 -5
  161. package/src/verax/detect/invariants-enforcer.js +147 -0
  162. package/src/verax/detect/journey-stall-detector.js +558 -0
  163. package/src/verax/detect/navigation-silent-failure.js +82 -0
  164. package/src/verax/detect/problem-aggregator.js +361 -0
  165. package/src/verax/detect/route-findings.js +219 -0
  166. package/src/verax/detect/summary-writer.js +477 -0
  167. package/src/verax/detect/test-failure-cause-inference.js +314 -0
  168. package/src/verax/detect/ui-feedback-findings.js +207 -0
  169. package/src/verax/detect/view-switch-correlator.js +242 -0
  170. package/src/verax/flow/flow-engine.js +2 -1
  171. package/src/verax/flow/flow-spec.js +0 -6
  172. package/src/verax/index.js +4 -0
  173. package/src/verax/intel/ts-program.js +1 -0
  174. package/src/verax/intel/vue-navigation-extractor.js +3 -0
  175. package/src/verax/learn/action-contract-extractor.js +3 -0
  176. package/src/verax/learn/ast-contract-extractor.js +1 -1
  177. package/src/verax/learn/flow-extractor.js +1 -0
  178. package/src/verax/learn/project-detector.js +5 -0
  179. package/src/verax/learn/react-router-extractor.js +2 -0
  180. package/src/verax/learn/source-instrumenter.js +1 -0
  181. package/src/verax/learn/state-extractor.js +2 -1
  182. package/src/verax/learn/static-extractor.js +1 -0
  183. package/src/verax/observe/coverage-gaps.js +132 -0
  184. package/src/verax/observe/expectation-handler.js +126 -0
  185. package/src/verax/observe/incremental-skip.js +46 -0
  186. package/src/verax/observe/index.js +51 -155
  187. package/src/verax/observe/interaction-executor.js +192 -0
  188. package/src/verax/observe/interaction-runner.js +782 -513
  189. package/src/verax/observe/network-firewall.js +86 -0
  190. package/src/verax/observe/observation-builder.js +169 -0
  191. package/src/verax/observe/observe-context.js +205 -0
  192. package/src/verax/observe/observe-helpers.js +192 -0
  193. package/src/verax/observe/observe-runner.js +230 -0
  194. package/src/verax/observe/observers/budget-observer.js +185 -0
  195. package/src/verax/observe/observers/console-observer.js +102 -0
  196. package/src/verax/observe/observers/coverage-observer.js +107 -0
  197. package/src/verax/observe/observers/interaction-observer.js +471 -0
  198. package/src/verax/observe/observers/navigation-observer.js +132 -0
  199. package/src/verax/observe/observers/network-observer.js +87 -0
  200. package/src/verax/observe/observers/safety-observer.js +82 -0
  201. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  202. package/src/verax/observe/page-traversal.js +138 -0
  203. package/src/verax/observe/snapshot-ops.js +94 -0
  204. package/src/verax/observe/ui-feedback-detector.js +742 -0
  205. package/src/verax/scan-summary-writer.js +2 -0
  206. package/src/verax/shared/artifact-manager.js +25 -5
  207. package/src/verax/shared/caching.js +1 -0
  208. package/src/verax/shared/css-spinner-rules.js +204 -0
  209. package/src/verax/shared/expectation-tracker.js +1 -0
  210. package/src/verax/shared/view-switch-rules.js +208 -0
  211. package/src/verax/shared/zip-artifacts.js +6 -0
  212. package/src/verax/shared/config-loader.js +0 -169
  213. /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
@@ -0,0 +1,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
+ }
@@ -52,6 +52,7 @@ export function loadPreviousSnapshot(projectDir, runId) {
52
52
 
53
53
  try {
54
54
  const content = readFileSync(snapshotPath, 'utf-8');
55
+ // @ts-expect-error - readFileSync with encoding returns string
55
56
  return JSON.parse(content);
56
57
  } catch {
57
58
  return null;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * PHASE 6A: Hard Budget Enforcement
3
+ *
4
+ * Enforces performance budgets as HARD limits with immediate termination.
5
+ * No warnings, no soft limits - budget exceeded = ANALYSIS_INCOMPLETE.
6
+ */
7
+
8
+ import { SKIP_REASON } from '../../../cli/util/types.js';
9
+
10
+ /**
11
+ * Check if budget is exceeded and enforce hard limit
12
+ *
13
+ * @param {Object} budget - Budget configuration
14
+ * @param {Object} metrics - Current metrics
15
+ * @param {Function} recordSkip - Function to record skip
16
+ * @param {Function} markIncomplete - Function to mark analysis incomplete
17
+ * @returns {{ exceeded: boolean, phase?: string, limit?: number, actual?: number }} Result
18
+ */
19
+ export function enforceBudget(budget, metrics, recordSkip, markIncomplete) {
20
+ // Check observe budget
21
+ if (budget.observeMaxMs && metrics.observeMs >= budget.observeMaxMs) {
22
+ recordSkip(SKIP_REASON.TIMEOUT_OBSERVE, 1);
23
+ markIncomplete('observe', budget.observeMaxMs, metrics.observeMs);
24
+ return {
25
+ exceeded: true,
26
+ phase: 'observe',
27
+ limit: budget.observeMaxMs,
28
+ actual: metrics.observeMs,
29
+ };
30
+ }
31
+
32
+ // Check detect budget
33
+ if (budget.detectMaxMs && metrics.detectMs >= budget.detectMaxMs) {
34
+ recordSkip(SKIP_REASON.TIMEOUT_DETECT, 1);
35
+ markIncomplete('detect', budget.detectMaxMs, metrics.detectMs);
36
+ return {
37
+ exceeded: true,
38
+ phase: 'detect',
39
+ limit: budget.detectMaxMs,
40
+ actual: metrics.detectMs,
41
+ };
42
+ }
43
+
44
+ // Check total budget
45
+ if (budget.totalMaxMs && metrics.totalMs >= budget.totalMaxMs) {
46
+ recordSkip(SKIP_REASON.TIMEOUT_TOTAL, 1);
47
+ markIncomplete('total', budget.totalMaxMs, metrics.totalMs);
48
+ return {
49
+ exceeded: true,
50
+ phase: 'total',
51
+ limit: budget.totalMaxMs,
52
+ actual: metrics.totalMs,
53
+ };
54
+ }
55
+
56
+ // Check expectations budget
57
+ if (budget.maxExpectations && metrics.expectationsAnalyzed >= budget.maxExpectations) {
58
+ const skippedCount = metrics.expectationsDiscovered - budget.maxExpectations;
59
+ if (skippedCount > 0) {
60
+ recordSkip(SKIP_REASON.BUDGET_EXCEEDED, skippedCount);
61
+ markIncomplete('expectations', budget.maxExpectations, metrics.expectationsAnalyzed);
62
+ return {
63
+ exceeded: true,
64
+ phase: 'expectations',
65
+ limit: budget.maxExpectations,
66
+ actual: metrics.expectationsAnalyzed,
67
+ };
68
+ }
69
+ }
70
+
71
+ return { exceeded: false };
72
+ }
73
+
74
+ /**
75
+ * Create budget guard that throws on budget exceeded
76
+ *
77
+ * @param {Object} budget - Budget configuration
78
+ * @param {Object} metrics - Metrics object (will be updated)
79
+ * @returns {Function} Guard function to check budget
80
+ */
81
+ export function createBudgetGuard(budget, metrics) {
82
+ return (phase) => {
83
+ const check = enforceBudget(
84
+ budget,
85
+ metrics,
86
+ () => {}, // Skip recording handled elsewhere
87
+ () => {} // Marking handled elsewhere
88
+ );
89
+
90
+ if (check.exceeded) {
91
+ const error = new Error(
92
+ `Budget exceeded: ${phase} phase (limit: ${check.limit}ms, actual: ${check.actual}ms)`
93
+ );
94
+ error.name = 'BudgetExceededError';
95
+ // @ts-expect-error - Dynamic error properties
96
+ error.code = 'BUDGET_EXCEEDED';
97
+ // @ts-expect-error - Dynamic error properties
98
+ error.phase = phase;
99
+ // @ts-expect-error - Dynamic error properties
100
+ error.limit = check.limit;
101
+ // @ts-expect-error - Dynamic error properties
102
+ error.actual = check.actual;
103
+ throw error;
104
+ }
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Wrap async operation with budget enforcement
110
+ *
111
+ * @param {Promise} operation - Async operation
112
+ * @param {Function} budgetGuard - Budget guard function
113
+ * @param {string} phase - Phase name
114
+ * @param {Function} onCheck - Called periodically to check budget
115
+ * @returns {Promise} Operation result or throws on budget exceeded
116
+ */
117
+ export async function withBudgetEnforcement(operation, budgetGuard, phase, onCheck) {
118
+ const checkInterval = setInterval(() => {
119
+ try {
120
+ budgetGuard(phase);
121
+ if (onCheck) {
122
+ onCheck();
123
+ }
124
+ } catch (error) {
125
+ clearInterval(checkInterval);
126
+ throw error;
127
+ }
128
+ }, 1000); // Check every second
129
+
130
+ try {
131
+ const result = await operation;
132
+ clearInterval(checkInterval);
133
+ return result;
134
+ } catch (error) {
135
+ clearInterval(checkInterval);
136
+ throw error;
137
+ }
138
+ }