@veraxhq/verax 0.3.0 → 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 (191) hide show
  1. package/README.md +28 -20
  2. package/bin/verax.js +11 -18
  3. package/package.json +28 -7
  4. package/src/cli/commands/baseline.js +1 -2
  5. package/src/cli/commands/default.js +72 -81
  6. package/src/cli/commands/doctor.js +29 -0
  7. package/src/cli/commands/ga.js +3 -0
  8. package/src/cli/commands/gates.js +1 -1
  9. package/src/cli/commands/inspect.js +6 -133
  10. package/src/cli/commands/release-check.js +2 -0
  11. package/src/cli/commands/run.js +74 -246
  12. package/src/cli/commands/security-check.js +2 -1
  13. package/src/cli/commands/truth.js +0 -1
  14. package/src/cli/entry.js +82 -309
  15. package/src/cli/util/angular-component-extractor.js +2 -2
  16. package/src/cli/util/angular-navigation-detector.js +2 -2
  17. package/src/cli/util/ast-interactive-detector.js +4 -6
  18. package/src/cli/util/ast-network-detector.js +3 -3
  19. package/src/cli/util/ast-promise-extractor.js +581 -0
  20. package/src/cli/util/ast-usestate-detector.js +3 -3
  21. package/src/cli/util/atomic-write.js +12 -1
  22. package/src/cli/util/console-reporter.js +72 -0
  23. package/src/cli/util/detection-engine.js +105 -41
  24. package/src/cli/util/determinism-runner.js +2 -1
  25. package/src/cli/util/determinism-writer.js +1 -1
  26. package/src/cli/util/digest-engine.js +359 -0
  27. package/src/cli/util/dom-diff.js +226 -0
  28. package/src/cli/util/env-url.js +0 -4
  29. package/src/cli/util/evidence-engine.js +287 -0
  30. package/src/cli/util/expectation-extractor.js +217 -367
  31. package/src/cli/util/findings-writer.js +19 -126
  32. package/src/cli/util/framework-detector.js +572 -0
  33. package/src/cli/util/idgen.js +1 -1
  34. package/src/cli/util/interaction-planner.js +529 -0
  35. package/src/cli/util/learn-writer.js +2 -2
  36. package/src/cli/util/ledger-writer.js +110 -0
  37. package/src/cli/util/monorepo-resolver.js +162 -0
  38. package/src/cli/util/observation-engine.js +127 -278
  39. package/src/cli/util/observe-writer.js +2 -2
  40. package/src/cli/util/paths.js +12 -3
  41. package/src/cli/util/project-discovery.js +284 -3
  42. package/src/cli/util/project-writer.js +2 -2
  43. package/src/cli/util/run-id.js +23 -27
  44. package/src/cli/util/run-result.js +778 -0
  45. package/src/cli/util/selector-resolver.js +235 -0
  46. package/src/cli/util/summary-writer.js +2 -1
  47. package/src/cli/util/svelte-navigation-detector.js +3 -3
  48. package/src/cli/util/svelte-sfc-extractor.js +0 -1
  49. package/src/cli/util/svelte-state-detector.js +1 -2
  50. package/src/cli/util/trust-activation-integration.js +496 -0
  51. package/src/cli/util/trust-activation-wrapper.js +85 -0
  52. package/src/cli/util/trust-integration-hooks.js +164 -0
  53. package/src/cli/util/types.js +153 -0
  54. package/src/cli/util/url-validation.js +40 -0
  55. package/src/cli/util/vue-navigation-detector.js +4 -3
  56. package/src/cli/util/vue-sfc-extractor.js +1 -2
  57. package/src/cli/util/vue-state-detector.js +1 -1
  58. package/src/types/fs-augment.d.ts +23 -0
  59. package/src/types/global.d.ts +137 -0
  60. package/src/types/internal-types.d.ts +35 -0
  61. package/src/verax/cli/finding-explainer.js +3 -56
  62. package/src/verax/cli/init.js +4 -18
  63. package/src/verax/core/action-classifier.js +4 -3
  64. package/src/verax/core/artifacts/registry.js +0 -15
  65. package/src/verax/core/artifacts/verifier.js +18 -8
  66. package/src/verax/core/baseline/baseline.snapshot.js +2 -0
  67. package/src/verax/core/capabilities/gates.js +7 -1
  68. package/src/verax/core/confidence/confidence-compute.js +14 -7
  69. package/src/verax/core/confidence/confidence.loader.js +1 -0
  70. package/src/verax/core/confidence-engine-refactor.js +8 -3
  71. package/src/verax/core/confidence-engine.js +162 -23
  72. package/src/verax/core/contracts/types.js +1 -0
  73. package/src/verax/core/contracts/validators.js +79 -4
  74. package/src/verax/core/decision-snapshot.js +3 -30
  75. package/src/verax/core/decisions/decision.trace.js +2 -0
  76. package/src/verax/core/determinism/contract-writer.js +2 -2
  77. package/src/verax/core/determinism/contract.js +1 -1
  78. package/src/verax/core/determinism/diff.js +42 -1
  79. package/src/verax/core/determinism/engine.js +7 -6
  80. package/src/verax/core/determinism/finding-identity.js +3 -2
  81. package/src/verax/core/determinism/normalize.js +32 -4
  82. package/src/verax/core/determinism/report-writer.js +1 -0
  83. package/src/verax/core/determinism/run-fingerprint.js +7 -2
  84. package/src/verax/core/dynamic-route-intelligence.js +8 -7
  85. package/src/verax/core/evidence/evidence-capture-service.js +1 -0
  86. package/src/verax/core/evidence/evidence-intent-ledger.js +2 -1
  87. package/src/verax/core/evidence-builder.js +2 -2
  88. package/src/verax/core/execution-mode-context.js +1 -1
  89. package/src/verax/core/execution-mode-detector.js +5 -3
  90. package/src/verax/core/failures/exit-codes.js +39 -37
  91. package/src/verax/core/failures/failure-summary.js +1 -1
  92. package/src/verax/core/failures/failure.factory.js +3 -3
  93. package/src/verax/core/failures/failure.ledger.js +3 -2
  94. package/src/verax/core/ga/ga.artifact.js +1 -1
  95. package/src/verax/core/ga/ga.contract.js +3 -2
  96. package/src/verax/core/ga/ga.enforcer.js +1 -0
  97. package/src/verax/core/guardrails/policy.loader.js +1 -0
  98. package/src/verax/core/guardrails/truth-reconciliation.js +1 -1
  99. package/src/verax/core/guardrails-engine.js +2 -2
  100. package/src/verax/core/incremental-store.js +1 -0
  101. package/src/verax/core/integrity/budget.js +138 -0
  102. package/src/verax/core/integrity/determinism.js +342 -0
  103. package/src/verax/core/integrity/integrity.js +208 -0
  104. package/src/verax/core/integrity/poisoning.js +108 -0
  105. package/src/verax/core/integrity/transaction.js +140 -0
  106. package/src/verax/core/observe/run-timeline.js +2 -0
  107. package/src/verax/core/perf/perf.report.js +2 -0
  108. package/src/verax/core/pipeline-tracker.js +5 -0
  109. package/src/verax/core/release/provenance.builder.js +73 -214
  110. package/src/verax/core/release/release.enforcer.js +14 -9
  111. package/src/verax/core/release/reproducibility.check.js +1 -0
  112. package/src/verax/core/release/sbom.builder.js +32 -23
  113. package/src/verax/core/replay-validator.js +2 -0
  114. package/src/verax/core/replay.js +4 -0
  115. package/src/verax/core/report/cross-index.js +6 -3
  116. package/src/verax/core/report/human-summary.js +141 -1
  117. package/src/verax/core/route-intelligence.js +4 -3
  118. package/src/verax/core/run-id.js +6 -3
  119. package/src/verax/core/run-manifest.js +4 -3
  120. package/src/verax/core/security/secrets.scan.js +10 -7
  121. package/src/verax/core/security/security.enforcer.js +4 -0
  122. package/src/verax/core/security/supplychain.policy.js +9 -1
  123. package/src/verax/core/security/vuln.scan.js +2 -2
  124. package/src/verax/core/truth/truth.certificate.js +3 -1
  125. package/src/verax/core/ui-feedback-intelligence.js +12 -46
  126. package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
  127. package/src/verax/detect/confidence-engine.js +100 -660
  128. package/src/verax/detect/confidence-helper.js +1 -0
  129. package/src/verax/detect/detection-engine.js +1 -18
  130. package/src/verax/detect/dynamic-route-findings.js +17 -14
  131. package/src/verax/detect/expectation-chain-detector.js +1 -1
  132. package/src/verax/detect/expectation-model.js +3 -5
  133. package/src/verax/detect/failure-cause-inference.js +293 -0
  134. package/src/verax/detect/findings-writer.js +126 -166
  135. package/src/verax/detect/flow-detector.js +2 -2
  136. package/src/verax/detect/form-silent-failure.js +98 -0
  137. package/src/verax/detect/index.js +51 -234
  138. package/src/verax/detect/invariants-enforcer.js +147 -0
  139. package/src/verax/detect/journey-stall-detector.js +4 -4
  140. package/src/verax/detect/navigation-silent-failure.js +82 -0
  141. package/src/verax/detect/problem-aggregator.js +361 -0
  142. package/src/verax/detect/route-findings.js +7 -6
  143. package/src/verax/detect/summary-writer.js +477 -0
  144. package/src/verax/detect/test-failure-cause-inference.js +314 -0
  145. package/src/verax/detect/ui-feedback-findings.js +18 -18
  146. package/src/verax/detect/verdict-engine.js +3 -57
  147. package/src/verax/detect/view-switch-correlator.js +2 -2
  148. package/src/verax/flow/flow-engine.js +2 -1
  149. package/src/verax/flow/flow-spec.js +0 -6
  150. package/src/verax/index.js +48 -412
  151. package/src/verax/intel/ts-program.js +1 -0
  152. package/src/verax/intel/vue-navigation-extractor.js +3 -0
  153. package/src/verax/learn/action-contract-extractor.js +67 -682
  154. package/src/verax/learn/ast-contract-extractor.js +1 -1
  155. package/src/verax/learn/flow-extractor.js +1 -0
  156. package/src/verax/learn/project-detector.js +5 -0
  157. package/src/verax/learn/react-router-extractor.js +2 -0
  158. package/src/verax/learn/route-validator.js +1 -4
  159. package/src/verax/learn/source-instrumenter.js +1 -0
  160. package/src/verax/learn/state-extractor.js +2 -1
  161. package/src/verax/learn/static-extractor.js +1 -0
  162. package/src/verax/observe/coverage-gaps.js +132 -0
  163. package/src/verax/observe/expectation-handler.js +126 -0
  164. package/src/verax/observe/incremental-skip.js +46 -0
  165. package/src/verax/observe/index.js +735 -84
  166. package/src/verax/observe/interaction-executor.js +192 -0
  167. package/src/verax/observe/interaction-runner.js +782 -530
  168. package/src/verax/observe/network-firewall.js +86 -0
  169. package/src/verax/observe/observation-builder.js +169 -0
  170. package/src/verax/observe/observe-context.js +1 -1
  171. package/src/verax/observe/observe-helpers.js +2 -1
  172. package/src/verax/observe/observe-runner.js +28 -24
  173. package/src/verax/observe/observers/budget-observer.js +3 -3
  174. package/src/verax/observe/observers/console-observer.js +4 -4
  175. package/src/verax/observe/observers/coverage-observer.js +4 -4
  176. package/src/verax/observe/observers/interaction-observer.js +3 -3
  177. package/src/verax/observe/observers/navigation-observer.js +4 -4
  178. package/src/verax/observe/observers/network-observer.js +4 -4
  179. package/src/verax/observe/observers/safety-observer.js +1 -1
  180. package/src/verax/observe/observers/ui-feedback-observer.js +4 -4
  181. package/src/verax/observe/page-traversal.js +138 -0
  182. package/src/verax/observe/snapshot-ops.js +94 -0
  183. package/src/verax/observe/ui-signal-sensor.js +2 -148
  184. package/src/verax/scan-summary-writer.js +10 -42
  185. package/src/verax/shared/artifact-manager.js +30 -13
  186. package/src/verax/shared/caching.js +1 -0
  187. package/src/verax/shared/expectation-tracker.js +1 -0
  188. package/src/verax/shared/zip-artifacts.js +6 -0
  189. package/src/verax/core/confidence-engine.js.backup +0 -471
  190. package/src/verax/shared/config-loader.js +0 -169
  191. /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
@@ -1,196 +1,156 @@
1
1
  import { resolve } from 'path';
2
2
  import { mkdirSync, writeFileSync } from 'fs';
3
3
  import { CANONICAL_OUTCOMES } from '../core/canonical-outcomes.js';
4
- import { enforceContractsOnFindings, FINDING_STATUS } from '../core/contracts/index.js';
5
- import { ARTIFACT_REGISTRY, getArtifactVersions } from '../core/artifacts/registry.js';
6
- import { buildAndEnforceEvidencePackage, EvidenceBuildError, validateEvidencePackageStrict } from '../core/evidence-builder.js';
7
- import { writeEvidenceIntentLedger } from '../core/evidence/evidence-intent-ledger.js';
4
+ import { ARTIFACT_REGISTRY } from '../core/artifacts/registry.js';
8
5
 
9
- /**
10
- * Write findings to canonical artifact root.
11
- * Writes to .verax/runs/<runId>/findings.json.
12
- *
13
- * PHASE 0 ENFORCEMENT: Applies contracts enforcement
14
- * - Downgrades findings without evidence from CONFIRMED to SUSPECTED
15
- * - Drops findings that violate critical contracts
16
- *
17
- * PHASE 2: Includes outcome classification summary.
18
- * PHASE 3: Includes promise type summary.
19
- *
20
- * @param {string} projectDir
21
- * @param {string} url
22
- * @param {Array} findings
23
- * @param {Array} coverageGaps
24
- * @param {string} runDirOpt - Required absolute run directory path
25
- * @returns {Object} findings report with enforcement metadata
26
- */
27
- export function writeFindings(projectDir, url, findings, coverageGaps = [], runDirOpt) {
28
- if (!runDirOpt) {
29
- throw new Error('runDirOpt is required');
30
- }
31
- mkdirSync(runDirOpt, { recursive: true });
32
- const findingsPath = resolve(runDirOpt, ARTIFACT_REGISTRY.findings.filename);
6
+ const DEFAULT_CLOCK = () => new Date().toISOString();
33
7
 
34
- // ARCHITECTURAL HARDENING: Evidence capture failures downgrade findings, never drop them
35
- // A finding must NEVER disappear. If evidence capture fails, downgrade explicitly.
36
- const findingsWithEvidence = [];
37
- const evidenceBuildFailures = [];
38
-
39
- for (const finding of (findings || [])) {
40
- // If evidencePackage already exists, validate it strictly for CONFIRMED findings
41
- if (finding.evidencePackage) {
42
- const severity = finding.severity || finding.status || 'SUSPECTED';
43
- if (severity === 'CONFIRMED') {
44
- try {
45
- validateEvidencePackageStrict(finding.evidencePackage, severity);
46
- findingsWithEvidence.push(finding);
47
- } catch (error) {
48
- // CONFIRMED finding with incomplete evidencePackage → DOWNGRADE to SUSPECTED
49
- const downgradedFinding = {
50
- ...finding,
51
- severity: 'SUSPECTED',
52
- status: 'SUSPECTED',
53
- evidenceCompleteness: {
54
- downgraded: true,
55
- reason: `Evidence package incomplete: ${error.message}`,
56
- originalSeverity: 'CONFIRMED'
57
- }
58
- };
59
- findingsWithEvidence.push(downgradedFinding);
60
- evidenceBuildFailures.push({
61
- finding: { type: finding.type || 'unknown', id: finding.findingId || finding.id },
62
- reason: `EVIDENCE_VALIDATION_FAILED: ${error.message}`,
63
- errorCode: error.code || 'EVIDENCE_VALIDATION_FAILED',
64
- action: 'DOWNGRADED'
65
- });
66
- }
67
- } else {
68
- findingsWithEvidence.push(finding);
69
- }
8
+ // Pure: builds deterministic report object from provided data and timestamp
9
+ export function buildFindingsReport({ url, findings = [], coverageGaps = [], detectedAt }) {
10
+ const outcomeSummary = {};
11
+ Object.values(CANONICAL_OUTCOMES).forEach(outcome => {
12
+ outcomeSummary[outcome] = 0;
13
+ });
14
+
15
+ const promiseSummary = {};
16
+
17
+ // Contract enforcement: separate findings into valid, downgradable, and droppable
18
+ const downgrades = [];
19
+ const droppedFindingIds = [];
20
+ const enforcedFindings = [];
21
+
22
+ for (const finding of findings) {
23
+ // Check for critical missing fields (drop these)
24
+ const hasCriticalNarrativeFields =
25
+ finding.what_happened &&
26
+ finding.what_was_expected &&
27
+ finding.what_was_observed &&
28
+ finding.why_it_matters;
29
+
30
+ const hasRequiredType = finding.type;
31
+
32
+ if (!hasRequiredType || !hasCriticalNarrativeFields) {
33
+ droppedFindingIds.push(finding.id || 'unknown');
70
34
  continue;
71
35
  }
72
-
73
- // Build evidence package from finding data
74
- // If building fails DOWNGRADE finding (never drop)
75
- try {
76
- const findingWithEvidence = buildAndEnforceEvidencePackage(finding, {
77
- expectation: finding.expectation || null,
78
- trace: {
79
- interaction: finding.interaction || {},
80
- before: finding.evidence?.before ? { screenshot: finding.evidence.before, url: finding.evidence.beforeUrl } : null,
81
- after: finding.evidence?.after ? { screenshot: finding.evidence.after, url: finding.evidence.afterUrl } : null,
82
- sensors: finding.evidence?.sensors || {},
83
- dom: finding.evidence?.dom || {},
84
- },
85
- evidence: finding.evidence || {},
86
- confidence: finding.confidenceLevel ? {
87
- level: finding.confidenceLevel,
88
- score: finding.confidence,
89
- reasons: finding.confidenceReasons || [],
90
- } : null,
91
- });
92
- findingsWithEvidence.push(findingWithEvidence);
93
- } catch (error) {
94
- // Evidence building failure → DOWNGRADE finding (never drop)
95
- const originalSeverity = finding.severity || finding.status || 'SUSPECTED';
36
+
37
+ // Check for Evidence Law violation (downgrade)
38
+ if (finding.status === 'CONFIRMED' && (!finding.evidence || Object.keys(finding.evidence).length === 0)) {
96
39
  const downgradedFinding = {
97
40
  ...finding,
98
- severity: 'SUSPECTED',
99
41
  status: 'SUSPECTED',
100
- evidenceCompleteness: {
101
- downgraded: true,
102
- reason: `Evidence capture failed: ${error.message}`,
103
- originalSeverity: originalSeverity,
104
- errorCode: error.code || 'EVIDENCE_BUILD_FAILED',
105
- missingFields: error.missingFields || []
106
- }
42
+ reason: (finding.reason || '') + ' (Evidence Law enforced - no evidence exists for CONFIRMED status)'
107
43
  };
108
- findingsWithEvidence.push(downgradedFinding);
109
-
110
- if (error instanceof EvidenceBuildError) {
111
- evidenceBuildFailures.push({
112
- finding: { type: finding.type || 'unknown', id: finding.findingId || finding.id },
113
- reason: `EVIDENCE_BUILD_FAILED: ${error.message}`,
114
- errorCode: error.code || 'EVIDENCE_BUILD_FAILED',
115
- missingFields: error.missingFields || [],
116
- action: 'DOWNGRADED'
117
- });
118
- } else {
119
- evidenceBuildFailures.push({
120
- finding: { type: finding.type || 'unknown', id: finding.findingId || finding.id },
121
- reason: `EVIDENCE_BUILD_FAILED: Unexpected error: ${error.message}`,
122
- errorCode: 'EVIDENCE_BUILD_FAILED',
123
- action: 'DOWNGRADED'
124
- });
125
- }
44
+ downgrades.push({
45
+ id: finding.id || 'unknown',
46
+ originalStatus: 'CONFIRMED',
47
+ downgradeToStatus: 'SUSPECTED',
48
+ reason: 'Evidence Law enforced - no evidence exists for CONFIRMED status'
49
+ });
50
+ enforcedFindings.push(downgradedFinding);
51
+ } else {
52
+ // Valid finding
53
+ enforcedFindings.push(finding);
126
54
  }
127
55
  }
128
-
129
- // PHASE 0: Enforce contracts on all findings (Evidence Law)
130
- const { valid: enforcedFindings, dropped, downgrades } = enforceContractsOnFindings(findingsWithEvidence);
131
-
132
- // ARCHITECTURAL HARDENING: Track evidence build failures separately (they result in downgrades, not drops)
133
- // Only contract violations result in drops
134
- const allDropped = dropped;
135
-
136
- // PHASE 2: Compute outcome summary (using enforced findings)
137
- const outcomeSummary = {};
138
- Object.values(CANONICAL_OUTCOMES).forEach(outcome => {
139
- outcomeSummary[outcome] = 0;
140
- });
141
-
142
- // PHASE 3: Compute promise summary
143
- const promiseSummary = {};
144
-
56
+
57
+ // Build outcome and promise summary from enforced findings
145
58
  for (const finding of enforcedFindings) {
146
59
  const outcome = finding.outcome || CANONICAL_OUTCOMES.SILENT_FAILURE;
147
60
  outcomeSummary[outcome] = (outcomeSummary[outcome] || 0) + 1;
148
-
61
+
149
62
  const promiseType = finding.promise?.type || 'UNKNOWN_PROMISE';
150
63
  promiseSummary[promiseType] = (promiseSummary[promiseType] || 0) + 1;
151
64
  }
152
65
 
153
- const findingsReport = {
66
+ // Sort findings deterministically by id
67
+ const sortedFindings = enforcedFindings.sort((a, b) => (a.id || '').localeCompare(b.id || ''));
68
+
69
+ // Production report: exclude enforcement metadata (Trust Lock)
70
+ return {
154
71
  version: 1,
155
- contractVersion: 1, // Track schema changes from contracts enforcement
156
- artifactVersions: getArtifactVersions(),
157
- detectedAt: new Date().toISOString(),
158
- url: url,
159
- outcomeSummary: outcomeSummary, // PHASE 2
160
- promiseSummary: promiseSummary, // PHASE 3
161
- findings: enforcedFindings,
162
- coverageGaps: coverageGaps,
163
- // PHASE 0: Enforcement metadata
164
- // PHASE 21.1: Include evidence build failures in dropped count
165
- enforcement: {
166
- droppedCount: allDropped.length,
167
- downgradedCount: downgrades.length,
168
- downgrades: downgrades.map(d => ({
169
- reason: d.reason,
170
- originalStatus: d.original.status,
171
- downgradeToStatus: d.downgraded.status
172
- })),
173
- evidenceBuildFailures: evidenceBuildFailures.map(f => ({
174
- reason: f.reason,
175
- errorCode: f.errorCode,
176
- findingType: f.finding.type || 'unknown',
177
- findingId: f.finding.id || null,
178
- missingFields: f.missingFields || [],
179
- action: f.action || 'DOWNGRADED'
180
- }))
181
- },
72
+ contractVersion: ARTIFACT_REGISTRY.findings.contractVersion,
73
+ detectedAt: detectedAt,
74
+ url,
75
+ outcomeSummary, // PHASE 2
76
+ promiseSummary, // PHASE 3
77
+ findings: sortedFindings,
78
+ coverageGaps,
182
79
  notes: []
183
80
  };
81
+ }
184
82
 
185
- writeFileSync(findingsPath, JSON.stringify(findingsReport, null, 2) + '\n');
83
+ // Internal helper: builds report with enforcement metadata for disk persistence
84
+ function buildFindingsReportWithEnforcement({ url, findings = [], coverageGaps = [], detectedAt }) {
85
+ const report = buildFindingsReport({ url, findings, coverageGaps, detectedAt });
86
+
87
+ const downgrades = [];
88
+ const droppedFindingIds = [];
186
89
 
187
- // PHASE 22: Write evidence intent ledger
188
- const evidenceIntentPath = writeEvidenceIntentLedger(runDirOpt, enforcedFindings, captureFailuresMap);
90
+ for (const finding of findings) {
91
+ const hasCriticalNarrativeFields =
92
+ finding.what_happened &&
93
+ finding.what_was_expected &&
94
+ finding.what_was_observed &&
95
+ finding.why_it_matters;
96
+
97
+ const hasRequiredType = finding.type;
98
+
99
+ if (!hasRequiredType || !hasCriticalNarrativeFields) {
100
+ droppedFindingIds.push(finding.id || 'unknown');
101
+ continue;
102
+ }
103
+
104
+ if (finding.status === 'CONFIRMED' && (!finding.evidence || Object.keys(finding.evidence).length === 0)) {
105
+ downgrades.push({
106
+ id: finding.id || 'unknown',
107
+ originalStatus: 'CONFIRMED',
108
+ downgradeToStatus: 'SUSPECTED',
109
+ reason: 'Evidence Law enforced - no evidence exists for CONFIRMED status'
110
+ });
111
+ }
112
+ }
189
113
 
190
114
  return {
191
- ...findingsReport,
192
- findingsPath: findingsPath,
193
- evidenceIntentPath: evidenceIntentPath
115
+ ...report,
116
+ enforcement: {
117
+ evidenceLawEnforced: true,
118
+ contractVersion: 1,
119
+ timestamp: detectedAt,
120
+ droppedCount: droppedFindingIds.length,
121
+ downgradedCount: downgrades.length,
122
+ downgrades: downgrades
123
+ }
194
124
  };
195
125
  }
196
126
 
127
+ // Side-effectful: persists a fully built report to disk
128
+ export function persistFindingsReport(runDir, report) {
129
+ if (!runDir) {
130
+ throw new Error('runDirOpt is required');
131
+ }
132
+ mkdirSync(runDir, { recursive: true });
133
+ const findingsPath = resolve(runDir, 'findings.json');
134
+ writeFileSync(findingsPath, JSON.stringify(report, null, 2) + '\n');
135
+ return { ...report, findingsPath };
136
+ }
137
+
138
+ /**
139
+ * Write findings to canonical artifact root.
140
+ * Writes to .verax/runs/<runId>/findings.json.
141
+ *
142
+ * PHASE 2: Includes outcome classification summary.
143
+ * PHASE 3: Includes promise type summary.
144
+ *
145
+ * @param {string} projectDir
146
+ * @param {string} url
147
+ * @param {Array} findings
148
+ * @param {Array} coverageGaps
149
+ * @param {string} runDirOpt - Required absolute run directory path
150
+ */
151
+ export function writeFindings(projectDir, url, findings, coverageGaps = [], runDirOpt) {
152
+ const detectedAt = DEFAULT_CLOCK();
153
+ const report = buildFindingsReportWithEnforcement({ url, findings: findings || [], coverageGaps: coverageGaps || [], detectedAt });
154
+ return persistFindingsReport(runDirOpt, report);
155
+ }
156
+
@@ -153,7 +153,7 @@ export function detectFlowSilentFailures(traces, manifest, findings, coverageGap
153
153
  // Check navigation expectation
154
154
  if (expectsNavigation(manifest, interaction, beforeUrl)) {
155
155
  const navExp = manifest.staticExpectations?.find(e =>
156
- e.type === 'navigation' &&
156
+ (e.type === 'navigation' || e.type === 'spa_navigation') &&
157
157
  isProvenExpectation(e) &&
158
158
  e.fromPath && getUrlPath(beforeUrl) &&
159
159
  e.fromPath.replace(/\/$/, '') === getUrlPath(beforeUrl).replace(/\/$/, '') &&
@@ -223,7 +223,7 @@ export function detectFlowSilentFailures(traces, manifest, findings, coverageGap
223
223
  }
224
224
 
225
225
  // Also check network expectation even if navigation was matched (for step 2 of flow)
226
- if (matchedExpectation && matchedExpectation.type === 'navigation' && manifest.staticExpectations) {
226
+ if (matchedExpectation && (matchedExpectation.type === 'navigation' || matchedExpectation.type === 'spa_navigation') && manifest.staticExpectations) {
227
227
  const networkExp = manifest.staticExpectations.find(e =>
228
228
  e.type === 'network_action' &&
229
229
  isProvenExpectation(e) &&
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Form Silent Failure Detection
3
+ *
4
+ * Detects form submissions where:
5
+ * - Form submit handler executes
6
+ * - Network request completes with 2xx status
7
+ * - AND no success UI feedback appears (no toast, modal, or DOM change)
8
+ * - AND no navigation occurs
9
+ *
10
+ * CONFIDENCE: HIGH (network + UI evidence)
11
+ * Note: Does NOT attempt to parse response content (unsupported)
12
+ */
13
+
14
+ import { hasMeaningfulUrlChange, hasDomChange } from './comparison.js';
15
+ import { enrichFindingWithExplanations } from './finding-detector.js';
16
+
17
+ export function detectFormSilentFailures(traces, manifest, findings) {
18
+ // Parameters:
19
+ // traces - array of interaction traces from observation
20
+ // manifest - project manifest (contains expectations)
21
+ // findings - array to append new findings to (mutated in-place)
22
+
23
+ for (const trace of traces) {
24
+ const interaction = trace.interaction || {};
25
+
26
+ // Only analyze form interactions
27
+ if (interaction.type !== 'form' && interaction.category !== 'form') {
28
+ continue;
29
+ }
30
+
31
+ const beforeUrl = trace.before?.url || trace.beforeUrl || '';
32
+ const afterUrl = trace.after?.url || trace.afterUrl || '';
33
+ const sensors = trace.sensors || {};
34
+ const network = sensors.network || {};
35
+ const uiSignals = sensors.uiSignals || {};
36
+ const uiDiff = uiSignals.diff || {};
37
+
38
+ const urlChanged = hasMeaningfulUrlChange(beforeUrl, afterUrl);
39
+ const domChanged = hasDomChange(trace);
40
+ const uiChanged = uiDiff.changed === true;
41
+
42
+ // Check for successful network requests (2xx)
43
+ const successNetworkRequest = (network.requests || []).some(req => {
44
+ const status = req.status || req.statusCode || 0;
45
+ return status >= 200 && status < 300;
46
+ });
47
+
48
+ // Detection logic:
49
+ // Form submitted with successful network response but:
50
+ // 1. No navigation (URL unchanged)
51
+ // 2. No DOM change (no new content loaded)
52
+ // 3. No UI feedback (no toast, modal, highlight, etc.)
53
+ if (successNetworkRequest && !urlChanged && !domChanged && !uiChanged) {
54
+ const evidence = {
55
+ before: trace.before?.screenshot || trace.beforeScreenshot || '',
56
+ after: trace.after?.screenshot || trace.afterScreenshot || '',
57
+ beforeUrl,
58
+ afterUrl,
59
+ networkRequests: network.requests || [],
60
+ successRequests: (network.requests || []).filter(r => {
61
+ const status = r.status || r.statusCode || 0;
62
+ return status >= 200 && status < 300;
63
+ }).length,
64
+ uiChanged,
65
+ domChanged,
66
+ urlChanged,
67
+ reason: 'Form submitted successfully but provided no visual feedback'
68
+ };
69
+
70
+ const finding = {
71
+ type: 'form_silent_failure',
72
+ description: `Form submission succeeded with no success feedback to user`,
73
+ summary: `Form submitted successfully (2xx response) but no UI feedback (no toast, message, or redirect)`,
74
+ explanation: `The form submission was completed by the server (2xx status code received), but the application provided no visual feedback to the user. The page remained unchanged, with no success message, toast notification, or redirect.`,
75
+ evidence,
76
+ confidence: {
77
+ level: 0.90, // HIGH - network + UI evidence
78
+ reasons: [
79
+ 'Form submitted and network request returned 2xx (success)',
80
+ 'No UI feedback appeared (no toast, modal, or message)',
81
+ 'No page navigation (user stayed on same page)',
82
+ 'No DOM change (no new content loaded)'
83
+ ]
84
+ },
85
+ promise: {
86
+ type: 'form_submission',
87
+ expected: 'Submit form and display success feedback',
88
+ actual: 'Form submitted successfully but no feedback provided'
89
+ },
90
+ capabilityNote: 'Detection based on network status and visual feedback only. Does not parse response body or validate success semantics (UNSUPPORTED).'
91
+ };
92
+
93
+ // Enrich with explanations
94
+ enrichFindingWithExplanations(finding, trace);
95
+ findings.push(finding);
96
+ }
97
+ }
98
+ }