@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,221 @@
1
+ /**
2
+ * PHASE 18 — Determinism Engine
3
+ * PHASE 21.2 — Determinism Truth Lock: Enforces HARD verdict
4
+ *
5
+ * Runs the same scan multiple times and compares results for determinism.
6
+ * PHASE 21.2: Also checks DecisionRecorder for adaptive events that break determinism.
7
+ */
8
+
9
+ import { readFileSync, existsSync } from 'fs';
10
+ import { join, resolve } from 'path';
11
+ import { normalizeArtifact } from './normalize.js';
12
+ import { diffArtifacts } from './diff.js';
13
+ import { computeFindingIdentity } from './finding-identity.js';
14
+ import { ARTIFACT_REGISTRY } from '../artifacts/registry.js';
15
+ import { computeDeterminismVerdict, DETERMINISM_VERDICT, DETERMINISM_REASON } from './contract.js';
16
+ import { DecisionRecorder } from '../determinism-model.js';
17
+
18
+ /**
19
+ * PHASE 18: Determinism verdict (re-exported from contract for backward compatibility)
20
+ */
21
+ export { DETERMINISM_VERDICT, DETERMINISM_REASON } from './contract.js';
22
+
23
+ /**
24
+ * PHASE 18: Run determinism check
25
+ *
26
+ * @param {Function} runFn - Function that executes a scan and returns artifact paths or in-memory artifacts
27
+ * @param {Object} options - Options
28
+ * @param {number} options.runs - Number of runs (default: 2)
29
+ * @param {Object} options.config - Configuration for runs
30
+ * @param {boolean} options.normalize - Whether to normalize artifacts (default: true)
31
+ * @returns {Object} { verdict, summary, diffs, runsMeta }
32
+ */
33
+ export async function runDeterminismCheck(runFn, options = {}) {
34
+ const { runs = 2, config = {}, normalize = true } = options;
35
+
36
+ const runsMeta = [];
37
+ const runArtifacts = [];
38
+
39
+ // Execute runs
40
+ for (let i = 0; i < runs; i++) {
41
+ const runResult = await runFn(config);
42
+ runsMeta.push({
43
+ runIndex: i + 1,
44
+ runId: runResult.runId || null,
45
+ timestamp: new Date().toISOString(),
46
+ artifactPaths: runResult.artifactPaths || {},
47
+ artifacts: runResult.artifacts || {},
48
+ });
49
+
50
+ // Load artifacts if paths provided
51
+ const artifacts = {};
52
+ if (runResult.artifactPaths) {
53
+ for (const [key, path] of Object.entries(runResult.artifactPaths)) {
54
+ try {
55
+ const content = readFileSync(path, 'utf-8');
56
+ artifacts[key] = JSON.parse(content);
57
+ } catch (error) {
58
+ // Artifact not found or invalid
59
+ }
60
+ }
61
+ } else if (runResult.artifacts) {
62
+ Object.assign(artifacts, runResult.artifacts);
63
+ }
64
+
65
+ runArtifacts.push(artifacts);
66
+ }
67
+
68
+ // Compare runs
69
+ const diffs = [];
70
+ const allArtifacts = new Set();
71
+
72
+ // Collect all artifact names
73
+ for (const artifacts of runArtifacts) {
74
+ for (const key of Object.keys(artifacts)) {
75
+ allArtifacts.add(key);
76
+ }
77
+ }
78
+
79
+ // Compare each artifact across runs
80
+ for (const artifactName of allArtifacts) {
81
+ const artifacts = runArtifacts.map(run => run[artifactName]);
82
+
83
+ // Normalize if requested
84
+ const normalizedArtifacts = normalize
85
+ ? artifacts.map(art => art ? normalizeArtifact(artifactName, art) : null)
86
+ : artifacts;
87
+
88
+ // Compare first run with all subsequent runs
89
+ for (let i = 1; i < normalizedArtifacts.length; i++) {
90
+ const artifactA = normalizedArtifacts[0];
91
+ const artifactB = normalizedArtifacts[i];
92
+
93
+ // Build finding identity map for findings artifacts
94
+ let findingIdentityMap = null;
95
+ if (artifactName === 'findings' && artifactA && artifactB) {
96
+ findingIdentityMap = buildFindingIdentityMap(artifactA, artifactB);
97
+ }
98
+
99
+ const artifactDiffs = diffArtifacts(artifactA, artifactB, artifactName, findingIdentityMap);
100
+
101
+ // Add run context to diffs
102
+ for (const diff of artifactDiffs) {
103
+ diff.runA = 1;
104
+ diff.runB = i + 1;
105
+ }
106
+
107
+ diffs.push(...artifactDiffs);
108
+ }
109
+ }
110
+
111
+ // PHASE 21.2: Check for adaptive events in DecisionRecorder (if available)
112
+ // HARD RULE: If adaptive events exist → verdict MUST be NON_DETERMINISTIC
113
+ let adaptiveVerdict = null;
114
+ let adaptiveReasons = [];
115
+ let adaptiveEvents = [];
116
+
117
+ // Try to load decisions.json from first run
118
+ if (runsMeta.length > 0 && runsMeta[0].artifactPaths?.runDir) {
119
+ const runDir = runsMeta[0].artifactPaths.runDir;
120
+ const decisionsPath = resolve(runDir, 'decisions.json');
121
+
122
+ if (existsSync(decisionsPath)) {
123
+ try {
124
+ const decisionsData = JSON.parse(readFileSync(decisionsPath, 'utf-8'));
125
+ const decisionRecorder = DecisionRecorder.fromExport(decisionsData);
126
+ const adaptiveCheck = computeDeterminismVerdict(decisionRecorder);
127
+
128
+ adaptiveVerdict = adaptiveCheck.verdict;
129
+ adaptiveReasons = adaptiveCheck.reasons;
130
+ adaptiveEvents = adaptiveCheck.adaptiveEvents;
131
+ } catch (error) {
132
+ // Ignore errors reading decisions
133
+ }
134
+ }
135
+ }
136
+
137
+ // PHASE 21.2: Determine verdict
138
+ // HARD RULE: If adaptive events exist → verdict MUST be NON_DETERMINISTIC (even if artifacts match)
139
+ const blockerDiffs = diffs.filter(d => d.severity === 'BLOCKER');
140
+ const artifactVerdict = blockerDiffs.length === 0 ? DETERMINISM_VERDICT.DETERMINISTIC : DETERMINISM_VERDICT.NON_DETERMINISTIC;
141
+
142
+ // PHASE 21.2: Final verdict - adaptive events override artifact comparison
143
+ const verdict = (adaptiveVerdict === DETERMINISM_VERDICT.NON_DETERMINISTIC)
144
+ ? DETERMINISM_VERDICT.NON_DETERMINISTIC
145
+ : artifactVerdict;
146
+
147
+ // Build summary
148
+ const summary = buildSummary(diffs, runsMeta);
149
+
150
+ // PHASE 21.2: Include adaptive event information in summary
151
+ if (adaptiveVerdict === DETERMINISM_VERDICT.NON_DETERMINISTIC) {
152
+ summary.adaptiveEventsDetected = true;
153
+ summary.adaptiveEventCount = adaptiveEvents.length;
154
+ summary.adaptiveReasons = adaptiveReasons;
155
+ }
156
+
157
+ return {
158
+ verdict,
159
+ summary,
160
+ diffs,
161
+ runsMeta,
162
+ // PHASE 21.2: Include adaptive event information
163
+ adaptiveVerdict,
164
+ adaptiveReasons,
165
+ adaptiveEvents
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Build finding identity map for matching findings across runs
171
+ */
172
+ function buildFindingIdentityMap(artifactA, artifactB) {
173
+ const map = new Map();
174
+
175
+ const findingsA = artifactA.findings || [];
176
+ const findingsB = artifactB.findings || [];
177
+
178
+ // Build identity for findings in both runs
179
+ for (const finding of [...findingsA, ...findingsB]) {
180
+ const identity = computeFindingIdentity(finding);
181
+ map.set(finding, identity);
182
+ }
183
+
184
+ return map;
185
+ }
186
+
187
+ /**
188
+ * Build summary from diffs
189
+ */
190
+ function buildSummary(diffs, runsMeta) {
191
+ const blockerCount = diffs.filter(d => d.severity === 'BLOCKER').length;
192
+ const warnCount = diffs.filter(d => d.severity === 'WARN').length;
193
+ const infoCount = diffs.filter(d => d.severity === 'INFO').length;
194
+
195
+ // Group by reason code
196
+ const reasonCounts = {};
197
+ for (const diff of diffs) {
198
+ const code = diff.reasonCode || 'UNKNOWN';
199
+ reasonCounts[code] = (reasonCounts[code] || 0) + 1;
200
+ }
201
+
202
+ // Top reasons
203
+ const topReasons = Object.entries(reasonCounts)
204
+ .sort((a, b) => b[1] - a[1])
205
+ .slice(0, 5)
206
+ .map(([code, count]) => ({ code, count }));
207
+
208
+ // Stability score (0..1)
209
+ const totalDiffs = diffs.length;
210
+ const stabilityScore = totalDiffs === 0 ? 1.0 : Math.max(0, 1.0 - (blockerCount * 0.5 + warnCount * 0.2 + infoCount * 0.1) / Math.max(1, totalDiffs));
211
+
212
+ return {
213
+ totalDiffs,
214
+ blockerCount,
215
+ warnCount,
216
+ infoCount,
217
+ topReasons,
218
+ stabilityScore,
219
+ };
220
+ }
221
+
@@ -0,0 +1,148 @@
1
+ /**
2
+ * PHASE 18 — Stable Finding Identity
3
+ *
4
+ * Computes stable identity keys for findings that are consistent across runs.
5
+ * Used for matching findings between runs for comparison.
6
+ */
7
+
8
+ import { createHash } from 'crypto';
9
+
10
+ /**
11
+ * PHASE 18: Compute stable finding identity
12
+ *
13
+ * Uses type + promise signature + route/network target + interaction selector/context chain
14
+ * Includes evidence trigger source signature where available (AST snippet hash or location)
15
+ * Must NOT include runId/timestamps
16
+ *
17
+ * @param {Object} finding - Finding object
18
+ * @returns {string} Stable identity key
19
+ */
20
+ export function computeFindingIdentity(finding) {
21
+ const parts = [];
22
+
23
+ // Part 1: Finding type
24
+ parts.push(`type:${finding.type || 'unknown'}`);
25
+
26
+ // Part 2: Interaction signature
27
+ const interaction = finding.interaction || {};
28
+ if (interaction.type && interaction.selector) {
29
+ parts.push(`interaction:${interaction.type}:${normalizeSelector(interaction.selector)}`);
30
+ }
31
+ if (interaction.label) {
32
+ parts.push(`label:${interaction.label}`);
33
+ }
34
+
35
+ // Part 3: Promise signature
36
+ const expectation = finding.expectation || {};
37
+ const promise = finding.promise || {};
38
+
39
+ if (expectation.type) {
40
+ parts.push(`promiseType:${expectation.type}`);
41
+ }
42
+ if (expectation.targetPath) {
43
+ parts.push(`targetPath:${normalizePath(expectation.targetPath)}`);
44
+ }
45
+ if (expectation.urlPath) {
46
+ parts.push(`urlPath:${normalizePath(expectation.urlPath)}`);
47
+ }
48
+ if (promise.type) {
49
+ parts.push(`promise:${promise.type}`);
50
+ }
51
+
52
+ // Part 4: Route/network target
53
+ if (finding.route) {
54
+ const route = finding.route;
55
+ if (route.path) {
56
+ parts.push(`route:${normalizePath(route.path)}`);
57
+ }
58
+ if (route.originalPattern) {
59
+ parts.push(`routePattern:${normalizePath(route.originalPattern)}`);
60
+ }
61
+ }
62
+
63
+ if (finding.evidence?.networkRequest?.url) {
64
+ parts.push(`networkUrl:${normalizeUrl(finding.evidence.networkRequest.url)}`);
65
+ }
66
+
67
+ // Part 5: Evidence trigger source signature
68
+ const source = finding.source || expectation.source || {};
69
+ if (source.file) {
70
+ parts.push(`sourceFile:${normalizePath(source.file)}`);
71
+ }
72
+ if (source.line) {
73
+ parts.push(`sourceLine:${source.line}`);
74
+ }
75
+ if (source.astSource) {
76
+ // Hash AST source for stability
77
+ const astHash = hashString(source.astSource);
78
+ parts.push(`astHash:${astHash}`);
79
+ }
80
+
81
+ // Part 6: Evidence trigger from evidencePackage
82
+ if (finding.evidencePackage?.trigger?.astSource) {
83
+ const triggerHash = hashString(finding.evidencePackage.trigger.astSource);
84
+ parts.push(`triggerHash:${triggerHash}`);
85
+ }
86
+ if (finding.evidencePackage?.trigger?.source?.file) {
87
+ parts.push(`triggerFile:${normalizePath(finding.evidencePackage.trigger.source.file)}`);
88
+ }
89
+ if (finding.evidencePackage?.trigger?.source?.line) {
90
+ parts.push(`triggerLine:${finding.evidencePackage.trigger.source.line}`);
91
+ }
92
+
93
+ // Part 7: Context chain (if available)
94
+ if (finding.contextChain && Array.isArray(finding.contextChain)) {
95
+ const contextSig = finding.contextChain.map(c => `${c.type}:${c.name || ''}`).join('>');
96
+ parts.push(`context:${contextSig}`);
97
+ }
98
+
99
+ // Combine all parts into stable identity
100
+ const identity = parts.join('|');
101
+
102
+ // Return hash for compactness and stability
103
+ return hashString(identity);
104
+ }
105
+
106
+ /**
107
+ * Normalize selector (remove volatile parts)
108
+ */
109
+ function normalizeSelector(selector) {
110
+ if (!selector || typeof selector !== 'string') return '';
111
+ // Remove any dynamic IDs or classes that might change
112
+ return selector.replace(/\[data-[^\]]+\]/g, '').replace(/\.\w+-\d+/g, '');
113
+ }
114
+
115
+ /**
116
+ * Normalize path (remove absolute paths, normalize separators)
117
+ */
118
+ function normalizePath(path) {
119
+ if (!path || typeof path !== 'string') return '';
120
+ // Normalize separators
121
+ let normalized = path.replace(/\\/g, '/');
122
+ // Remove absolute path prefixes (keep relative structure)
123
+ normalized = normalized.replace(/^[A-Z]:\/[^\/]+/, '');
124
+ normalized = normalized.replace(/^\/[^\/]+/, '');
125
+ return normalized;
126
+ }
127
+
128
+ /**
129
+ * Normalize URL (remove query params, hash, normalize domain)
130
+ */
131
+ function normalizeUrl(url) {
132
+ if (!url || typeof url !== 'string') return '';
133
+ try {
134
+ const urlObj = new URL(url);
135
+ // Keep only pathname for comparison
136
+ return urlObj.pathname;
137
+ } catch {
138
+ return url;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Hash string for stable identity
144
+ */
145
+ function hashString(str) {
146
+ return createHash('sha256').update(str).digest('hex').substring(0, 16);
147
+ }
148
+