@veraxhq/verax 0.1.0 → 0.2.1

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 (135) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +24 -36
  4. package/src/cli/commands/default.js +681 -0
  5. package/src/cli/commands/doctor.js +197 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +586 -0
  8. package/src/cli/entry.js +196 -0
  9. package/src/cli/util/atomic-write.js +37 -0
  10. package/src/cli/util/detection-engine.js +297 -0
  11. package/src/cli/util/env-url.js +33 -0
  12. package/src/cli/util/errors.js +44 -0
  13. package/src/cli/util/events.js +110 -0
  14. package/src/cli/util/expectation-extractor.js +388 -0
  15. package/src/cli/util/findings-writer.js +32 -0
  16. package/src/cli/util/idgen.js +87 -0
  17. package/src/cli/util/learn-writer.js +39 -0
  18. package/src/cli/util/observation-engine.js +412 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +30 -0
  21. package/src/cli/util/project-discovery.js +297 -0
  22. package/src/cli/util/project-writer.js +26 -0
  23. package/src/cli/util/redact.js +128 -0
  24. package/src/cli/util/run-id.js +30 -0
  25. package/src/cli/util/runtime-budget.js +147 -0
  26. package/src/cli/util/summary-writer.js +43 -0
  27. package/src/types/global.d.ts +28 -0
  28. package/src/types/ts-ast.d.ts +24 -0
  29. package/src/verax/cli/ci-summary.js +35 -0
  30. package/src/verax/cli/context-explanation.js +89 -0
  31. package/src/verax/cli/doctor.js +277 -0
  32. package/src/verax/cli/error-normalizer.js +154 -0
  33. package/src/verax/cli/explain-output.js +105 -0
  34. package/src/verax/cli/finding-explainer.js +130 -0
  35. package/src/verax/cli/init.js +237 -0
  36. package/src/verax/cli/run-overview.js +163 -0
  37. package/src/verax/cli/url-safety.js +111 -0
  38. package/src/verax/cli/wizard.js +109 -0
  39. package/src/verax/cli/zero-findings-explainer.js +57 -0
  40. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  41. package/src/verax/core/action-classifier.js +86 -0
  42. package/src/verax/core/budget-engine.js +218 -0
  43. package/src/verax/core/canonical-outcomes.js +157 -0
  44. package/src/verax/core/decision-snapshot.js +335 -0
  45. package/src/verax/core/determinism-model.js +432 -0
  46. package/src/verax/core/incremental-store.js +245 -0
  47. package/src/verax/core/invariants.js +356 -0
  48. package/src/verax/core/promise-model.js +230 -0
  49. package/src/verax/core/replay-validator.js +350 -0
  50. package/src/verax/core/replay.js +222 -0
  51. package/src/verax/core/run-id.js +175 -0
  52. package/src/verax/core/run-manifest.js +99 -0
  53. package/src/verax/core/silence-impact.js +369 -0
  54. package/src/verax/core/silence-model.js +523 -0
  55. package/src/verax/detect/comparison.js +7 -34
  56. package/src/verax/detect/confidence-engine.js +764 -329
  57. package/src/verax/detect/detection-engine.js +293 -0
  58. package/src/verax/detect/evidence-index.js +127 -0
  59. package/src/verax/detect/expectation-model.js +241 -168
  60. package/src/verax/detect/explanation-helpers.js +187 -0
  61. package/src/verax/detect/finding-detector.js +450 -0
  62. package/src/verax/detect/findings-writer.js +41 -12
  63. package/src/verax/detect/flow-detector.js +366 -0
  64. package/src/verax/detect/index.js +200 -288
  65. package/src/verax/detect/interactive-findings.js +612 -0
  66. package/src/verax/detect/signal-mapper.js +308 -0
  67. package/src/verax/detect/skip-classifier.js +4 -4
  68. package/src/verax/detect/verdict-engine.js +561 -0
  69. package/src/verax/evidence-index-writer.js +61 -0
  70. package/src/verax/flow/flow-engine.js +3 -2
  71. package/src/verax/flow/flow-spec.js +1 -2
  72. package/src/verax/index.js +103 -15
  73. package/src/verax/intel/effect-detector.js +368 -0
  74. package/src/verax/intel/handler-mapper.js +249 -0
  75. package/src/verax/intel/index.js +281 -0
  76. package/src/verax/intel/route-extractor.js +280 -0
  77. package/src/verax/intel/ts-program.js +256 -0
  78. package/src/verax/intel/vue-navigation-extractor.js +642 -0
  79. package/src/verax/intel/vue-router-extractor.js +325 -0
  80. package/src/verax/learn/action-contract-extractor.js +338 -104
  81. package/src/verax/learn/ast-contract-extractor.js +148 -6
  82. package/src/verax/learn/flow-extractor.js +172 -0
  83. package/src/verax/learn/index.js +36 -2
  84. package/src/verax/learn/manifest-writer.js +122 -58
  85. package/src/verax/learn/project-detector.js +40 -0
  86. package/src/verax/learn/route-extractor.js +28 -97
  87. package/src/verax/learn/route-validator.js +8 -7
  88. package/src/verax/learn/state-extractor.js +212 -0
  89. package/src/verax/learn/static-extractor-navigation.js +114 -0
  90. package/src/verax/learn/static-extractor-validation.js +88 -0
  91. package/src/verax/learn/static-extractor.js +119 -10
  92. package/src/verax/learn/truth-assessor.js +24 -21
  93. package/src/verax/learn/ts-contract-resolver.js +14 -12
  94. package/src/verax/observe/aria-sensor.js +211 -0
  95. package/src/verax/observe/browser.js +30 -6
  96. package/src/verax/observe/console-sensor.js +2 -18
  97. package/src/verax/observe/domain-boundary.js +10 -1
  98. package/src/verax/observe/expectation-executor.js +513 -0
  99. package/src/verax/observe/flow-matcher.js +143 -0
  100. package/src/verax/observe/focus-sensor.js +196 -0
  101. package/src/verax/observe/human-driver.js +660 -273
  102. package/src/verax/observe/index.js +910 -26
  103. package/src/verax/observe/interaction-discovery.js +378 -15
  104. package/src/verax/observe/interaction-runner.js +562 -197
  105. package/src/verax/observe/loading-sensor.js +145 -0
  106. package/src/verax/observe/navigation-sensor.js +255 -0
  107. package/src/verax/observe/network-sensor.js +55 -7
  108. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  109. package/src/verax/observe/observed-expectation.js +305 -0
  110. package/src/verax/observe/page-frontier.js +234 -0
  111. package/src/verax/observe/settle.js +38 -17
  112. package/src/verax/observe/state-sensor.js +393 -0
  113. package/src/verax/observe/state-ui-sensor.js +7 -1
  114. package/src/verax/observe/timing-sensor.js +228 -0
  115. package/src/verax/observe/traces-writer.js +73 -21
  116. package/src/verax/observe/ui-signal-sensor.js +143 -17
  117. package/src/verax/scan-summary-writer.js +80 -15
  118. package/src/verax/shared/artifact-manager.js +111 -9
  119. package/src/verax/shared/budget-profiles.js +136 -0
  120. package/src/verax/shared/caching.js +1 -1
  121. package/src/verax/shared/ci-detection.js +39 -0
  122. package/src/verax/shared/config-loader.js +169 -0
  123. package/src/verax/shared/dynamic-route-utils.js +224 -0
  124. package/src/verax/shared/expectation-coverage.js +44 -0
  125. package/src/verax/shared/expectation-prover.js +81 -0
  126. package/src/verax/shared/expectation-tracker.js +201 -0
  127. package/src/verax/shared/expectations-writer.js +60 -0
  128. package/src/verax/shared/first-run.js +44 -0
  129. package/src/verax/shared/progress-reporter.js +171 -0
  130. package/src/verax/shared/retry-policy.js +9 -1
  131. package/src/verax/shared/root-artifacts.js +49 -0
  132. package/src/verax/shared/scan-budget.js +86 -0
  133. package/src/verax/shared/url-normalizer.js +162 -0
  134. package/src/verax/shared/zip-artifacts.js +66 -0
  135. package/src/verax/validate/context-validator.js +244 -0
@@ -1,8 +1,47 @@
1
- import { resolve } from 'path';
2
1
  import { writeFileSync, mkdirSync } from 'fs';
3
- import { appendTrace } from '../shared/artifact-manager.js';
2
+ import { getArtifactPath, getRunArtifactDir } from '../core/run-id.js';
4
3
 
5
- export function writeTraces(projectDir, url, traces, coverage = null, warnings = [], artifactPaths = null) {
4
+ /**
5
+ * @typedef {Object} WriteTracesResult
6
+ * @property {number} version
7
+ * @property {string} observedAt
8
+ * @property {string} url
9
+ * @property {Array} traces
10
+ * @property {Array} [observedExpectations]
11
+ * @property {Object} [coverage]
12
+ * @property {Array} [warnings]
13
+ * @property {Object} [silences] - Added by writeTraces if silenceTracker provided
14
+ * @property {string} tracesPath
15
+ * @property {Object} observeTruth
16
+ * @property {Object} [expectationExecution] - Added by caller after writeTraces
17
+ * @property {Array} [expectationCoverageGaps] - Added by caller after writeTraces
18
+ * @property {Object} [incremental] - Added by caller after writeTraces
19
+ */
20
+
21
+ /**
22
+ * SILENCE TRACKING: Write observation traces with explicit silence tracking.
23
+ * All gaps, skips, caps, and unknowns must be recorded and surfaced.
24
+ *
25
+ * PHASE 5: Writes to deterministic artifact path .verax/runs/<runId>/traces.json
26
+ *
27
+ * @param {string} projectDir - Project directory
28
+ * @param {string} url - URL observed
29
+ * @param {Array} traces - Execution traces
30
+ * @param {Object} [coverage] - Coverage data (if capped, this is a silence)
31
+ * @param {Array} [warnings] - Warnings (caps are silences)
32
+ * @param {Array} [observedExpectations] - Observed expectations
33
+ * @param {Object} [silenceTracker] - Silence tracker (optional)
34
+ * @param {string} [runId] - Run identifier (Phase 5) - required but optional in signature for type compatibility
35
+ * @returns {WriteTracesResult}
36
+ */
37
+ export function writeTraces(projectDir, url, traces, coverage = null, warnings = [], observedExpectations = [], silenceTracker = null, runId = null) {
38
+ if (!runId) {
39
+ throw new Error('runId is required');
40
+ }
41
+ const observeDir = getRunArtifactDir(projectDir, runId);
42
+ const tracesPath = getArtifactPath(projectDir, runId, 'traces.json');
43
+ mkdirSync(observeDir, { recursive: true });
44
+
6
45
  const observation = {
7
46
  version: 1,
8
47
  observedAt: new Date().toISOString(),
@@ -10,6 +49,10 @@ export function writeTraces(projectDir, url, traces, coverage = null, warnings =
10
49
  traces: traces
11
50
  };
12
51
 
52
+ if (observedExpectations && observedExpectations.length > 0) {
53
+ observation.observedExpectations = observedExpectations;
54
+ }
55
+
13
56
  if (coverage) {
14
57
  observation.coverage = coverage;
15
58
  }
@@ -17,23 +60,7 @@ export function writeTraces(projectDir, url, traces, coverage = null, warnings =
17
60
  observation.warnings = warnings;
18
61
  }
19
62
 
20
- let tracesPath;
21
- if (artifactPaths) {
22
- // Use new artifact structure
23
- // Write JSONL format (one trace per line) for artifact structure
24
- traces.forEach(trace => appendTrace(artifactPaths, trace));
25
-
26
- // Also write full observation JSON for detect() compatibility
27
- const jsonPath = resolve(artifactPaths.evidence, 'observation-traces.json');
28
- writeFileSync(jsonPath, JSON.stringify(observation, null, 2) + '\n');
29
- tracesPath = jsonPath;
30
- } else {
31
- // Legacy structure
32
- const observeDir = resolve(projectDir, '.veraxverax', 'observe');
33
- mkdirSync(observeDir, { recursive: true });
34
- tracesPath = resolve(observeDir, 'observation-traces.json');
35
- writeFileSync(tracesPath, JSON.stringify(observation, null, 2) + '\n');
36
- }
63
+ writeFileSync(tracesPath, JSON.stringify(observation, null, 2) + '\n');
37
64
 
38
65
  let externalNavigationBlockedCount = 0;
39
66
  let timeoutsCount = 0;
@@ -63,9 +90,29 @@ export function writeTraces(projectDir, url, traces, coverage = null, warnings =
63
90
 
64
91
  if (coverage) {
65
92
  observeTruth.coverage = coverage;
93
+ // SILENCE TRACKING: Track budget exceeded as silence (cap = unevaluated interactions)
66
94
  if (coverage.capped) {
95
+ observeTruth.budgetExceeded = true;
67
96
  if (!warnings || warnings.length === 0) {
68
- warnings = [{ code: 'INTERACTIONS_CAPPED', message: 'Interaction discovery reached the cap (30). Scan coverage is incomplete.' }];
97
+ warnings = [{ code: 'INTERACTIONS_CAPPED', message: `Interaction discovery reached the cap (${coverage.cap}). Scan coverage is incomplete.` }];
98
+ }
99
+
100
+ // Record budget cap as silence
101
+ if (silenceTracker) {
102
+ const unevaluatedCount = (coverage.candidatesDiscovered || 0) - (coverage.candidatesSelected || 0);
103
+ silenceTracker.record({
104
+ scope: 'interaction',
105
+ reason: 'interaction_limit_exceeded',
106
+ description: `Budget cap reached: ${unevaluatedCount} interactions not evaluated`,
107
+ context: {
108
+ cap: coverage.cap,
109
+ discovered: coverage.candidatesDiscovered,
110
+ evaluated: coverage.candidatesSelected,
111
+ unevaluated: unevaluatedCount
112
+ },
113
+ impact: 'affects_expectations',
114
+ count: unevaluatedCount
115
+ });
69
116
  }
70
117
  }
71
118
  }
@@ -73,6 +120,11 @@ export function writeTraces(projectDir, url, traces, coverage = null, warnings =
73
120
  observeTruth.warnings = warnings;
74
121
  }
75
122
 
123
+ // SILENCE TRACKING: Attach silence entries to observation for detect phase
124
+ if (silenceTracker && silenceTracker.entries.length > 0) {
125
+ observation.silences = silenceTracker.export();
126
+ }
127
+
76
128
  return {
77
129
  ...observation,
78
130
  tracesPath: tracesPath,
@@ -17,6 +17,7 @@ export class UISignalSensor {
17
17
  hasErrorSignal: false,
18
18
  hasStatusSignal: false,
19
19
  hasLiveRegion: false,
20
+ validationFeedbackDetected: false,
20
21
  disabledElements: [],
21
22
  explanation: []
22
23
  };
@@ -36,22 +37,28 @@ export class UISignalSensor {
36
37
  result.explanation.push('Found [data-loading]');
37
38
  }
38
39
 
39
- // role=status or role=alert with aria-live
40
- const statusRegions = document.querySelectorAll('[role="status"], [role="alert"]');
41
- if (statusRegions.length > 0) {
40
+ // role=status or role=alert with aria-live (visible only)
41
+ const statusRegions = Array.from(document.querySelectorAll('[role="status"], [role="alert"]'));
42
+ const visibleStatusRegions = statusRegions.filter((el) => {
43
+ const style = window.getComputedStyle(el);
44
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
45
+ return el.offsetParent !== null && style.visibility !== 'hidden' && style.display !== 'none' && style.opacity !== '0';
46
+ });
47
+ if (visibleStatusRegions.length > 0) {
42
48
  result.hasStatusSignal = true;
43
- result.explanation.push(`Found ${statusRegions.length} status/alert region(s)`);
49
+ result.explanation.push(`Found ${visibleStatusRegions.length} visible status/alert region(s)`);
44
50
  }
45
51
 
46
- // aria-live region
47
- const liveRegions = document.querySelectorAll('[aria-live]');
48
- if (liveRegions.length > 0) {
52
+ // aria-live region (legacy check - will be checked again for visibility below)
53
+ const allLiveRegions = document.querySelectorAll('[aria-live]');
54
+ if (allLiveRegions.length > 0) {
49
55
  result.hasLiveRegion = true;
50
- result.explanation.push(`Found ${liveRegions.length} aria-live region(s)`);
56
+ result.explanation.push(`Found ${allLiveRegions.length} aria-live region(s)`);
51
57
  }
52
58
 
53
59
  // Check for dialogs
54
60
  const dialog = document.querySelector('[role="dialog"], [aria-modal="true"]');
61
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
55
62
  if (dialog && dialog.offsetParent !== null) {
56
63
  // offsetParent is null if element is hidden
57
64
  result.hasDialog = true;
@@ -78,8 +85,93 @@ export class UISignalSensor {
78
85
  );
79
86
  }
80
87
 
81
- // Check for error signals: aria-invalid visible elements
82
- const invalidElements = document.querySelectorAll('[aria-invalid="true"]');
88
+ // VALIDATION INTELLIGENCE v1: Detect visible validation feedback
89
+ // Check for aria-invalid="true" with visible error text nearby
90
+ const invalidElements = Array.from(document.querySelectorAll('[aria-invalid="true"]'));
91
+ let hasVisibleValidationError = false;
92
+
93
+ for (const invalidEl of invalidElements) {
94
+ const style = window.getComputedStyle(invalidEl);
95
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
96
+ const isVisible = invalidEl.offsetParent !== null &&
97
+ style.visibility !== 'hidden' &&
98
+ style.display !== 'none' &&
99
+ style.opacity !== '0';
100
+
101
+ if (isVisible) {
102
+ // Check for visible error text near this input
103
+ // Look in parent, next sibling, or aria-describedby target
104
+ const describedBy = invalidEl.getAttribute('aria-describedby');
105
+ if (describedBy) {
106
+ const errorTarget = document.getElementById(describedBy);
107
+ if (errorTarget) {
108
+ const targetStyle = window.getComputedStyle(errorTarget);
109
+ const targetVisible = errorTarget.offsetParent !== null &&
110
+ targetStyle.visibility !== 'hidden' &&
111
+ targetStyle.display !== 'none' &&
112
+ targetStyle.opacity !== '0';
113
+ if (targetVisible && errorTarget.textContent.trim().length > 0) {
114
+ hasVisibleValidationError = true;
115
+ break;
116
+ }
117
+ }
118
+ }
119
+
120
+ // Check parent for error text
121
+ const parent = invalidEl.parentElement;
122
+ if (parent) {
123
+ const errorText = Array.from(parent.querySelectorAll('[role="alert"], .error, .invalid-feedback'))
124
+ .find(el => {
125
+ const elStyle = window.getComputedStyle(el);
126
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
127
+ return el.offsetParent !== null &&
128
+ elStyle.visibility !== 'hidden' &&
129
+ elStyle.display !== 'none' &&
130
+ elStyle.opacity !== '0' &&
131
+ el.textContent.trim().length > 0;
132
+ });
133
+ if (errorText) {
134
+ hasVisibleValidationError = true;
135
+ break;
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ // Check for visible role="alert" or role="status" regions
142
+ const alertRegions = Array.from(document.querySelectorAll('[role="alert"], [role="status"]'));
143
+ const visibleAlertRegions = alertRegions.filter((el) => {
144
+ const style = window.getComputedStyle(el);
145
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
146
+ const isVisible = el.offsetParent !== null &&
147
+ style.visibility !== 'hidden' &&
148
+ style.display !== 'none' &&
149
+ style.opacity !== '0';
150
+ return isVisible && el.textContent.trim().length > 0;
151
+ });
152
+
153
+ // Check for visible aria-live regions with content
154
+ const liveRegions = Array.from(document.querySelectorAll('[aria-live]'));
155
+ const visibleLiveRegions = liveRegions.filter((el) => {
156
+ const style = window.getComputedStyle(el);
157
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
158
+ const isVisible = el.offsetParent !== null &&
159
+ style.visibility !== 'hidden' &&
160
+ style.display !== 'none' &&
161
+ style.opacity !== '0';
162
+ return isVisible && el.textContent.trim().length > 0;
163
+ });
164
+
165
+ // VALIDATION INTELLIGENCE v1: Set validationFeedbackDetected
166
+ result.validationFeedbackDetected = hasVisibleValidationError ||
167
+ visibleAlertRegions.length > 0 ||
168
+ visibleLiveRegions.length > 0;
169
+
170
+ if (result.validationFeedbackDetected) {
171
+ result.explanation.push('Visible validation feedback detected');
172
+ }
173
+
174
+ // Legacy: Check for error signals
83
175
  if (invalidElements.length > 0) {
84
176
  result.hasErrorSignal = true;
85
177
  result.explanation.push(`Found ${invalidElements.length} invalid element(s)`);
@@ -90,6 +182,7 @@ export class UISignalSensor {
90
182
  '[role="alert"], [class*="error"], [class*="danger"]'
91
183
  );
92
184
  if (errorMessages.length > 0) {
185
+ // @ts-expect-error - NodeListOf is iterable in browser context
93
186
  for (const elem of errorMessages) {
94
187
  const text = elem.textContent.trim().slice(0, 50);
95
188
  if (text && (text.toLowerCase().includes('error') || text.toLowerCase().includes('fail'))) {
@@ -111,16 +204,30 @@ export class UISignalSensor {
111
204
  * Returns: { changed: boolean, explanation: string[], summary: {...} }
112
205
  */
113
206
  diff(before, after) {
207
+ const defaults = {
208
+ hasLoadingIndicator: false,
209
+ hasDialog: false,
210
+ hasErrorSignal: false,
211
+ hasStatusSignal: false,
212
+ hasLiveRegion: false,
213
+ disabledElements: [],
214
+ validationFeedbackDetected: false
215
+ };
216
+
217
+ const safeBefore = { ...defaults, ...(before || {}) };
218
+ const safeAfter = { ...defaults, ...(after || {}) };
219
+
114
220
  const result = {
115
221
  changed: false,
116
222
  explanation: [],
117
223
  summary: {
118
- loadingStateChanged: before.hasLoadingIndicator !== after.hasLoadingIndicator,
119
- dialogStateChanged: before.hasDialog !== after.hasDialog,
120
- errorSignalChanged: before.hasErrorSignal !== after.hasErrorSignal,
121
- statusSignalChanged: before.hasStatusSignal !== after.hasStatusSignal,
122
- liveRegionStateChanged: before.hasLiveRegion !== after.hasLiveRegion,
123
- disabledButtonsChanged: before.disabledElements.length !== after.disabledElements.length
224
+ loadingStateChanged: safeBefore.hasLoadingIndicator !== safeAfter.hasLoadingIndicator,
225
+ dialogStateChanged: safeBefore.hasDialog !== safeAfter.hasDialog,
226
+ errorSignalChanged: safeBefore.hasErrorSignal !== safeAfter.hasErrorSignal,
227
+ statusSignalChanged: safeBefore.hasStatusSignal !== safeAfter.hasStatusSignal,
228
+ liveRegionStateChanged: safeBefore.hasLiveRegion !== safeAfter.hasLiveRegion,
229
+ disabledButtonsChanged: safeBefore.disabledElements.length !== safeAfter.disabledElements.length,
230
+ validationFeedbackChanged: safeBefore.validationFeedbackDetected !== safeAfter.validationFeedbackDetected // VALIDATION INTELLIGENCE v1
124
231
  }
125
232
  };
126
233
 
@@ -159,6 +266,14 @@ export class UISignalSensor {
159
266
  `Live region: ${before.hasLiveRegion} → ${after.hasLiveRegion}`
160
267
  );
161
268
  }
269
+
270
+ // Also check if status signal content changed (text added to role=status)
271
+ if (!result.changed && before.hasStatusSignal && after.hasStatusSignal) {
272
+ // Both have status signals, but content might have changed
273
+ // This is a conservative check - if status signal exists and is visible, consider it feedback
274
+ result.changed = true;
275
+ result.explanation.push('Status signal content changed');
276
+ }
162
277
 
163
278
  if (result.summary.disabledButtonsChanged) {
164
279
  result.changed = true;
@@ -167,7 +282,18 @@ export class UISignalSensor {
167
282
  );
168
283
  }
169
284
 
170
- return result;
285
+ // VALIDATION INTELLIGENCE v1: Check for validation feedback changes
286
+ if (result.summary.validationFeedbackChanged) {
287
+ result.changed = true;
288
+ result.explanation.push(
289
+ `Validation feedback: ${before.validationFeedbackDetected} → ${after.validationFeedbackDetected}`
290
+ );
291
+ }
292
+
293
+ return {
294
+ ...result,
295
+ explanation: result.explanation.join(' | ')
296
+ };
171
297
  }
172
298
 
173
299
  /**
@@ -1,17 +1,93 @@
1
1
  import { resolve } from 'path';
2
- import { writeFileSync, mkdirSync } from 'fs';
2
+ import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs';
3
+ import { computeExpectationsSummary } from './shared/artifact-manager.js';
4
+ import { createImpactSummary } from './core/silence-impact.js';
5
+ import { computeDecisionSnapshot } from './core/decision-snapshot.js';
3
6
 
4
- export function writeScanSummary(projectDir, url, projectType, learnTruth, observeTruth, detectTruth, manifestPath, tracesPath, findingsPath, artifactPaths = null) {
7
+ export function writeScanSummary(projectDir, url, projectType, learnTruth, observeTruth, detectTruth, manifestPath, tracesPath, findingsPath, runDirOpt, findingsArray = null) {
8
+ if (!runDirOpt) {
9
+ throw new Error('runDirOpt is required');
10
+ }
11
+ const scanDir = resolve(runDirOpt);
12
+ mkdirSync(scanDir, { recursive: true });
13
+
14
+ // Compute expectations summary from manifest
15
+ let expectationsSummary = { total: 0, navigation: 0, networkActions: 0, stateActions: 0 };
16
+ if (manifestPath && existsSync(manifestPath)) {
17
+ try {
18
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
19
+ expectationsSummary = computeExpectationsSummary(manifest);
20
+ } catch (error) {
21
+ // Ignore errors reading manifest
22
+ }
23
+ }
24
+
25
+ // PHASE 4: Compute silence impact summary
26
+ let silenceImpactSummary = null;
27
+ if (detectTruth?.silences?.entries) {
28
+ silenceImpactSummary = createImpactSummary(detectTruth.silences.entries);
29
+ }
30
+
31
+ // PHASE 6: Compute determinism summary from decisions.json
32
+ let determinismSummary = null;
33
+ if (runDirOpt && observeTruth?.runId) {
34
+ const decisionsPath = resolve(runDirOpt, 'decisions.json');
35
+ if (existsSync(decisionsPath)) {
36
+ try {
37
+ const decisions = JSON.parse(readFileSync(decisionsPath, 'utf-8'));
38
+ const { DecisionRecorder } = require('./core/determinism-model.js');
39
+ const recorder = DecisionRecorder.fromExport(decisions);
40
+ const summary = recorder.getSummary();
41
+
42
+ determinismSummary = {
43
+ isDeterministic: summary.isDeterministic,
44
+ totalDecisions: summary.totalDecisions,
45
+ decisionsByCategory: summary.decisionsByCategory,
46
+ decisionsPath: decisionsPath
47
+ };
48
+ } catch (error) {
49
+ // Ignore errors reading decisions
50
+ }
51
+ }
52
+ }
53
+
54
+ // PHASE 7: Compute decision snapshot (answers 6 mandatory questions)
55
+ let decisionSnapshot = null;
56
+ if (findingsArray && detectTruth && observeTruth) {
57
+ const silences = detectTruth.silences;
58
+ decisionSnapshot = computeDecisionSnapshot(findingsArray, detectTruth, observeTruth, silences);
59
+ }
60
+
5
61
  const summary = {
6
62
  version: 1,
7
63
  scannedAt: new Date().toISOString(),
8
64
  url: url,
9
65
  projectType: projectType,
66
+ expectationsSummary: expectationsSummary,
67
+ // PHASE 7: Decision snapshot first (most important for human decision-making)
68
+ decisionSnapshot: decisionSnapshot,
69
+ // PHASE 7: Misinterpretation guards (explicit warnings)
70
+ interpretationGuards: {
71
+ zeroFindings: 'Zero findings does NOT mean no problems. Check unverified count and confidence level.',
72
+ deterministicRun: 'Deterministic run does NOT mean correct site. Only means scan was reproducible.',
73
+ highSilenceImpact: 'High silence impact does NOT mean failures exist. Only means unknowns affect confidence.'
74
+ },
10
75
  truth: {
11
76
  learn: learnTruth,
12
77
  observe: observeTruth,
13
78
  detect: detectTruth
14
79
  },
80
+ // PHASE 4: Add silence lifecycle and impact summary
81
+ silenceLifecycle: detectTruth?.silences ? {
82
+ total: detectTruth.silences.total || 0,
83
+ byType: detectTruth.silences.summary?.byType || {},
84
+ byEvaluationStatus: detectTruth.silences.summary?.byEvaluationStatus || {},
85
+ byOutcome: detectTruth.silences.summary?.byOutcome || {},
86
+ withPromiseAssociation: detectTruth.silences.summary?.withPromiseAssociation || 0,
87
+ impactSummary: silenceImpactSummary
88
+ } : null,
89
+ // PHASE 6: Add determinism summary
90
+ determinism: determinismSummary,
15
91
  paths: {
16
92
  manifest: manifestPath,
17
93
  traces: tracesPath,
@@ -19,19 +95,8 @@ export function writeScanSummary(projectDir, url, projectType, learnTruth, obser
19
95
  }
20
96
  };
21
97
 
22
- let summaryPath;
23
- if (artifactPaths) {
24
- // Use new artifact structure
25
- summaryPath = artifactPaths.summary;
26
- // Write the scan summary with truth data
27
- writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + '\n');
28
- } else {
29
- // Legacy structure
30
- const scanDir = resolve(projectDir, '.veraxverax', 'scan');
31
- mkdirSync(scanDir, { recursive: true });
32
- summaryPath = resolve(scanDir, 'scan-summary.json');
33
- writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + '\n');
34
- }
98
+ const summaryPath = resolve(scanDir, 'scan-summary.json');
99
+ writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + '\n');
35
100
 
36
101
  return {
37
102
  ...summary,
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { existsSync, mkdirSync, writeFileSync, appendFileSync } from 'fs';
16
- import { resolve, join } from 'path';
16
+ import { resolve } from 'path';
17
17
  import { randomBytes } from 'crypto';
18
18
 
19
19
  /**
@@ -39,6 +39,7 @@ export function initArtifactPaths(projectRoot, runId = null) {
39
39
  runDir,
40
40
  summary: resolve(runDir, 'summary.json'),
41
41
  findings: resolve(runDir, 'findings.json'),
42
+ expectations: resolve(runDir, 'expectations.json'),
42
43
  traces: resolve(runDir, 'traces.jsonl'),
43
44
  evidence: resolve(runDir, 'evidence'),
44
45
  flows: resolve(runDir, 'flows'),
@@ -55,23 +56,118 @@ export function initArtifactPaths(projectRoot, runId = null) {
55
56
  return paths;
56
57
  }
57
58
 
59
+ /**
60
+ * Compute expectations summary from manifest
61
+ * @param {Object} manifest - Manifest object
62
+ * @returns {Object} Expectations summary
63
+ */
64
+ export function computeExpectationsSummary(manifest) {
65
+ const staticExpectations = manifest.staticExpectations || [];
66
+ const spaExpectations = manifest.spaExpectations || [];
67
+ const actionContracts = manifest.actionContracts || [];
68
+ const proven = (item) => item && item.proof === 'PROVEN_EXPECTATION';
69
+
70
+ // Count by type
71
+ let navigation = 0;
72
+ let networkActions = 0;
73
+ let stateActions = 0;
74
+
75
+ // Static expectations: navigation, form_submission, network_action, state_action
76
+ for (const exp of staticExpectations) {
77
+ if (!proven(exp)) continue;
78
+ if (exp.type === 'navigation' || exp.type === 'spa_navigation') {
79
+ navigation++;
80
+ } else if (exp.type === 'form_submission') {
81
+ networkActions++; // Form submissions are network actions
82
+ } else if (exp.type === 'network_action') {
83
+ networkActions++;
84
+ } else if (exp.type === 'state_action') {
85
+ stateActions++;
86
+ }
87
+ }
88
+
89
+ // SPA expectations: navigation
90
+ for (const exp of spaExpectations) {
91
+ if (!proven(exp)) continue;
92
+ if (exp.type === 'navigation') {
93
+ navigation++;
94
+ }
95
+ }
96
+
97
+ // Action contracts: network and state actions
98
+ for (const contract of actionContracts) {
99
+ if (!proven(contract)) continue;
100
+ if (contract.kind === 'NETWORK_ACTION' || contract.kind === 'network' || contract.kind === 'fetch' || contract.kind === 'axios') {
101
+ networkActions++;
102
+ } else if (contract.kind === 'STATE_ACTION' || contract.kind === 'state' || contract.kind === 'redux' || contract.kind === 'zustand') {
103
+ stateActions++;
104
+ }
105
+ }
106
+
107
+ const total = navigation + networkActions + stateActions;
108
+
109
+ return {
110
+ total,
111
+ navigation,
112
+ networkActions,
113
+ stateActions
114
+ };
115
+ }
116
+
58
117
  /**
59
118
  * Write summary.json with metadata and metrics.
60
119
  * @param {Object} paths - Artifact paths from initArtifactPaths
61
- * @param {Object} summary - Summary data { url, duration, findings, metrics }
120
+ * @param {Object} summary - Summary data { url, duration, findings, metrics, manifest, contextCheck }
62
121
  */
63
122
  export function writeSummary(paths, summary) {
123
+ const expectationsSummary = summary.manifest
124
+ ? computeExpectationsSummary(summary.manifest)
125
+ : { total: 0, navigation: 0, networkActions: 0, stateActions: 0 };
126
+
127
+ const contextCheck = summary.contextCheck || {
128
+ ran: false,
129
+ forced: false,
130
+ matchedRoutesCount: 0,
131
+ matchedLinksCount: 0,
132
+ sampleMatched: []
133
+ };
134
+
135
+ const metrics = summary.metrics || {
136
+ learnMs: 0,
137
+ validateMs: 0,
138
+ observeMs: 0,
139
+ detectMs: 0,
140
+ totalMs: 0
141
+ };
142
+
143
+ const safety = summary.safety || {
144
+ publicUrlConfirmed: false,
145
+ usedYesFlag: false
146
+ };
147
+
64
148
  const data = {
65
149
  runId: paths.runId,
66
150
  timestamp: new Date().toISOString(),
67
151
  url: summary.url,
68
152
  projectRoot: summary.projectRoot,
69
- metrics: summary.metrics || {
70
- parseMs: 0,
71
- resolveMs: 0,
72
- observeMs: 0,
73
- detectMs: 0,
74
- totalMs: 0
153
+ expectationsSummary: expectationsSummary,
154
+ contextCheck: {
155
+ ran: contextCheck.ran,
156
+ forced: contextCheck.forced || false,
157
+ matchedRoutesCount: contextCheck.matchedRoutesCount || 0,
158
+ matchedLinksCount: contextCheck.matchedLinksCount || 0,
159
+ sampleMatched: (contextCheck.sampleMatched || []).slice(0, 5)
160
+ },
161
+ safety: {
162
+ publicUrlConfirmed: safety.publicUrlConfirmed || false,
163
+ usedYesFlag: safety.usedYesFlag || false
164
+ },
165
+ metrics: {
166
+ learnMs: metrics.learnMs || 0,
167
+ validateMs: metrics.validateMs || 0,
168
+ observeMs: metrics.observeMs || 0,
169
+ detectMs: metrics.detectMs || 0,
170
+ totalMs: metrics.totalMs || 0
75
171
  },
76
172
  findingsCounts: summary.findingsCounts || {
77
173
  HIGH: 0,
@@ -80,7 +176,13 @@ export function writeSummary(paths, summary) {
80
176
  UNKNOWN: 0
81
177
  },
82
178
  topFindings: summary.topFindings || [],
83
- cacheStats: summary.cacheStats || {}
179
+ cacheStats: summary.cacheStats || {},
180
+ progressStats: summary.progressStats || null,
181
+ interactionStats: summary.interactionStats || null,
182
+ expectationUsageStats: summary.expectationUsageStats || null,
183
+ runOverview: summary.runOverview || null,
184
+ coverage: summary.coverage || null,
185
+ coverageGaps: summary.coverageGaps || []
84
186
  };
85
187
 
86
188
  writeFileSync(paths.summary, JSON.stringify(data, null, 2) + '\n');