@veraxhq/verax 0.2.1 → 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 (152) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +3 -3
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +79 -25
  6. package/src/cli/commands/ga.js +243 -0
  7. package/src/cli/commands/gates.js +95 -0
  8. package/src/cli/commands/inspect.js +131 -2
  9. package/src/cli/commands/release-check.js +213 -0
  10. package/src/cli/commands/run.js +246 -35
  11. package/src/cli/commands/security-check.js +211 -0
  12. package/src/cli/commands/truth.js +114 -0
  13. package/src/cli/entry.js +304 -67
  14. package/src/cli/util/angular-component-extractor.js +179 -0
  15. package/src/cli/util/angular-navigation-detector.js +141 -0
  16. package/src/cli/util/angular-network-detector.js +161 -0
  17. package/src/cli/util/angular-state-detector.js +162 -0
  18. package/src/cli/util/ast-interactive-detector.js +546 -0
  19. package/src/cli/util/ast-network-detector.js +603 -0
  20. package/src/cli/util/ast-usestate-detector.js +602 -0
  21. package/src/cli/util/bootstrap-guard.js +86 -0
  22. package/src/cli/util/determinism-runner.js +123 -0
  23. package/src/cli/util/determinism-writer.js +129 -0
  24. package/src/cli/util/env-url.js +4 -0
  25. package/src/cli/util/expectation-extractor.js +369 -73
  26. package/src/cli/util/findings-writer.js +126 -16
  27. package/src/cli/util/learn-writer.js +3 -1
  28. package/src/cli/util/observe-writer.js +3 -1
  29. package/src/cli/util/paths.js +3 -12
  30. package/src/cli/util/project-discovery.js +3 -0
  31. package/src/cli/util/project-writer.js +3 -1
  32. package/src/cli/util/run-resolver.js +64 -0
  33. package/src/cli/util/source-requirement.js +55 -0
  34. package/src/cli/util/summary-writer.js +1 -0
  35. package/src/cli/util/svelte-navigation-detector.js +163 -0
  36. package/src/cli/util/svelte-network-detector.js +80 -0
  37. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  38. package/src/cli/util/svelte-state-detector.js +243 -0
  39. package/src/cli/util/vue-navigation-detector.js +177 -0
  40. package/src/cli/util/vue-sfc-extractor.js +162 -0
  41. package/src/cli/util/vue-state-detector.js +215 -0
  42. package/src/verax/cli/finding-explainer.js +56 -3
  43. package/src/verax/core/artifacts/registry.js +154 -0
  44. package/src/verax/core/artifacts/verifier.js +980 -0
  45. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  46. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  47. package/src/verax/core/capabilities/gates.js +499 -0
  48. package/src/verax/core/capabilities/registry.js +475 -0
  49. package/src/verax/core/confidence/confidence-compute.js +137 -0
  50. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  51. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  52. package/src/verax/core/confidence/confidence-weights.js +44 -0
  53. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  54. package/src/verax/core/confidence/confidence.loader.js +79 -0
  55. package/src/verax/core/confidence/confidence.schema.js +94 -0
  56. package/src/verax/core/confidence-engine-refactor.js +484 -0
  57. package/src/verax/core/confidence-engine.js +486 -0
  58. package/src/verax/core/confidence-engine.js.backup +471 -0
  59. package/src/verax/core/contracts/index.js +29 -0
  60. package/src/verax/core/contracts/types.js +185 -0
  61. package/src/verax/core/contracts/validators.js +381 -0
  62. package/src/verax/core/decision-snapshot.js +30 -3
  63. package/src/verax/core/decisions/decision.trace.js +276 -0
  64. package/src/verax/core/determinism/contract-writer.js +89 -0
  65. package/src/verax/core/determinism/contract.js +139 -0
  66. package/src/verax/core/determinism/diff.js +364 -0
  67. package/src/verax/core/determinism/engine.js +221 -0
  68. package/src/verax/core/determinism/finding-identity.js +148 -0
  69. package/src/verax/core/determinism/normalize.js +438 -0
  70. package/src/verax/core/determinism/report-writer.js +92 -0
  71. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  72. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  73. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  74. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  75. package/src/verax/core/evidence-builder.js +487 -0
  76. package/src/verax/core/execution-mode-context.js +77 -0
  77. package/src/verax/core/execution-mode-detector.js +190 -0
  78. package/src/verax/core/failures/exit-codes.js +86 -0
  79. package/src/verax/core/failures/failure-summary.js +76 -0
  80. package/src/verax/core/failures/failure.factory.js +225 -0
  81. package/src/verax/core/failures/failure.ledger.js +132 -0
  82. package/src/verax/core/failures/failure.types.js +196 -0
  83. package/src/verax/core/failures/index.js +10 -0
  84. package/src/verax/core/ga/ga-report-writer.js +43 -0
  85. package/src/verax/core/ga/ga.artifact.js +49 -0
  86. package/src/verax/core/ga/ga.contract.js +434 -0
  87. package/src/verax/core/ga/ga.enforcer.js +86 -0
  88. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  89. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  90. package/src/verax/core/guardrails/policy.loader.js +83 -0
  91. package/src/verax/core/guardrails/policy.schema.js +110 -0
  92. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  93. package/src/verax/core/guardrails-engine.js +505 -0
  94. package/src/verax/core/observe/run-timeline.js +316 -0
  95. package/src/verax/core/perf/perf.contract.js +186 -0
  96. package/src/verax/core/perf/perf.display.js +65 -0
  97. package/src/verax/core/perf/perf.enforcer.js +91 -0
  98. package/src/verax/core/perf/perf.monitor.js +209 -0
  99. package/src/verax/core/perf/perf.report.js +198 -0
  100. package/src/verax/core/pipeline-tracker.js +238 -0
  101. package/src/verax/core/product-definition.js +127 -0
  102. package/src/verax/core/release/provenance.builder.js +271 -0
  103. package/src/verax/core/release/release-report-writer.js +40 -0
  104. package/src/verax/core/release/release.enforcer.js +159 -0
  105. package/src/verax/core/release/reproducibility.check.js +221 -0
  106. package/src/verax/core/release/sbom.builder.js +283 -0
  107. package/src/verax/core/report/cross-index.js +192 -0
  108. package/src/verax/core/report/human-summary.js +222 -0
  109. package/src/verax/core/route-intelligence.js +419 -0
  110. package/src/verax/core/security/secrets.scan.js +326 -0
  111. package/src/verax/core/security/security-report.js +50 -0
  112. package/src/verax/core/security/security.enforcer.js +124 -0
  113. package/src/verax/core/security/supplychain.defaults.json +38 -0
  114. package/src/verax/core/security/supplychain.policy.js +326 -0
  115. package/src/verax/core/security/vuln.scan.js +265 -0
  116. package/src/verax/core/truth/truth.certificate.js +250 -0
  117. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  118. package/src/verax/detect/confidence-engine.js +628 -40
  119. package/src/verax/detect/confidence-helper.js +33 -0
  120. package/src/verax/detect/detection-engine.js +18 -1
  121. package/src/verax/detect/dynamic-route-findings.js +335 -0
  122. package/src/verax/detect/expectation-chain-detector.js +417 -0
  123. package/src/verax/detect/expectation-model.js +3 -1
  124. package/src/verax/detect/findings-writer.js +141 -5
  125. package/src/verax/detect/index.js +229 -5
  126. package/src/verax/detect/journey-stall-detector.js +558 -0
  127. package/src/verax/detect/route-findings.js +218 -0
  128. package/src/verax/detect/ui-feedback-findings.js +207 -0
  129. package/src/verax/detect/verdict-engine.js +57 -3
  130. package/src/verax/detect/view-switch-correlator.js +242 -0
  131. package/src/verax/index.js +413 -45
  132. package/src/verax/learn/action-contract-extractor.js +682 -64
  133. package/src/verax/learn/route-validator.js +4 -1
  134. package/src/verax/observe/index.js +88 -843
  135. package/src/verax/observe/interaction-runner.js +25 -8
  136. package/src/verax/observe/observe-context.js +205 -0
  137. package/src/verax/observe/observe-helpers.js +191 -0
  138. package/src/verax/observe/observe-runner.js +226 -0
  139. package/src/verax/observe/observers/budget-observer.js +185 -0
  140. package/src/verax/observe/observers/console-observer.js +102 -0
  141. package/src/verax/observe/observers/coverage-observer.js +107 -0
  142. package/src/verax/observe/observers/interaction-observer.js +471 -0
  143. package/src/verax/observe/observers/navigation-observer.js +132 -0
  144. package/src/verax/observe/observers/network-observer.js +87 -0
  145. package/src/verax/observe/observers/safety-observer.js +82 -0
  146. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  147. package/src/verax/observe/ui-feedback-detector.js +742 -0
  148. package/src/verax/observe/ui-signal-sensor.js +148 -2
  149. package/src/verax/scan-summary-writer.js +42 -8
  150. package/src/verax/shared/artifact-manager.js +8 -5
  151. package/src/verax/shared/css-spinner-rules.js +204 -0
  152. package/src/verax/shared/view-switch-rules.js +208 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * PHASE 25 — Determinism Contract Writer
3
+ *
4
+ * Writes determinism.contract.json artifact capturing adaptive events,
5
+ * retries, timing adjustments, and other non-deterministic behaviors.
6
+ */
7
+
8
+ import { writeFileSync } from 'fs';
9
+ import { resolve } from 'path';
10
+ import { DecisionRecorder } from '../determinism-model.js';
11
+ import { ARTIFACT_REGISTRY } from '../artifacts/registry.js';
12
+
13
+ /**
14
+ * Write determinism contract artifact
15
+ *
16
+ * @param {string} runDir - Absolute run directory path
17
+ * @param {DecisionRecorder} decisionRecorder - Decision recorder instance
18
+ * @returns {string} Path to written contract
19
+ */
20
+ export function writeDeterminismContract(runDir, decisionRecorder) {
21
+ const contractPath = resolve(runDir, ARTIFACT_REGISTRY.determinismContract.filename);
22
+
23
+ const adaptiveEvents = [];
24
+ const retryEvents = [];
25
+ const timingAdjustments = [];
26
+
27
+ if (decisionRecorder) {
28
+ // Extract adaptive stabilization events
29
+ const adaptiveStabilization = decisionRecorder.getByCategory('ADAPTIVE_STABILIZATION');
30
+ for (const decision of adaptiveStabilization) {
31
+ adaptiveEvents.push({
32
+ decision_id: decision.decision_id,
33
+ category: decision.category,
34
+ timestamp: decision.timestamp,
35
+ reason: decision.reason,
36
+ context: decision.context || null,
37
+ chosen_value: decision.chosen_value,
38
+ inputs: decision.inputs || {}
39
+ });
40
+ }
41
+
42
+ // Extract retry events
43
+ const retries = decisionRecorder.getByCategory('RETRY');
44
+ for (const decision of retries) {
45
+ retryEvents.push({
46
+ decision_id: decision.decision_id,
47
+ category: decision.category,
48
+ timestamp: decision.timestamp,
49
+ reason: decision.reason,
50
+ context: decision.context || null,
51
+ chosen_value: decision.chosen_value,
52
+ inputs: decision.inputs || {}
53
+ });
54
+ }
55
+
56
+ // Extract timing adjustments (timeout decisions)
57
+ const timeouts = decisionRecorder.getByCategory('TIMEOUT');
58
+ for (const decision of timeouts) {
59
+ timingAdjustments.push({
60
+ decision_id: decision.decision_id,
61
+ category: decision.category,
62
+ timestamp: decision.timestamp,
63
+ reason: decision.reason,
64
+ context: decision.context || null,
65
+ chosen_value: decision.chosen_value,
66
+ inputs: decision.inputs || {}
67
+ });
68
+ }
69
+ }
70
+
71
+ const contract = {
72
+ version: 1,
73
+ generatedAt: new Date().toISOString(),
74
+ adaptiveEvents,
75
+ retryEvents,
76
+ timingAdjustments,
77
+ summary: {
78
+ adaptiveEventsCount: adaptiveEvents.length,
79
+ retryEventsCount: retryEvents.length,
80
+ timingAdjustmentsCount: timingAdjustments.length,
81
+ isDeterministic: adaptiveEvents.length === 0 && retryEvents.length === 0
82
+ }
83
+ };
84
+
85
+ writeFileSync(contractPath, JSON.stringify(contract, null, 2), 'utf8');
86
+
87
+ return contractPath;
88
+ }
89
+
@@ -0,0 +1,139 @@
1
+ /**
2
+ * PHASE 21.2 — Determinism Truth Lock: Strict Contract
3
+ *
4
+ * DETERMINISM CONTRACT:
5
+ *
6
+ * DETERMINISTIC means:
7
+ * - Same inputs (source code, URL, config)
8
+ * - Same environment (browser, OS, Node version)
9
+ * - Same config (budget, timeouts, flags)
10
+ * → identical normalized artifacts (findings, traces, evidence)
11
+ *
12
+ * NON_DETERMINISTIC means:
13
+ * - Any adaptive behavior occurred (adaptive stabilization, retries, dynamic timeouts)
14
+ * - Any timing variance that affects results
15
+ * - Any environment-dependent behavior
16
+ * - Tracking adaptive decisions is NOT determinism
17
+ *
18
+ * HARD RULE: If adaptiveEvents.length > 0 → verdict MUST be NON_DETERMINISTIC
19
+ */
20
+
21
+ /**
22
+ * PHASE 21.2: Determinism verdict (binary)
23
+ * PHASE 25: Extended with expected/unexpected distinction
24
+ */
25
+ export const DETERMINISM_VERDICT = {
26
+ DETERMINISTIC: 'DETERMINISTIC',
27
+ NON_DETERMINISTIC_EXPECTED: 'NON_DETERMINISTIC_EXPECTED',
28
+ NON_DETERMINISTIC_UNEXPECTED: 'NON_DETERMINISTIC_UNEXPECTED',
29
+ NON_DETERMINISTIC: 'NON_DETERMINISTIC' // Backward compatibility
30
+ };
31
+
32
+ /**
33
+ * PHASE 21.2: Determinism reason codes (stable)
34
+ * PHASE 25: Extended with new reason codes
35
+ */
36
+ export const DETERMINISM_REASON = {
37
+ ADAPTIVE_STABILIZATION_USED: 'ADAPTIVE_STABILIZATION_USED',
38
+ RETRY_TRIGGERED: 'RETRY_TRIGGERED',
39
+ TIMING_VARIANCE: 'TIMING_VARIANCE',
40
+ TRUNCATION_OCCURRED: 'TRUNCATION_OCCURRED',
41
+ ENVIRONMENT_VARIANCE: 'ENVIRONMENT_VARIANCE',
42
+ NO_ADAPTIVE_EVENTS: 'NO_ADAPTIVE_EVENTS',
43
+ RUN_FINGERPRINT_MISMATCH: 'RUN_FINGERPRINT_MISMATCH',
44
+ ARTIFACT_DIFF_DETECTED: 'ARTIFACT_DIFF_DETECTED',
45
+ VERIFIER_ERRORS_DETECTED: 'VERIFIER_ERRORS_DETECTED',
46
+ EXPECTED_ADAPTIVE_BEHAVIOR: 'EXPECTED_ADAPTIVE_BEHAVIOR',
47
+ UNEXPECTED_DIFF_WITHOUT_ADAPTIVE: 'UNEXPECTED_DIFF_WITHOUT_ADAPTIVE'
48
+ };
49
+
50
+ /**
51
+ * PHASE 21.2: Adaptive event categories that break determinism
52
+ */
53
+ export const ADAPTIVE_EVENT_CATEGORIES = [
54
+ 'ADAPTIVE_STABILIZATION',
55
+ 'RETRY',
56
+ 'TRUNCATION'
57
+ ];
58
+
59
+ /**
60
+ * PHASE 21.2: Check if a decision category breaks determinism
61
+ *
62
+ * @param {string} category - Decision category
63
+ * @returns {boolean} True if this category breaks determinism
64
+ */
65
+ export function isAdaptiveEventCategory(category) {
66
+ return ADAPTIVE_EVENT_CATEGORIES.includes(category);
67
+ }
68
+
69
+ /**
70
+ * PHASE 21.2: Compute determinism verdict from DecisionRecorder
71
+ *
72
+ * HARD RULE: If any adaptive event occurred → NON_DETERMINISTIC
73
+ *
74
+ * @param {DecisionRecorder} decisionRecorder - Decision recorder instance
75
+ * @returns {Object} { verdict, reasons, adaptiveEvents }
76
+ */
77
+ export function computeDeterminismVerdict(decisionRecorder) {
78
+ if (!decisionRecorder) {
79
+ return {
80
+ verdict: DETERMINISM_VERDICT.NON_DETERMINISTIC,
81
+ reasons: [DETERMINISM_REASON.ENVIRONMENT_VARIANCE],
82
+ adaptiveEvents: [],
83
+ message: 'DecisionRecorder not available - cannot verify determinism'
84
+ };
85
+ }
86
+
87
+ const adaptiveEvents = [];
88
+ const reasons = [];
89
+
90
+ // Check for adaptive stabilization
91
+ const adaptiveStabilization = decisionRecorder.getByCategory('ADAPTIVE_STABILIZATION');
92
+ const adaptiveExtensions = adaptiveStabilization.filter(d =>
93
+ d.decision_id === 'ADAPTIVE_STABILIZATION_extended'
94
+ );
95
+
96
+ if (adaptiveExtensions.length > 0) {
97
+ adaptiveEvents.push(...adaptiveExtensions);
98
+ reasons.push(DETERMINISM_REASON.ADAPTIVE_STABILIZATION_USED);
99
+ }
100
+
101
+ // Check for retries
102
+ const retries = decisionRecorder.getByCategory('RETRY');
103
+ if (retries.length > 0) {
104
+ adaptiveEvents.push(...retries);
105
+ reasons.push(DETERMINISM_REASON.RETRY_TRIGGERED);
106
+ }
107
+
108
+ // Check for truncations
109
+ const truncations = decisionRecorder.getByCategory('TRUNCATION');
110
+ if (truncations.length > 0) {
111
+ adaptiveEvents.push(...truncations);
112
+ reasons.push(DETERMINISM_REASON.TRUNCATION_OCCURRED);
113
+ }
114
+
115
+ // HARD RULE: If any adaptive events → NON_DETERMINISTIC
116
+ if (adaptiveEvents.length > 0) {
117
+ return {
118
+ verdict: DETERMINISM_VERDICT.NON_DETERMINISTIC,
119
+ reasons,
120
+ adaptiveEvents: adaptiveEvents.map(e => ({
121
+ decision_id: e.decision_id,
122
+ category: e.category,
123
+ timestamp: e.timestamp,
124
+ reason: e.reason,
125
+ context: e.context || null
126
+ })),
127
+ message: `Non-deterministic: ${adaptiveEvents.length} adaptive event(s) detected`
128
+ };
129
+ }
130
+
131
+ // No adaptive events → DETERMINISTIC
132
+ return {
133
+ verdict: DETERMINISM_VERDICT.DETERMINISTIC,
134
+ reasons: [DETERMINISM_REASON.NO_ADAPTIVE_EVENTS],
135
+ adaptiveEvents: [],
136
+ message: 'Deterministic: No adaptive events detected'
137
+ };
138
+ }
139
+
@@ -0,0 +1,364 @@
1
+ /**
2
+ * PHASE 18 — Determinism Diff Builder
3
+ *
4
+ * Generates structured diffs between normalized artifacts from different runs.
5
+ */
6
+
7
+ import { computeFindingIdentity } from './finding-identity.js';
8
+
9
+ /**
10
+ * PHASE 18: Diff reason codes
11
+ * PHASE 25: Extended with new reason codes
12
+ */
13
+ export const DIFF_REASON = {
14
+ MISSING_ARTIFACT: 'DET_DIFF_MISSING_ARTIFACT',
15
+ SCHEMA_MISMATCH: 'DET_DIFF_SCHEMA_MISMATCH',
16
+ FINDING_ADDED: 'DET_DIFF_FINDING_ADDED',
17
+ FINDING_REMOVED: 'DET_DIFF_FINDING_REMOVED',
18
+ FINDING_STATUS_CHANGED: 'DET_DIFF_FINDING_STATUS_CHANGED',
19
+ FINDING_SEVERITY_CHANGED: 'DET_DIFF_FINDING_SEVERITY_CHANGED',
20
+ CONFIDENCE_CHANGED: 'DET_DIFF_CONFIDENCE_CHANGED',
21
+ CONFIDENCE_REASONS_CHANGED: 'DET_DIFF_CONFIDENCE_REASONS_CHANGED',
22
+ GUARDRAILS_CHANGED: 'DET_DIFF_GUARDRAILS_CHANGED',
23
+ EVIDENCE_COMPLETENESS_CHANGED: 'DET_DIFF_EVIDENCE_COMPLETENESS_CHANGED',
24
+ EVIDENCE_MISSING: 'DET_DIFF_EVIDENCE_MISSING',
25
+ OBSERVATION_COUNT_CHANGED: 'DET_DIFF_OBSERVATION_COUNT_CHANGED',
26
+ FIELD_VALUE_CHANGED: 'DET_DIFF_FIELD_VALUE_CHANGED',
27
+ RUN_FINGERPRINT_MISMATCH: 'DET_DIFF_RUN_FINGERPRINT_MISMATCH',
28
+ };
29
+
30
+ /**
31
+ * PHASE 18: Diff categories
32
+ */
33
+ export const DIFF_CATEGORY = {
34
+ FINDINGS: 'FINDINGS',
35
+ EXPECTATIONS: 'EXPECTATIONS',
36
+ OBSERVATIONS: 'OBSERVATIONS',
37
+ EVIDENCE: 'EVIDENCE',
38
+ STATUS: 'STATUS',
39
+ ARTIFACTS: 'ARTIFACTS',
40
+ };
41
+
42
+ /**
43
+ * PHASE 18: Diff severity
44
+ */
45
+ export const DIFF_SEVERITY = {
46
+ BLOCKER: 'BLOCKER',
47
+ WARN: 'WARN',
48
+ INFO: 'INFO',
49
+ };
50
+
51
+ /**
52
+ * PHASE 18: Diff artifacts
53
+ *
54
+ * @param {Object} artifactA - First artifact (normalized)
55
+ * @param {Object} artifactB - Second artifact (normalized)
56
+ * @param {string} artifactName - Name of artifact
57
+ * @param {Map} findingIdentityMap - Map of finding identity to finding (for matching)
58
+ * @returns {Array} Array of diff objects
59
+ */
60
+ export function diffArtifacts(artifactA, artifactB, artifactName, findingIdentityMap = null) {
61
+ const diffs = [];
62
+
63
+ // Check if artifacts exist
64
+ if (!artifactA && artifactB) {
65
+ diffs.push({
66
+ category: DIFF_CATEGORY.ARTIFACTS,
67
+ severity: DIFF_SEVERITY.BLOCKER,
68
+ reasonCode: DIFF_REASON.MISSING_ARTIFACT,
69
+ message: `Artifact ${artifactName} missing in first run`,
70
+ artifact: artifactName,
71
+ });
72
+ return diffs;
73
+ }
74
+
75
+ if (artifactA && !artifactB) {
76
+ diffs.push({
77
+ category: DIFF_CATEGORY.ARTIFACTS,
78
+ severity: DIFF_SEVERITY.BLOCKER,
79
+ reasonCode: DIFF_REASON.MISSING_ARTIFACT,
80
+ message: `Artifact ${artifactName} missing in second run`,
81
+ artifact: artifactName,
82
+ });
83
+ return diffs;
84
+ }
85
+
86
+ if (!artifactA && !artifactB) {
87
+ return diffs; // Both missing, no diff
88
+ }
89
+
90
+ // Artifact-specific diffing
91
+ if (artifactName === 'findings') {
92
+ diffs.push(...diffFindings(artifactA, artifactB, findingIdentityMap));
93
+ } else if (artifactName === 'runMeta') {
94
+ diffs.push(...diffRunMeta(artifactA, artifactB));
95
+ } else if (artifactName === 'determinismContract') {
96
+ diffs.push(...diffDeterminismContract(artifactA, artifactB));
97
+ } else if (artifactName === 'confidenceReport' || artifactName === 'guardrailsReport' || artifactName === 'evidenceIntent') {
98
+ diffs.push(...diffReportArtifact(artifactA, artifactB, artifactName));
99
+ } else {
100
+ // Generic diff for other artifacts
101
+ diffs.push(...diffGeneric(artifactA, artifactB, artifactName));
102
+ }
103
+
104
+ return diffs;
105
+ }
106
+
107
+ /**
108
+ * Diff findings artifacts
109
+ */
110
+ function diffFindings(artifactA, artifactB, findingIdentityMap) {
111
+ const diffs = [];
112
+
113
+ const findingsA = artifactA.findings || [];
114
+ const findingsB = artifactB.findings || [];
115
+
116
+ // Check counts
117
+ if (findingsA.length !== findingsB.length) {
118
+ diffs.push({
119
+ category: DIFF_CATEGORY.FINDINGS,
120
+ severity: DIFF_SEVERITY.BLOCKER,
121
+ reasonCode: DIFF_REASON.OBSERVATION_COUNT_CHANGED,
122
+ message: `Finding count changed: ${findingsA.length} → ${findingsB.length}`,
123
+ artifact: 'findings',
124
+ field: 'findings.length',
125
+ oldValue: findingsA.length,
126
+ newValue: findingsB.length,
127
+ });
128
+ }
129
+
130
+ // Build identity maps if not provided
131
+ const mapA = new Map();
132
+ const mapB = new Map();
133
+
134
+ for (const finding of findingsA) {
135
+ const identity = findingIdentityMap ? findingIdentityMap.get(finding) : computeFindingIdentitySimple(finding);
136
+ mapA.set(identity, finding);
137
+ }
138
+
139
+ for (const finding of findingsB) {
140
+ const identity = findingIdentityMap ? findingIdentityMap.get(finding) : computeFindingIdentitySimple(finding);
141
+ mapB.set(identity, finding);
142
+ }
143
+
144
+ // Find added findings
145
+ for (const [identity, finding] of mapB) {
146
+ if (!mapA.has(identity)) {
147
+ diffs.push({
148
+ category: DIFF_CATEGORY.FINDINGS,
149
+ severity: DIFF_SEVERITY.BLOCKER,
150
+ reasonCode: DIFF_REASON.FINDING_ADDED,
151
+ message: `Finding added: ${finding.type || 'unknown'}`,
152
+ artifact: 'findings',
153
+ findingIdentity: identity,
154
+ finding: finding,
155
+ });
156
+ }
157
+ }
158
+
159
+ // Find removed findings
160
+ for (const [identity, finding] of mapA) {
161
+ if (!mapB.has(identity)) {
162
+ diffs.push({
163
+ category: DIFF_CATEGORY.FINDINGS,
164
+ severity: DIFF_SEVERITY.BLOCKER,
165
+ reasonCode: DIFF_REASON.FINDING_REMOVED,
166
+ message: `Finding removed: ${finding.type || 'unknown'}`,
167
+ artifact: 'findings',
168
+ findingIdentity: identity,
169
+ finding: finding,
170
+ });
171
+ }
172
+ }
173
+
174
+ // Find changed findings
175
+ for (const [identity, findingA] of mapA) {
176
+ const findingB = mapB.get(identity);
177
+ if (findingB) {
178
+ diffs.push(...diffFinding(findingA, findingB, identity));
179
+ }
180
+ }
181
+
182
+ return diffs;
183
+ }
184
+
185
+ /**
186
+ * Diff individual finding
187
+ */
188
+ function diffFinding(findingA, findingB, identity) {
189
+ const diffs = [];
190
+
191
+ // Check status/severity
192
+ const statusA = findingA.severity || findingA.status;
193
+ const statusB = findingB.severity || findingB.status;
194
+ if (statusA !== statusB) {
195
+ diffs.push({
196
+ category: DIFF_CATEGORY.FINDINGS,
197
+ severity: DIFF_SEVERITY.BLOCKER,
198
+ reasonCode: DIFF_REASON.FINDING_SEVERITY_CHANGED,
199
+ message: `Finding severity changed: ${statusA} → ${statusB}`,
200
+ artifact: 'findings',
201
+ findingIdentity: identity,
202
+ field: 'severity',
203
+ oldValue: statusA,
204
+ newValue: statusB,
205
+ });
206
+ }
207
+
208
+ // Check confidence
209
+ const confA = findingA.confidence || 0;
210
+ const confB = findingB.confidence || 0;
211
+ if (Math.abs(confA - confB) > 0.001) {
212
+ diffs.push({
213
+ category: DIFF_CATEGORY.FINDINGS,
214
+ severity: DIFF_SEVERITY.WARN,
215
+ reasonCode: DIFF_REASON.CONFIDENCE_CHANGED,
216
+ message: `Finding confidence changed: ${confA.toFixed(3)} → ${confB.toFixed(3)}`,
217
+ artifact: 'findings',
218
+ findingIdentity: identity,
219
+ field: 'confidence',
220
+ oldValue: confA,
221
+ newValue: confB,
222
+ });
223
+ }
224
+
225
+ // Check confidence reasons
226
+ const reasonsA = findingA.confidenceReasons || [];
227
+ const reasonsB = findingB.confidenceReasons || [];
228
+ if (JSON.stringify(reasonsA.sort()) !== JSON.stringify(reasonsB.sort())) {
229
+ diffs.push({
230
+ category: DIFF_CATEGORY.FINDINGS,
231
+ severity: DIFF_SEVERITY.WARN,
232
+ reasonCode: DIFF_REASON.CONFIDENCE_REASONS_CHANGED,
233
+ message: `Finding confidence reasons changed`,
234
+ artifact: 'findings',
235
+ findingIdentity: identity,
236
+ field: 'confidenceReasons',
237
+ oldValue: reasonsA,
238
+ newValue: reasonsB,
239
+ });
240
+ }
241
+
242
+ // Check guardrails
243
+ const guardrailsA = findingA.guardrails;
244
+ const guardrailsB = findingB.guardrails;
245
+ if (guardrailsA || guardrailsB) {
246
+ if (!guardrailsA || !guardrailsB) {
247
+ diffs.push({
248
+ category: DIFF_CATEGORY.FINDINGS,
249
+ severity: DIFF_SEVERITY.WARN,
250
+ reasonCode: DIFF_REASON.GUARDRAILS_CHANGED,
251
+ message: `Finding guardrails presence changed`,
252
+ artifact: 'findings',
253
+ findingIdentity: identity,
254
+ field: 'guardrails',
255
+ });
256
+ } else if (guardrailsA.finalDecision !== guardrailsB.finalDecision) {
257
+ diffs.push({
258
+ category: DIFF_CATEGORY.FINDINGS,
259
+ severity: DIFF_SEVERITY.WARN,
260
+ reasonCode: DIFF_REASON.GUARDRAILS_CHANGED,
261
+ message: `Finding guardrails decision changed: ${guardrailsA.finalDecision} → ${guardrailsB.finalDecision}`,
262
+ artifact: 'findings',
263
+ findingIdentity: identity,
264
+ field: 'guardrails.finalDecision',
265
+ oldValue: guardrailsA.finalDecision,
266
+ newValue: guardrailsB.finalDecision,
267
+ });
268
+ }
269
+ }
270
+
271
+ // Check evidence completeness
272
+ const evidenceA = findingA.evidenceCompleteness;
273
+ const evidenceB = findingB.evidenceCompleteness;
274
+ if (evidenceA || evidenceB) {
275
+ if (!evidenceA || !evidenceB) {
276
+ diffs.push({
277
+ category: DIFF_CATEGORY.EVIDENCE,
278
+ severity: DIFF_SEVERITY.BLOCKER,
279
+ reasonCode: DIFF_REASON.EVIDENCE_COMPLETENESS_CHANGED,
280
+ message: `Finding evidence completeness presence changed`,
281
+ artifact: 'findings',
282
+ findingIdentity: identity,
283
+ field: 'evidenceCompleteness',
284
+ });
285
+ } else if (evidenceA.isComplete !== evidenceB.isComplete) {
286
+ diffs.push({
287
+ category: DIFF_CATEGORY.EVIDENCE,
288
+ severity: DIFF_SEVERITY.BLOCKER,
289
+ reasonCode: DIFF_REASON.EVIDENCE_COMPLETENESS_CHANGED,
290
+ message: `Finding evidence completeness changed: ${evidenceA.isComplete} → ${evidenceB.isComplete}`,
291
+ artifact: 'findings',
292
+ findingIdentity: identity,
293
+ field: 'evidenceCompleteness.isComplete',
294
+ oldValue: evidenceA.isComplete,
295
+ newValue: evidenceB.isComplete,
296
+ });
297
+ }
298
+ }
299
+
300
+ // Check evidencePackage presence
301
+ const evidencePackageA = findingA.evidencePackage;
302
+ const evidencePackageB = findingB.evidencePackage;
303
+ if (!evidencePackageA && evidencePackageB) {
304
+ diffs.push({
305
+ category: DIFF_CATEGORY.EVIDENCE,
306
+ severity: DIFF_SEVERITY.BLOCKER,
307
+ reasonCode: DIFF_REASON.EVIDENCE_MISSING,
308
+ message: `Finding evidence package missing in first run`,
309
+ artifact: 'findings',
310
+ findingIdentity: identity,
311
+ field: 'evidencePackage',
312
+ });
313
+ } else if (evidencePackageA && !evidencePackageB) {
314
+ diffs.push({
315
+ category: DIFF_CATEGORY.EVIDENCE,
316
+ severity: DIFF_SEVERITY.BLOCKER,
317
+ reasonCode: DIFF_REASON.EVIDENCE_MISSING,
318
+ message: `Finding evidence package missing in second run`,
319
+ artifact: 'findings',
320
+ findingIdentity: identity,
321
+ field: 'evidencePackage',
322
+ });
323
+ }
324
+
325
+ return diffs;
326
+ }
327
+
328
+ /**
329
+ * Diff generic artifacts
330
+ */
331
+ function diffGeneric(artifactA, artifactB, artifactName) {
332
+ const diffs = [];
333
+
334
+ // Simple deep comparison
335
+ const jsonA = JSON.stringify(artifactA, null, 2);
336
+ const jsonB = JSON.stringify(artifactB, null, 2);
337
+
338
+ if (jsonA !== jsonB) {
339
+ diffs.push({
340
+ category: DIFF_CATEGORY.ARTIFACTS,
341
+ severity: DIFF_SEVERITY.WARN,
342
+ reasonCode: DIFF_REASON.FIELD_VALUE_CHANGED,
343
+ message: `Artifact ${artifactName} content changed`,
344
+ artifact: artifactName,
345
+ });
346
+ }
347
+
348
+ return diffs;
349
+ }
350
+
351
+ /**
352
+ * Simple finding identity computation (fallback)
353
+ */
354
+ function computeFindingIdentitySimple(finding) {
355
+ const parts = [
356
+ finding.type || 'unknown',
357
+ finding.interaction?.type || '',
358
+ finding.interaction?.selector || '',
359
+ finding.expectation?.targetPath || '',
360
+ finding.expectation?.urlPath || '',
361
+ ];
362
+ return parts.join('|');
363
+ }
364
+