@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,186 @@
1
+ /**
2
+ * VERAX Core Contracts - Canonical Type Definitions
3
+ *
4
+ * This module defines the single source of truth for all core VERAX types:
5
+ * Finding, Evidence, Observation, Confidence, and associated enums.
6
+ *
7
+ * These definitions enforce strict runtime contracts and form the foundation
8
+ * for the Evidence Law: findings without sufficient evidence cannot be Confirmed.
9
+ */
10
+
11
+ /**
12
+ * Confidence Levels - measure of certainty in a finding
13
+ */
14
+ export const CONFIDENCE_LEVEL = {
15
+ HIGH: 'HIGH',
16
+ MEDIUM: 'MEDIUM',
17
+ LOW: 'LOW',
18
+ UNPROVEN: 'UNPROVEN'
19
+ };
20
+
21
+ /**
22
+ * Finding Status - the evaluation outcome
23
+ */
24
+ export const FINDING_STATUS = {
25
+ CONFIRMED: 'CONFIRMED', // Evidence law satisfied: sufficient evidence exists
26
+ SUSPECTED: 'SUSPECTED', // Needs evidence: signal observed but evidence incomplete
27
+ INFORMATIONAL: 'INFORMATIONAL', // Observation recorded, no claim of failure
28
+ UNPROVEN: 'UNPROVEN', // Evidence Law v1: insufficient evidence to support claim
29
+ DROPPED: 'DROPPED' // Violated contracts, removed from report
30
+ };
31
+
32
+ /**
33
+ * Finding Type - category of silent failure
34
+ */
35
+ export const FINDING_TYPE = {
36
+ NAVIGATION_SILENT_FAILURE: 'navigation_silent_failure',
37
+ NETWORK_SILENT_FAILURE: 'network_silent_failure',
38
+ STATE_SILENT_FAILURE: 'state_silent_failure',
39
+ OBSERVED_BREAK: 'observed_break',
40
+ SILENT_FAILURE: 'silent_failure',
41
+ FLOW_SILENT_FAILURE: 'flow_silent_failure'
42
+ };
43
+
44
+ /**
45
+ * Impact Level - severity of the silence
46
+ */
47
+ export const IMPACT = {
48
+ HIGH: 'HIGH',
49
+ MEDIUM: 'MEDIUM',
50
+ LOW: 'LOW'
51
+ };
52
+
53
+ /**
54
+ * User Risk - how the silence affects the user
55
+ */
56
+ export const USER_RISK = {
57
+ BLOCKS: 'BLOCKS', // User action is blocked from completion
58
+ CONFUSES: 'CONFUSES', // User is confused about what happened
59
+ DEGRADES: 'DEGRADES' // User experience is degraded
60
+ };
61
+
62
+ /**
63
+ * Ownership - which layer failed
64
+ */
65
+ export const OWNERSHIP = {
66
+ FRONTEND: 'FRONTEND',
67
+ BACKEND: 'BACKEND',
68
+ INTEGRATION: 'INTEGRATION',
69
+ ACCESSIBILITY: 'ACCESSIBILITY',
70
+ PERFORMANCE: 'PERFORMANCE'
71
+ };
72
+
73
+ /**
74
+ * Evidence Type - what provides proof
75
+ */
76
+ export const EVIDENCE_TYPE = {
77
+ NETWORK_ACTIVITY: 'network_activity',
78
+ DOM_CHANGE: 'dom_change',
79
+ STATE_CHANGE: 'state_change',
80
+ URL_CHANGE: 'url_change',
81
+ SCREENSHOT: 'screenshot',
82
+ CONSOLE_OUTPUT: 'console_output',
83
+ SENSOR_DATA: 'sensor_data'
84
+ };
85
+
86
+ /**
87
+ * Canonical Finding Interface
88
+ *
89
+ * @typedef {Object} Finding
90
+ * @property {string} type - One of FINDING_TYPE
91
+ * @property {string} [status] - One of FINDING_STATUS (defaults to SUSPECTED if evidence insufficient)
92
+ * @property {Object} interaction - The user interaction that triggered analysis
93
+ * @property {Object} evidence - Proof of the gap (REQUIRED for CONFIRMED status)
94
+ * @property {Object} confidence - Certainty assessment
95
+ * @property {Object} signals - Impact classification (impact, userRisk, ownership, grouping)
96
+ * @property {string} what_happened - Factual description of what occurred
97
+ * @property {string} what_was_expected - Factual description of code promise
98
+ * @property {string} what_was_observed - Factual description of observed outcome
99
+ * @property {string} why_it_matters - Human explanation of the gap
100
+ * @property {string} [humanSummary] - Human-readable summary
101
+ * @property {string} [actionHint] - Recommended next step
102
+ * @property {Object} [promise] - Promise descriptor from source code
103
+ * @property {string} [id] - Optional unique identifier
104
+ * @property {string} [findingId] - Optional deterministic ID based on expectation
105
+ * @property {string} [expectationId] - Optional reference to matched expectation
106
+ */
107
+
108
+ /**
109
+ * Canonical Evidence Interface
110
+ *
111
+ * @typedef {Object} Evidence
112
+ * @property {string} [type] - One of EVIDENCE_TYPE
113
+ * @property {boolean} [hasDomChange] - Whether DOM structure changed
114
+ * @property {boolean} [hasUrlChange] - Whether URL changed
115
+ * @property {boolean} [hasNetworkActivity] - Whether network requests occurred
116
+ * @property {boolean} [hasStateChange] - Whether application state changed
117
+ * @property {string} [beforeUrl] - URL before interaction
118
+ * @property {string} [afterUrl] - URL after interaction
119
+ * @property {string} [before] - Before state (screenshot path or data)
120
+ * @property {string} [after] - After state (screenshot path or data)
121
+ * @property {Object} [beforeDom] - DOM structure before
122
+ * @property {Object} [afterDom] - DOM structure after
123
+ * @property {Array} [networkRequests] - Network activity captured
124
+ * @property {Array} [consoleLogs] - Console messages
125
+ * @property {Object} [sensors] - Sensor data (navigation, uiSignals, etc.)
126
+ * @property {string} [source] - Source file reference
127
+ * @property {string} [expectedTarget] - Expected target for navigation/network
128
+ * @property {boolean} [targetReached] - Whether expected target was reached
129
+ */
130
+
131
+ /**
132
+ * Canonical Confidence Interface
133
+ *
134
+ * @typedef {Object} Confidence
135
+ * @property {string} level - One of CONFIDENCE_LEVEL
136
+ * @property {number} score - 0-100 confidence percentage
137
+ * @property {Array} [factors] - List of confidence factors
138
+ * @property {string} [explanation] - Detailed explanation of confidence level
139
+ */
140
+
141
+ /**
142
+ * Canonical Observation Interface
143
+ *
144
+ * @typedef {Object} Observation
145
+ * @property {string} type - Type of observation
146
+ * @property {string} selector - Element selector
147
+ * @property {string} label - Human-readable label
148
+ * @property {Object} sensors - Sensor readings before and after
149
+ * @property {Array} [evidence] - Array of evidence items from observation
150
+ */
151
+
152
+ /**
153
+ * Canonical Signals Interface
154
+ *
155
+ * @typedef {Object} Signals
156
+ * @property {string} impact - One of IMPACT
157
+ * @property {string} userRisk - One of USER_RISK
158
+ * @property {string} ownership - One of OWNERSHIP
159
+ * @property {Object} grouping - Grouping metadata
160
+ * @property {string} [grouping.groupByRoute] - Route pattern
161
+ * @property {string} [grouping.groupByFailureType] - Type of failure
162
+ * @property {string} [grouping.groupByFeature] - Feature area
163
+ */
164
+
165
+ /**
166
+ * Verify all enum exports are available
167
+ */
168
+ export const ALL_ENUMS = {
169
+ CONFIDENCE_LEVEL,
170
+ FINDING_STATUS,
171
+ FINDING_TYPE,
172
+ IMPACT,
173
+ USER_RISK,
174
+ OWNERSHIP,
175
+ EVIDENCE_TYPE
176
+ };
177
+
178
+ export default {
179
+ CONFIDENCE_LEVEL,
180
+ FINDING_STATUS,
181
+ FINDING_TYPE,
182
+ IMPACT,
183
+ USER_RISK,
184
+ OWNERSHIP,
185
+ EVIDENCE_TYPE
186
+ };
@@ -0,0 +1,456 @@
1
+ /**
2
+ * VERAX Validators - Runtime contract enforcement
3
+ *
4
+ * Implements the Evidence Law:
5
+ * "A finding cannot be marked CONFIRMED without sufficient evidence."
6
+ *
7
+ * All validators follow the pattern:
8
+ * @returns {Object} { ok: boolean, errors: string[], downgrade?: string }
9
+ *
10
+ * If a finding violates contracts, it should be downgraded to SUSPECTED
11
+ * or dropped from the report if it's critical.
12
+ */
13
+
14
+ import {
15
+ CONFIDENCE_LEVEL,
16
+ FINDING_STATUS,
17
+ FINDING_TYPE,
18
+ IMPACT,
19
+ USER_RISK,
20
+ OWNERSHIP,
21
+ EVIDENCE_TYPE
22
+ } from './types.js';
23
+
24
+ /**
25
+ * Validate a Finding object against contracts
26
+ *
27
+ * Evidence Law Enforcement:
28
+ * - If confidence.level === CONFIRMED, evidence must be substantive
29
+ * - If evidence is missing or empty, finding cannot be CONFIRMED
30
+ * - Returns { ok, errors, shouldDowngrade, suggestedStatus }
31
+ *
32
+ * @param {Object} finding - Finding object to validate
33
+ * @returns {Object} Validation result with enforcement recommendation
34
+ */
35
+ export function validateFinding(finding) {
36
+ const errors = [];
37
+ let shouldDowngrade = false;
38
+ let suggestedStatus = null;
39
+
40
+ // Contract 1: Required top-level fields
41
+ if (!finding) {
42
+ return {
43
+ ok: false,
44
+ errors: ['Finding is null or undefined'],
45
+ shouldDowngrade: false
46
+ };
47
+ }
48
+
49
+ if (!finding.type) {
50
+ errors.push('Missing required field: type');
51
+ } else if (!Object.values(FINDING_TYPE).includes(finding.type)) {
52
+ errors.push(`Invalid type: ${finding.type}. Must be one of: ${Object.values(FINDING_TYPE).join(', ')}`);
53
+ }
54
+
55
+ if (!finding.interaction || typeof finding.interaction !== 'object') {
56
+ errors.push('Missing or invalid required field: interaction (must be object)');
57
+ }
58
+
59
+ if (!finding.what_happened || typeof finding.what_happened !== 'string') {
60
+ errors.push('Missing or invalid required field: what_happened (must be non-empty string)');
61
+ }
62
+
63
+ if (!finding.what_was_expected || typeof finding.what_was_expected !== 'string') {
64
+ errors.push('Missing or invalid required field: what_was_expected (must be non-empty string)');
65
+ }
66
+
67
+ if (!finding.what_was_observed || typeof finding.what_was_observed !== 'string') {
68
+ errors.push('Missing or invalid required field: what_was_observed (must be non-empty string)');
69
+ }
70
+
71
+ // Contract 2: Evidence validation (CRITICAL for Evidence Law)
72
+ const evidenceValidation = validateEvidence(finding.evidence);
73
+ if (!evidenceValidation.ok) {
74
+ errors.push(`Invalid evidence: ${evidenceValidation.errors.join('; ')}`);
75
+ shouldDowngrade = true;
76
+ suggestedStatus = FINDING_STATUS.SUSPECTED;
77
+ }
78
+
79
+ // Contract 3: Confidence validation
80
+ const confidenceValidation = validateConfidence(finding.confidence);
81
+ if (!confidenceValidation.ok) {
82
+ errors.push(`Invalid confidence: ${confidenceValidation.errors.join('; ')}`);
83
+ shouldDowngrade = true;
84
+ suggestedStatus = FINDING_STATUS.SUSPECTED;
85
+ }
86
+
87
+ // Contract 4: Signals validation
88
+ if (!finding.signals || typeof finding.signals !== 'object') {
89
+ errors.push('Missing or invalid required field: signals (must be object)');
90
+ } else {
91
+ const signalsValidation = validateSignals(finding.signals);
92
+ if (!signalsValidation.ok) {
93
+ errors.push(`Invalid signals: ${signalsValidation.errors.join('; ')}`);
94
+ }
95
+ }
96
+
97
+ // *** EVIDENCE LAW ENFORCEMENT ***
98
+ // PHASE 16: Check evidencePackage completeness for CONFIRMED findings
99
+ // PHASE 21.1: HARD LOCK - CONFIRMED without complete evidencePackage is IMPOSSIBLE
100
+ // PHASE 21: Evidence Law validation for CONFIRMED findings
101
+ if (finding.status === FINDING_STATUS.CONFIRMED || finding.severity === 'CONFIRMED') {
102
+ // EVIDENCE LAW v1: Check evidence structure first (context anchor + effect evidence)
103
+ const evidenceLawResult = enforceEvidenceLawV1(finding.evidence);
104
+ if (!evidenceLawResult.ok && evidenceLawResult.downgrade) {
105
+ console.log(
106
+ `EVIDENCE_LAW v1: downgraded CONFIRMED -> ${evidenceLawResult.downgrade} ` +
107
+ `(missing: ${evidenceLawResult.missing.join(', ')})`
108
+ );
109
+ return {
110
+ ok: true,
111
+ errors: [],
112
+ shouldDowngrade: true,
113
+ suggestedStatus: evidenceLawResult.downgrade
114
+ };
115
+ }
116
+
117
+ // PHASE 21.1: Strict invariant - CONFIRMED findings MUST have complete evidencePackage
118
+ if (finding.evidencePackage) {
119
+ const missingFields = finding.evidencePackage.missingEvidence || [];
120
+ const isComplete = finding.evidencePackage.isComplete === true;
121
+
122
+ if (!isComplete || missingFields.length > 0) {
123
+ // PHASE 21.1: HARD FAILURE - do not downgrade, fail validation
124
+ errors.push(
125
+ `Evidence Law Violation (CRITICAL): Finding marked CONFIRMED but evidencePackage is incomplete. ` +
126
+ `Missing fields: ${missingFields.join(', ')}. ` +
127
+ `evidencePackage.isComplete=${isComplete}. ` +
128
+ `This finding MUST be dropped, not downgraded.`
129
+ );
130
+ // Do not set shouldDowngrade - this is a critical failure that should drop the finding
131
+ return {
132
+ ok: false,
133
+ errors,
134
+ shouldDowngrade: false, // Fail closed - drop, don't downgrade
135
+ suggestedStatus: null
136
+ };
137
+ }
138
+ } else if (!isEvidenceSubstantive(finding.evidence)) {
139
+ // PHASE 21.1: CONFIRMED finding without evidencePackage and without substantive evidence → CRITICAL FAILURE
140
+ errors.push(
141
+ `Evidence Law Violation (CRITICAL): Finding marked CONFIRMED but lacks evidencePackage and evidence is insufficient. ` +
142
+ `This finding MUST be dropped, not downgraded.`
143
+ );
144
+ // Do not set shouldDowngrade - this is a critical failure that should drop the finding
145
+ return {
146
+ ok: false,
147
+ errors,
148
+ shouldDowngrade: false, // Fail closed - drop, don't downgrade
149
+ suggestedStatus: null
150
+ };
151
+ }
152
+ }
153
+
154
+ return {
155
+ ok: errors.length === 0,
156
+ errors,
157
+ shouldDowngrade,
158
+ suggestedStatus
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Validate an Evidence object
164
+ * Evidence is REQUIRED for any CONFIRMED finding.
165
+ *
166
+ * @param {Object} evidence - Evidence object to validate
167
+ * @returns {Object} { ok: boolean, errors: string[] }
168
+ */
169
+ export function validateEvidence(evidence) {
170
+ const errors = [];
171
+
172
+ if (!evidence) {
173
+ errors.push('Evidence object is missing');
174
+ return { ok: false, errors };
175
+ }
176
+
177
+ if (typeof evidence !== 'object') {
178
+ errors.push('Evidence must be an object');
179
+ return { ok: false, errors };
180
+ }
181
+
182
+ // Evidence should contain at least one substantive field
183
+ const substantiveFields = [
184
+ 'hasDomChange',
185
+ 'hasUrlChange',
186
+ 'hasNetworkActivity',
187
+ 'hasStateChange',
188
+ 'networkRequests',
189
+ 'consoleLogs',
190
+ 'before',
191
+ 'after',
192
+ 'beforeDom',
193
+ 'afterDom'
194
+ ];
195
+
196
+ const hasAtLeastOneField = substantiveFields.some(
197
+ field => evidence[field] !== undefined && evidence[field] !== null
198
+ );
199
+
200
+ if (!hasAtLeastOneField && !evidence.sensors) {
201
+ errors.push(
202
+ 'Evidence object is empty. Must contain at least one of: ' +
203
+ substantiveFields.join(', ') + ', or sensors data'
204
+ );
205
+ }
206
+
207
+ // Optional: validate specific evidence fields if present
208
+ if (evidence.type && !Object.values(EVIDENCE_TYPE).includes(evidence.type)) {
209
+ errors.push(`Invalid evidence type: ${evidence.type}`);
210
+ }
211
+
212
+ return {
213
+ ok: errors.length === 0,
214
+ errors
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Validate a Confidence object
220
+ *
221
+ * @param {Object} confidence - Confidence object to validate
222
+ * @returns {Object} { ok: boolean, errors: string[] }
223
+ */
224
+ export function validateConfidence(confidence) {
225
+ const errors = [];
226
+
227
+ if (!confidence || typeof confidence !== 'object') {
228
+ errors.push('Confidence must be a non-empty object');
229
+ return { ok: false, errors };
230
+ }
231
+
232
+ if (!confidence.level) {
233
+ errors.push('Missing required field: confidence.level');
234
+ } else if (!Object.values(CONFIDENCE_LEVEL).includes(confidence.level)) {
235
+ errors.push(
236
+ `Invalid confidence level: ${confidence.level}. ` +
237
+ `Must be one of: ${Object.values(CONFIDENCE_LEVEL).join(', ')}`
238
+ );
239
+ }
240
+
241
+ // Contract v1: Accept both score01 (0-1) and legacy score (0-100) for backward compat
242
+ if (confidence.score01 !== undefined) {
243
+ if (typeof confidence.score01 !== 'number' || confidence.score01 < 0 || confidence.score01 > 1) {
244
+ errors.push(`Invalid confidence.score01: ${confidence.score01}. Must be a number 0-1`);
245
+ }
246
+ } else if (confidence.score !== undefined) {
247
+ if (typeof confidence.score !== 'number' || confidence.score < 0 || confidence.score > 100) {
248
+ errors.push(`Invalid confidence.score: ${confidence.score}. Must be a number 0-100`);
249
+ }
250
+ } else {
251
+ errors.push('Missing required field: confidence.score01 or confidence.score');
252
+ }
253
+
254
+ return {
255
+ ok: errors.length === 0,
256
+ errors
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Validate a Signals object
262
+ *
263
+ * @param {Object} signals - Signals object to validate
264
+ * @returns {Object} { ok: boolean, errors: string[] }
265
+ */
266
+ export function validateSignals(signals) {
267
+ const errors = [];
268
+
269
+ if (!signals || typeof signals !== 'object') {
270
+ errors.push('Signals must be a non-empty object');
271
+ return { ok: false, errors };
272
+ }
273
+
274
+ if (!signals.impact) {
275
+ errors.push('Missing required field: signals.impact');
276
+ } else if (!Object.values(IMPACT).includes(signals.impact)) {
277
+ errors.push(
278
+ `Invalid impact: ${signals.impact}. ` +
279
+ `Must be one of: ${Object.values(IMPACT).join(', ')}`
280
+ );
281
+ }
282
+
283
+ if (!signals.userRisk) {
284
+ errors.push('Missing required field: signals.userRisk');
285
+ } else if (!Object.values(USER_RISK).includes(signals.userRisk)) {
286
+ errors.push(
287
+ `Invalid userRisk: ${signals.userRisk}. ` +
288
+ `Must be one of: ${Object.values(USER_RISK).join(', ')}`
289
+ );
290
+ }
291
+
292
+ if (!signals.ownership) {
293
+ errors.push('Missing required field: signals.ownership');
294
+ } else if (!Object.values(OWNERSHIP).includes(signals.ownership)) {
295
+ errors.push(
296
+ `Invalid ownership: ${signals.ownership}. ` +
297
+ `Must be one of: ${Object.values(OWNERSHIP).join(', ')}`
298
+ );
299
+ }
300
+
301
+ if (!signals.grouping || typeof signals.grouping !== 'object') {
302
+ errors.push('Missing or invalid required field: signals.grouping (must be object)');
303
+ }
304
+
305
+ return {
306
+ ok: errors.length === 0,
307
+ errors
308
+ };
309
+ }
310
+
311
+ /**
312
+ * EVIDENCE LAW v1: Check if CONFIRMED finding has complete evidence structure
313
+ *
314
+ * Rule A (Context Anchor): Must have beforeUrl OR beforeScreenshot OR before
315
+ * Rule B (Effect Evidence): Must have afterUrl OR after OR flags OR quantitative indicators
316
+ *
317
+ * @param {Object} evidence - Evidence object to validate
318
+ * @returns {Object} { ok: boolean, downgrade: 'UNPROVEN'|'SUSPECTED'|null, missing: string[] }
319
+ */
320
+ export function enforceEvidenceLawV1(evidence) {
321
+ if (!evidence || typeof evidence !== 'object' || Object.keys(evidence).length === 0) {
322
+ return { ok: false, downgrade: 'UNPROVEN', missing: ['evidence object'] };
323
+ }
324
+
325
+ const missing = [];
326
+
327
+ // Rule A: Context anchor (before state)
328
+ const hasContextAnchor = evidence.beforeUrl || evidence.beforeScreenshot || evidence.before;
329
+ if (!hasContextAnchor) {
330
+ missing.push('context anchor (beforeUrl/beforeScreenshot/before)');
331
+ }
332
+
333
+ // Rule B: Effect evidence (after state or change indicators)
334
+ const hasEffectEvidence =
335
+ evidence.afterUrl ||
336
+ evidence.afterScreenshot ||
337
+ evidence.after ||
338
+ evidence.urlChanged === true ||
339
+ evidence.domChanged === true ||
340
+ evidence.uiChanged === true ||
341
+ (typeof evidence.networkRequests === 'number' && evidence.networkRequests > 0) ||
342
+ (Array.isArray(evidence.networkRequests) && evidence.networkRequests.length > 0) ||
343
+ (typeof evidence.consoleErrors === 'number' && evidence.consoleErrors > 0) ||
344
+ (Array.isArray(evidence.consoleErrors) && evidence.consoleErrors.length > 0) ||
345
+ evidence.timingBreakdown;
346
+
347
+ if (!hasEffectEvidence) {
348
+ missing.push('effect evidence (after/flags/quantitative)');
349
+ }
350
+
351
+ // Downgrade logic
352
+ if (!hasContextAnchor && !hasEffectEvidence) {
353
+ return { ok: false, downgrade: 'UNPROVEN', missing };
354
+ }
355
+ if (!hasContextAnchor || !hasEffectEvidence) {
356
+ return { ok: false, downgrade: 'SUSPECTED', missing };
357
+ }
358
+
359
+ return { ok: true, downgrade: null, missing: [] };
360
+ }
361
+
362
+ /**
363
+ * EVIDENCE LAW: Determine if evidence is sufficient for CONFIRMED status
364
+ *
365
+ * Substantive evidence means:
366
+ * - At least one positive signal (dom change, url change, network activity, etc.)
367
+ * - OR concrete sensor data from interaction
368
+ * - NOT just empty object or missing evidence
369
+ *
370
+ * @param {Object} evidence - Evidence object to evaluate
371
+ * @returns {boolean} True if evidence is substantive enough for CONFIRMED status
372
+ */
373
+ export function isEvidenceSubstantive(evidence) {
374
+ if (!evidence || typeof evidence !== 'object') {
375
+ return false;
376
+ }
377
+
378
+ // Check for positive signal indicators
379
+ const hasPositiveSignal =
380
+ evidence.hasDomChange === true ||
381
+ evidence.hasUrlChange === true ||
382
+ evidence.hasNetworkActivity === true ||
383
+ evidence.hasStateChange === true ||
384
+ (Array.isArray(evidence.networkRequests) && evidence.networkRequests.length > 0) ||
385
+ (Array.isArray(evidence.consoleLogs) && evidence.consoleLogs.length > 0) ||
386
+ (evidence.before && evidence.after);
387
+
388
+ if (hasPositiveSignal) {
389
+ return true;
390
+ }
391
+
392
+ // Check for sensor data
393
+ if (evidence.sensors && typeof evidence.sensors === 'object' && Object.keys(evidence.sensors).length > 0) {
394
+ return true;
395
+ }
396
+
397
+ return false;
398
+ }
399
+
400
+ /**
401
+ * Enforce contracts on a finding array
402
+ *
403
+ * Returns findings with status downgraded if necessary, filters out
404
+ * findings that violate critical contracts.
405
+ *
406
+ * @param {Array} findings - Array of findings to validate
407
+ * @returns {Object} { valid: Array, dropped: Array, downgrades: Array }
408
+ */
409
+ export function enforceContractsOnFindings(findings) {
410
+ if (!Array.isArray(findings)) {
411
+ return { valid: [], dropped: [], downgrades: [] };
412
+ }
413
+
414
+ const valid = [];
415
+ const dropped = [];
416
+ const downgrades = [];
417
+
418
+ for (const finding of findings) {
419
+ const validation = validateFinding(finding);
420
+
421
+ if (!validation.ok && !validation.shouldDowngrade) {
422
+ // Critical contract violation - drop
423
+ dropped.push({
424
+ finding,
425
+ reason: validation.errors.join('; ')
426
+ });
427
+ continue;
428
+ }
429
+
430
+ if (validation.shouldDowngrade && validation.suggestedStatus) {
431
+ // Downgrade the finding
432
+ const downgraded = { ...finding, status: validation.suggestedStatus };
433
+ downgrades.push({
434
+ original: finding,
435
+ downgraded,
436
+ reason: validation.errors.join('; ')
437
+ });
438
+ valid.push(downgraded);
439
+ } else {
440
+ // Valid finding
441
+ valid.push(finding);
442
+ }
443
+ }
444
+
445
+ return { valid, dropped, downgrades };
446
+ }
447
+
448
+ export default {
449
+ validateFinding,
450
+ validateEvidence,
451
+ validateConfidence,
452
+ validateSignals,
453
+ isEvidenceSubstantive,
454
+ enforceEvidenceLawV1,
455
+ enforceContractsOnFindings
456
+ };