@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
@@ -0,0 +1,561 @@
1
+ /**
2
+ * OBSERVATION ENGINE
3
+ *
4
+ * Produces an observation summary from scan results.
5
+ *
6
+ * VERAX is an Outcome Observer - it does NOT judge, validate, or decide.
7
+ * It observes what code promises, what users do, and what actually happens.
8
+ * It reports observations, discrepancies, gaps, and unknowns - nothing more.
9
+ *
10
+ * NO VERDICT. NO JUDGMENT. NO SAFETY CLAIM. NO GO/NO-GO DECISIONS.
11
+ *
12
+ * PHASE 2: All observations include canonical outcome classifications.
13
+ * PHASE 3: All observations include Promise awareness - what promise was being evaluated.
14
+ * PHASE 4: All observations include Silence lifecycle - type, trigger, evaluation status, confidence impact.
15
+ */
16
+
17
+ import { buildEvidenceIndex } from './evidence-index.js';
18
+ import { CANONICAL_OUTCOMES } from '../core/canonical-outcomes.js';
19
+ import { SILENCE_TYPES, EVALUATION_STATUS } from '../core/silence-model.js';
20
+ import { inferPromiseFromInteraction } from '../core/promise-model.js';
21
+ import { createImpactSummary } from '../core/silence-impact.js';
22
+
23
+ /**
24
+ * Compute observation summary from scan findings and analysis.
25
+ *
26
+ * SILENCE TRACKING: All gaps, timeouts, skips, sensor failures are explicit.
27
+ * Nothing unobserved is allowed to disappear.
28
+ *
29
+ * @param {Array} findings - Array of finding objects (observed discrepancies)
30
+ * @param {Object} observeTruth - Coverage data (what was observed)
31
+ * @param {Object} learnTruth - Learned route data
32
+ * @param {Array} coverageGaps - Expectations/interactions not evaluated
33
+ * @param {Boolean} budgetExceeded - Whether budget was exceeded during scan
34
+ * @param {Object} detectTruth - Detection truth (includes silence data)
35
+ * @returns {Object} ObservationSummary with findings, gaps, unknowns, coverage facts, silences
36
+ */
37
+ export function computeObservationSummary(findings, observeTruth, learnTruth, coverageGaps, budgetExceeded, detectTruth = null, projectDir = null, silenceTracker = null) {
38
+ const isBudgetExceeded = budgetExceeded !== undefined ? budgetExceeded : (observeTruth?.budgetExceeded === true);
39
+ const traces = Array.isArray(observeTruth?.traces) ? observeTruth.traces : [];
40
+ const evidenceBuild = buildEvidenceIndex(traces, projectDir, silenceTracker);
41
+
42
+ // Extract coverage facts
43
+ const coverage = observeTruth?.coverage || {};
44
+ const pagesEvaluated = coverage.pagesVisited || 0;
45
+ const pagesDiscovered = coverage.pagesDiscovered || 0;
46
+ const interactionsEvaluated = coverage.interactionsExecuted || coverage.candidatesSelected || observeTruth?.interactionsObserved || 0;
47
+ const interactionsDiscovered = coverage.interactionsDiscovered || coverage.candidatesDiscovered || 0;
48
+ const expectationTotal = learnTruth?.expectationsDiscovered || 0;
49
+ const coverageGapsCount = coverageGaps?.length || 0;
50
+ const expectationsEvaluated = Math.max(0, expectationTotal - coverageGapsCount);
51
+
52
+ // Count unproven results (interactions without PROVEN expectations)
53
+ const unprovenTraces = traces.filter(t =>
54
+ t.unprovenResult === true || t.resultType === 'UNPROVEN_RESULT'
55
+ );
56
+ const skippedCount = coverage.skippedInteractions || 0;
57
+
58
+ // Count findings by confidence (for transparency, not judgment)
59
+ const findingsByConfidence = {
60
+ HIGH: 0,
61
+ MEDIUM: 0,
62
+ LOW: 0,
63
+ UNKNOWN: 0
64
+ };
65
+ const findingsByType = {};
66
+ const findingsByOutcome = {}; // PHASE 2: Added outcome tracking
67
+ const findingsByPromise = {}; // PHASE 3: Added promise tracking
68
+
69
+ for (const finding of (findings || [])) {
70
+ const confidence = finding.confidence?.level || 'UNKNOWN';
71
+ const type = finding.type || 'unknown';
72
+ const outcome = finding.outcome || CANONICAL_OUTCOMES.SILENT_FAILURE; // Default for legacy findings
73
+ const promiseType = finding.promise?.type || 'UNKNOWN_PROMISE'; // PHASE 3
74
+
75
+ if (Object.prototype.hasOwnProperty.call(findingsByConfidence, confidence)) {
76
+ findingsByConfidence[confidence]++;
77
+ }
78
+ findingsByType[type] = (findingsByType[type] || 0) + 1;
79
+ findingsByOutcome[outcome] = (findingsByOutcome[outcome] || 0) + 1; // PHASE 2
80
+ findingsByPromise[promiseType] = (findingsByPromise[promiseType] || 0) + 1; // PHASE 3
81
+ }
82
+
83
+ // Calculate ratios (factual, not judgmental)
84
+ const pageRatio = pagesDiscovered > 0 ? clampRatio(pagesEvaluated / pagesDiscovered) : (pagesDiscovered === 0 ? null : 0);
85
+ const interactionRatio = interactionsDiscovered > 0 ? clampRatio(interactionsEvaluated / interactionsDiscovered) : (interactionsDiscovered === 0 ? null : 0);
86
+ const expectationRatio = expectationTotal > 0 ? clampRatio(expectationsEvaluated / expectationTotal) : (expectationTotal === 0 ? null : 0);
87
+
88
+ // Identify gaps explicitly
89
+ const gaps = {
90
+ pages: pagesDiscovered > pagesEvaluated ? pagesDiscovered - pagesEvaluated : 0,
91
+ interactions: interactionsDiscovered > interactionsEvaluated ? interactionsDiscovered - interactionsEvaluated : 0,
92
+ expectations: coverageGapsCount,
93
+ skippedInteractions: skippedCount,
94
+ unprovenResults: unprovenTraces.length
95
+ };
96
+
97
+ // Build gap details
98
+ const gapDetails = [];
99
+ if (isBudgetExceeded) {
100
+ gapDetails.push({
101
+ outcome: CANONICAL_OUTCOMES.COVERAGE_GAP, // PHASE 2
102
+ type: 'budget_exceeded',
103
+ message: `Budget limit reached: ${pagesEvaluated} ${pagesEvaluated}/${pagesDiscovered} pages and ${interactionsEvaluated}/${interactionsDiscovered} interactions evaluated - observation incomplete`,
104
+ pagesAffected: pagesDiscovered - pagesEvaluated,
105
+ interactionsAffected: interactionsDiscovered - interactionsEvaluated
106
+ });
107
+ }
108
+ if (gaps.pages > 0) {
109
+ gapDetails.push({
110
+ outcome: CANONICAL_OUTCOMES.COVERAGE_GAP, // PHASE 2
111
+ type: 'pages_not_evaluated',
112
+ message: `${gaps.pages} page(s) discovered but not visited - observations for these pages are unavailable`,
113
+ count: gaps.pages
114
+ });
115
+ }
116
+ if (gaps.interactions > 0) {
117
+ gapDetails.push({
118
+ outcome: CANONICAL_OUTCOMES.COVERAGE_GAP, // PHASE 2
119
+ type: 'interactions_not_evaluated',
120
+ message: `${gaps.interactions} interaction(s) discovered but not executed - behavior of these interactions is unknown`,
121
+ count: gaps.interactions
122
+ });
123
+ }
124
+ if (gaps.expectations > 0) {
125
+ gapDetails.push({
126
+ outcome: CANONICAL_OUTCOMES.COVERAGE_GAP, // PHASE 2
127
+ type: 'expectations_not_evaluated',
128
+ message: `${gaps.expectations} expectation(s) defined but not evaluated - cannot determine if code matches reality for these`,
129
+ count: gaps.expectations,
130
+ details: coverageGaps.slice(0, 10) // Limit detail list
131
+ });
132
+ }
133
+ if (gaps.skippedInteractions > 0) {
134
+ gapDetails.push({
135
+ outcome: CANONICAL_OUTCOMES.UNPROVEN_INTERACTION, // PHASE 2: Executed but outcome not asserted
136
+ type: 'interactions_skipped',
137
+ message: `${gaps.skippedInteractions} interaction(s) executed but outcomes not evaluated (safety policy, ambiguous state, or technical limitations)`,
138
+ count: gaps.skippedInteractions
139
+ });
140
+ }
141
+
142
+ // Build observation summary
143
+ const summary = {
144
+ toolStatus: 'completed',
145
+ observations: {
146
+ discrepanciesObserved: findings?.length || 0,
147
+ discrepanciesByType: findingsByType,
148
+ discrepanciesByConfidence: findingsByConfidence,
149
+ discrepanciesByOutcome: findingsByOutcome, // PHASE 2: Canonical outcomes
150
+ discrepanciesByPromise: findingsByPromise, // PHASE 3: Promise types
151
+ findings: findings || []
152
+ },
153
+ coverage: {
154
+ pagesEvaluated,
155
+ pagesDiscovered,
156
+ pageRatio: pageRatio !== null ? pageRatio : undefined,
157
+ interactionsEvaluated,
158
+ interactionsDiscovered,
159
+ interactionRatio: interactionRatio !== null ? interactionRatio : undefined,
160
+ expectationsEvaluated,
161
+ expectationsDiscovered: expectationTotal,
162
+ expectationRatio: expectationRatio !== null ? expectationRatio : undefined
163
+ },
164
+ gaps: {
165
+ total: Object.values(gaps).reduce((a, b) => a + b, 0),
166
+ pages: gaps.pages,
167
+ interactions: gaps.interactions + gaps.skippedInteractions,
168
+ expectations: gaps.expectations,
169
+ unprovenResults: gaps.unprovenResults,
170
+ details: gapDetails
171
+ },
172
+ // SILENCE TRACKING: Attach all silence data for explicit reporting
173
+ silences: detectTruth?.silences || null,
174
+ // PHASE 4: Add silence impact accounting
175
+ silenceImpactSummary: detectTruth?.silences?.entries ?
176
+ createImpactSummary(detectTruth.silences.entries) :
177
+ null,
178
+ evidenceIndex: evidenceBuild.evidenceIndex,
179
+ observedAt: new Date().toISOString()
180
+ };
181
+
182
+ return summary;
183
+ }
184
+
185
+ /**
186
+ * Format observation summary for console output.
187
+ *
188
+ * PHASE 2: Includes canonical outcome classifications
189
+ *
190
+ * SILENCE PHILOSOPHY:
191
+ * - Gaps/unknowns ALWAYS shown, even if zero (no silent success)
192
+ * - Timeouts, caps, skips, sensor failures explicitly reported
193
+ * - Zero counts are explicit: "(No gaps)" not hidden
194
+ * - Nothing unobserved is allowed to disappear
195
+ *
196
+ * Observational, not judgmental. Reports facts: what was observed, gaps, unknowns.
197
+ *
198
+ * @param {Object} observationSummary - Observation summary object
199
+ * @returns {string} Formatted observation report
200
+ */
201
+ export function formatObservationSummary(observationSummary) {
202
+ const lines = [];
203
+
204
+ lines.push('\n═══════════════════════════════════════');
205
+ lines.push('OBSERVATION REPORT');
206
+ lines.push('═══════════════════════════════════════');
207
+
208
+ // Tool status (factual only)
209
+ lines.push('');
210
+ lines.push(`Tool Status: ${observationSummary.toolStatus || 'completed'}`);
211
+ lines.push(`(Indicates tool execution status, not site quality or safety)`);
212
+
213
+ // What was observed
214
+ lines.push('');
215
+ lines.push('DISCREPANCIES OBSERVED:');
216
+ const obs = observationSummary.observations || {};
217
+ lines.push(` Count: ${obs.discrepanciesObserved || 0}`);
218
+
219
+ if (obs.discrepanciesObserved > 0) {
220
+ lines.push(' Types observed:');
221
+ for (const [type, count] of Object.entries(obs.discrepanciesByType || {})) {
222
+ lines.push(` - ${type}: ${count}`);
223
+ }
224
+
225
+ // PHASE 2: Show outcomes
226
+ if (obs.discrepanciesByOutcome && Object.keys(obs.discrepanciesByOutcome).length > 0) {
227
+ lines.push(' By outcome classification:');
228
+ for (const [outcome, count] of Object.entries(obs.discrepanciesByOutcome)) {
229
+ if (count > 0) {
230
+ lines.push(` - ${outcome}: ${count}`);
231
+ }
232
+ }
233
+ }
234
+
235
+ // PHASE 3: Show promises
236
+ if (obs.discrepanciesByPromise && Object.keys(obs.discrepanciesByPromise).length > 0) {
237
+ lines.push(' By promise type:');
238
+ for (const [promise, count] of Object.entries(obs.discrepanciesByPromise)) {
239
+ if (count > 0) {
240
+ const promiseLabel = promise.replace(/_PROMISE$/, '').replace(/_/g, ' ');
241
+ lines.push(` - ${promiseLabel}: ${count}`);
242
+ }
243
+ }
244
+ }
245
+
246
+ lines.push(' By confidence level:');
247
+ const conf = obs.discrepanciesByConfidence || {};
248
+ if (conf.HIGH > 0) lines.push(` - HIGH: ${conf.HIGH}`);
249
+ if (conf.MEDIUM > 0) lines.push(` - MEDIUM: ${conf.MEDIUM}`);
250
+ if (conf.LOW > 0) lines.push(` - LOW: ${conf.LOW}`);
251
+ if (conf.UNKNOWN > 0) lines.push(` - UNKNOWN: ${conf.UNKNOWN}`);
252
+ } else {
253
+ lines.push(' No discrepancies observed between code promises and runtime behavior');
254
+ }
255
+
256
+ // Coverage facts
257
+ lines.push('');
258
+ lines.push('WHAT WAS EVALUATED:');
259
+ const cov = observationSummary.coverage || {};
260
+ lines.push(` Pages: ${cov.pagesEvaluated || 0} of ${cov.pagesDiscovered || 0} discovered${cov.pageRatio !== undefined ? ` (${(cov.pageRatio * 100).toFixed(1)}% evaluated)` : ''}`);
261
+ lines.push(` Interactions: ${cov.interactionsEvaluated || 0} of ${cov.interactionsDiscovered || 0} discovered${cov.interactionRatio !== undefined ? ` (${(cov.interactionRatio * 100).toFixed(1)}% evaluated)` : ''}`);
262
+ lines.push(` Expectations: ${cov.expectationsEvaluated || 0} of ${cov.expectationsDiscovered || 0} discovered${cov.expectationRatio !== undefined ? ` (${(cov.expectationRatio * 100).toFixed(1)}% evaluated)` : ''}`);
263
+
264
+ // Gaps explicitly reported (always shown, even if zero)
265
+ const gaps = observationSummary.gaps || {};
266
+ lines.push('');
267
+ lines.push('EVALUATION GAPS (NOT evaluated - observations incomplete for these items):');
268
+ lines.push(` Pages: ${gaps.pages || 0} not evaluated`);
269
+ lines.push(` Interactions: ${gaps.interactions || 0} not evaluated`);
270
+ lines.push(` Expectations: ${gaps.expectations || 0} not evaluated`);
271
+ if (gaps.unprovenResults > 0) {
272
+ lines.push(` Interactions without PROVEN expectations: ${gaps.unprovenResults}`);
273
+ }
274
+
275
+ if (gaps.details && gaps.details.length > 0) {
276
+ lines.push(' Gap reasons:');
277
+ for (const gap of gaps.details.slice(0, 5)) {
278
+ lines.push(` - ${gap.message}`);
279
+ }
280
+ } else if (gaps.total === 0) {
281
+ lines.push(' (No gaps reported - all discovered items were evaluated)');
282
+ }
283
+
284
+ // SILENCE TRACKING: Explicitly show all silences (timeouts, skips, sensor failures, caps)
285
+ // PHASE 4: Include lifecycle information (type, status, promise association, confidence impact)
286
+ const silences = observationSummary.silences;
287
+ if (silences && silences.totalSilences > 0) {
288
+ lines.push('');
289
+ lines.push('UNKNOWNS (Silences - things attempted but outcome unknown):');
290
+ lines.push(` Total silence events: ${silences.totalSilences}`);
291
+
292
+ // PHASE 2: Show outcomes in silence
293
+ if (silences.summary && silences.summary.byOutcome && Object.keys(silences.summary.byOutcome).length > 0) {
294
+ lines.push(' By outcome classification:');
295
+ for (const [outcome, count] of Object.entries(silences.summary.byOutcome)) {
296
+ if (count > 0) {
297
+ lines.push(` - ${outcome}: ${count}`);
298
+ }
299
+ }
300
+ }
301
+
302
+ // PHASE 4: Show silence lifecycle metrics
303
+ if (silences.summary && silences.summary.byType && Object.keys(silences.summary.byType).length > 0) {
304
+ lines.push(' By silence type:');
305
+ const types = Object.entries(silences.summary.byType)
306
+ .filter(([_, count]) => count > 0)
307
+ .sort((a, b) => b[1] - a[1])
308
+ .slice(0, 5);
309
+ for (const [type, count] of types) {
310
+ const typeLabel = type.replace(/_/g, ' ').toLowerCase();
311
+ lines.push(` - ${typeLabel}: ${count}`);
312
+ }
313
+ }
314
+
315
+ // PHASE 4: Show evaluation status distribution
316
+ if (silences.summary && silences.summary.byEvaluationStatus && Object.keys(silences.summary.byEvaluationStatus).length > 0) {
317
+ lines.push(' By evaluation status:');
318
+ for (const [status, count] of Object.entries(silences.summary.byEvaluationStatus)) {
319
+ if (count > 0) {
320
+ lines.push(` - ${status}: ${count}`);
321
+ }
322
+ }
323
+ }
324
+
325
+ // PHASE 4: Show promise association count
326
+ if (silences.summary && silences.summary.withPromiseAssociation) {
327
+ lines.push(` Silences with promise association: ${silences.summary.withPromiseAssociation}`);
328
+ }
329
+
330
+ // PHASE 4: Show confidence impact
331
+ if (silences.summary && silences.summary.confidenceImpact) {
332
+ const impact = silences.summary.confidenceImpact;
333
+ if (impact.coverage !== 0 || impact.promise_verification !== 0 || impact.overall !== 0) {
334
+ lines.push(' Confidence impact:');
335
+ if (impact.coverage !== 0) lines.push(` - Coverage confidence: ${impact.coverage > 0 ? '+' : ''}${impact.coverage}%`);
336
+ if (impact.promise_verification !== 0) lines.push(` - Promise verification confidence: ${impact.promise_verification > 0 ? '+' : ''}${impact.promise_verification}%`);
337
+ if (impact.overall !== 0) lines.push(` - Overall confidence: ${impact.overall > 0 ? '+' : ''}${impact.overall}%`);
338
+ }
339
+ }
340
+
341
+
342
+ if (silences.byCategory) {
343
+ lines.push(' By category:');
344
+ for (const [category, count] of Object.entries(silences.byCategory)) {
345
+ if (count > 0) {
346
+ lines.push(` - ${category}: ${count}`);
347
+ }
348
+ }
349
+ }
350
+
351
+ if (silences.byReason) {
352
+ lines.push(' By reason:');
353
+ const sortedReasons = Object.entries(silences.byReason)
354
+ .filter(([_, count]) => count > 0)
355
+ .sort((a, b) => b[1] - a[1])
356
+ .slice(0, 5);
357
+ for (const [reason, count] of sortedReasons) {
358
+ lines.push(` - ${reason.replace(/_/g, ' ')}: ${count}`);
359
+ }
360
+ }
361
+ } else {
362
+ lines.push('');
363
+ lines.push('UNKNOWNS (Silences):');
364
+ lines.push(' No silence events recorded (all attempted actions completed)');
365
+ }
366
+
367
+ // PHASE 4: Show silence impact summary
368
+ const impactSummary = observationSummary.silenceImpactSummary;
369
+ if (impactSummary && impactSummary.total_silences > 0) {
370
+ lines.push('');
371
+ lines.push('SILENCE IMPACT ON CONFIDENCE:');
372
+ const impact = impactSummary.aggregated_impact;
373
+ if (impact) {
374
+ lines.push(` Aggregated impact: ${impactSummary.confidence_interpretation}`);
375
+ lines.push(` Coverage confidence: ${impact.coverage > 0 ? '+' : ''}${impact.coverage}%`);
376
+ lines.push(` Promise verification confidence: ${impact.promise_verification > 0 ? '+' : ''}${impact.promise_verification}%`);
377
+ lines.push(` Overall observation confidence: ${impact.overall > 0 ? '+' : ''}${impact.overall}%`);
378
+ }
379
+
380
+ if (impactSummary.by_severity && Object.values(impactSummary.by_severity).some(v => v > 0)) {
381
+ lines.push(' Silences by severity:');
382
+ const sev = impactSummary.by_severity;
383
+ if (sev.critical > 0) lines.push(` - CRITICAL: ${sev.critical} events`);
384
+ if (sev.high > 0) lines.push(` - HIGH: ${sev.high} events`);
385
+ if (sev.medium > 0) lines.push(` - MEDIUM: ${sev.medium} events`);
386
+ if (sev.low > 0) lines.push(` - LOW: ${sev.low} events`);
387
+ }
388
+
389
+ if (impactSummary.most_impactful_types && impactSummary.most_impactful_types.length > 0) {
390
+ lines.push(' Most impactful silence types:');
391
+ for (const impactType of impactSummary.most_impactful_types.slice(0, 3)) {
392
+ lines.push(` - ${impactType.type.replace(/_/g, ' ')}: ${impactType.count} events, avg impact ${impactType.average_impact}%`);
393
+ }
394
+ }
395
+ }
396
+
397
+ if (obs.discrepanciesObserved > 0 && obs.findings && obs.findings.length > 0) {
398
+ lines.push('');
399
+ lines.push('DISCREPANCIES OBSERVED (sample):');
400
+ for (const finding of obs.findings.slice(0, 3)) {
401
+ const outcome = finding.outcome ? ` [${finding.outcome}]` : '';
402
+ const promiseInfo = finding.promise ? ` (${finding.promise.type.replace(/_PROMISE$/, '')})` : '';
403
+ const _confStr = finding.confidence?.level ? ` (${finding.confidence.level} confidence)` : '';
404
+ const userStmt = finding.what_happened ? `User: ${finding.what_happened}` : '';
405
+ lines.push(` • ${finding.type}${outcome}${promiseInfo}`);
406
+ if (userStmt) lines.push(` ${userStmt}`);
407
+ if (finding.what_was_expected) lines.push(` Expected: ${finding.what_was_expected}`);
408
+ if (finding.what_was_observed) lines.push(` Observed: ${finding.what_was_observed}`);
409
+ if (finding.promise?.expected_signal) lines.push(` Promise signal: ${finding.promise.expected_signal}`);
410
+ }
411
+ }
412
+
413
+ lines.push('');
414
+ lines.push('═══════════════════════════════════════');
415
+ lines.push('');
416
+
417
+ return lines.join('\n');
418
+ }
419
+
420
+
421
+ /**
422
+ * Build evidence index from traces array.
423
+ * Maps expectations and interactions to evidence (screenshots, trace files).
424
+ */
425
+ /**
426
+ * Build evidence index from traces array.
427
+ *
428
+ * PHASE 3: EVIDENCE INTEGRITY
429
+ * - Validates that evidence files actually exist
430
+ * - Missing evidence files are tracked as silence
431
+ * - Only includes verifiable evidence in index
432
+ *
433
+ * Maps expectations and interactions to evidence (screenshots, trace files).
434
+ */
435
+ // buildEvidenceIndex moved to evidence-index.js - imported above
436
+
437
+ /**
438
+ * PHASE 4: Associate silences with promises where applicable
439
+ *
440
+ * RULE: A silence can only be associated with a promise if we can infer what promise
441
+ * the user was attempting to verify when the silence occurred.
442
+ *
443
+ * Conservative approach:
444
+ * - Navigation timeouts → NAVIGATION_PROMISE
445
+ * - Interaction timeouts → infer from interaction type
446
+ * - Safety blocks → related promise
447
+ * - Budget/discovery failures → no promise (unevaluated)
448
+ *
449
+ * @param {Object} silence - SilenceEntry with silence_type, scope, context
450
+ * @returns {Object|null} Promise object with type/expected_signal, or null if cannot infer
451
+ */
452
+ export function inferPromiseForSilence(silence) {
453
+ if (!silence) return null;
454
+
455
+ const { silence_type, scope: _scope, reason, context } = silence;
456
+
457
+ // Navigation-related silences
458
+ if (silence_type === SILENCE_TYPES.NAVIGATION_TIMEOUT ||
459
+ silence_type === SILENCE_TYPES.PROMISE_VERIFICATION_BLOCKED ||
460
+ (reason && reason.includes('navigation'))) {
461
+ return {
462
+ type: 'NAVIGATION_PROMISE',
463
+ expected_signal: 'URL change or navigation settled',
464
+ reason_no_association: null
465
+ };
466
+ }
467
+
468
+ // Interaction-related silences
469
+ if (silence_type === SILENCE_TYPES.INTERACTION_TIMEOUT) {
470
+ // Try to infer from context if available
471
+ if (context && context.interaction) {
472
+ return inferPromiseFromInteraction(context.interaction);
473
+ }
474
+ return {
475
+ type: 'FEEDBACK_PROMISE',
476
+ expected_signal: 'User feedback or interaction acknowledgment',
477
+ reason_no_association: 'Interaction type unknown in context'
478
+ };
479
+ }
480
+
481
+ // Safety blocks - the promise being blocked
482
+ if (silence_type === SILENCE_TYPES.SAFETY_POLICY_BLOCK) {
483
+ if (context && context.interaction) {
484
+ const inferred = inferPromiseFromInteraction(context.interaction);
485
+ return {
486
+ ...inferred,
487
+ blocked_by_safety: true
488
+ };
489
+ }
490
+ return null; // Cannot infer without interaction context
491
+ }
492
+
493
+ // Discovery/sensor failures - no promise can be evaluated
494
+ if (silence_type === SILENCE_TYPES.DISCOVERY_FAILURE ||
495
+ silence_type === SILENCE_TYPES.SENSOR_FAILURE) {
496
+ return {
497
+ type: null,
498
+ reason_no_association: 'Observation infrastructure failure - no promise evaluatable'
499
+ };
500
+ }
501
+
502
+ // Budget/incremental/ambiguous - no promise
503
+ if (silence_type === SILENCE_TYPES.BUDGET_LIMIT_EXCEEDED ||
504
+ silence_type === SILENCE_TYPES.INCREMENTAL_REUSE ||
505
+ silence_type === SILENCE_TYPES.PROMISE_NOT_EVALUATED) {
506
+ return {
507
+ type: null,
508
+ reason_no_association: 'Promise not yet evaluated'
509
+ };
510
+ }
511
+
512
+ // Conservative default: no association
513
+ return null;
514
+ }
515
+
516
+ /**
517
+ * Validate that a silence event makes forensic sense
518
+ * RULE: Silence can NEVER appear as success. It is always a gap in observation.
519
+ *
520
+ * @param {Object} silence - SilenceEntry
521
+ * @returns {Object} Validation result: { valid: boolean, reason: string }
522
+ */
523
+ export function validateSilenceIntegrity(silence) {
524
+ if (!silence) {
525
+ return { valid: false, reason: 'Silence entry is null/undefined' };
526
+ }
527
+
528
+ // Silence can NEVER have outcome === INFORMATIONAL or any "success" outcome
529
+ const prohibitedOutcomes = ['SUCCESS', 'PASS', 'VERIFIED', 'CONFIRMED'];
530
+ if (prohibitedOutcomes.includes(silence.outcome?.toUpperCase())) {
531
+ return {
532
+ valid: false,
533
+ reason: `Silence cannot have outcome "${silence.outcome}" - silence is always a gap`
534
+ };
535
+ }
536
+
537
+ // Silence must have a valid scope
538
+ const validScopes = ['page', 'interaction', 'expectation', 'sensor', 'navigation', 'settle'];
539
+ if (!silence.scope || !validScopes.includes(silence.scope)) {
540
+ return { valid: false, reason: `Invalid scope: "${silence.scope}"` };
541
+ }
542
+
543
+ // Must have evaluation_status (Phase 4)
544
+ const validStatuses = Object.values(EVALUATION_STATUS);
545
+ if (!silence.evaluation_status || !validStatuses.includes(silence.evaluation_status)) {
546
+ return {
547
+ valid: false,
548
+ reason: `Invalid evaluation_status: "${silence.evaluation_status}". Must be one of: ${validStatuses.join(', ')}`
549
+ };
550
+ }
551
+
552
+ return { valid: true, reason: null };
553
+ }
554
+
555
+ function clampRatio(ratio) {
556
+ const clamped = Math.max(0, Math.min(1, ratio));
557
+ return Math.round(clamped * 10000) / 10000; // 4 decimal places
558
+ }
559
+
560
+ // writeEvidenceIndex moved to evidence-index.js - re-exported below
561
+ export { buildEvidenceIndex, writeEvidenceIndex } from './evidence-index.js';
@@ -0,0 +1,61 @@
1
+ import { resolve, dirname } from 'path';
2
+ import { mkdirSync, writeFileSync } from 'fs';
3
+
4
+ function resolveScreenshotPath(screenshotsDir, relativePath) {
5
+ if (!relativePath || !screenshotsDir) return null;
6
+ const observeDir = dirname(screenshotsDir);
7
+ return resolve(observeDir, relativePath);
8
+ }
9
+
10
+ function findEvidenceForFinding(finding, evidenceIndex) {
11
+ if (!Array.isArray(evidenceIndex)) return null;
12
+ if (finding.expectationId) {
13
+ const byExpectation = evidenceIndex.find(e => e.expectationId === finding.expectationId);
14
+ if (byExpectation) return byExpectation;
15
+ }
16
+ const selector = finding.interaction?.selector;
17
+ if (selector) {
18
+ const bySelector = evidenceIndex.find(e => e.interaction?.selector === selector);
19
+ if (bySelector) return bySelector;
20
+ }
21
+ return evidenceIndex[0] || null;
22
+ }
23
+
24
+ function buildEvidenceEntries(findings, evidenceIndex, tracesPath, screenshotsDir) {
25
+ const seen = new Set();
26
+ const entries = [];
27
+ (findings || []).forEach((finding, idx) => {
28
+ const evidence = findEvidenceForFinding(finding, evidenceIndex);
29
+ const findingId = finding.findingId || finding.id || `finding-${idx}`;
30
+ if (seen.has(findingId)) return;
31
+ seen.add(findingId);
32
+ entries.push({
33
+ findingId,
34
+ findingType: finding.type || 'finding',
35
+ expectationId: finding.expectationId || null,
36
+ interactionSelector: finding.interaction?.selector || null,
37
+ evidenceId: evidence?.id || null,
38
+ paths: {
39
+ beforeScreenshot: resolveScreenshotPath(screenshotsDir, evidence?.evidence?.beforeScreenshot || null),
40
+ afterScreenshot: resolveScreenshotPath(screenshotsDir, evidence?.evidence?.afterScreenshot || null),
41
+ traceFile: tracesPath || null,
42
+ networkTrace: null
43
+ }
44
+ });
45
+ });
46
+ return entries;
47
+ }
48
+
49
+ export function writeEvidenceIndex(projectDir, findings, verdict, tracesPath, screenshotsDir) {
50
+ const artifactsDir = resolve(projectDir, 'artifacts');
51
+ mkdirSync(artifactsDir, { recursive: true });
52
+ const items = buildEvidenceEntries(findings, verdict?.evidenceIndex || [], tracesPath, screenshotsDir);
53
+ const evidenceIndexPath = resolve(artifactsDir, 'evidence-index.json');
54
+ const payload = {
55
+ version: 1,
56
+ tracesPath,
57
+ items
58
+ };
59
+ writeFileSync(evidenceIndexPath, JSON.stringify(payload, null, 2) + '\n');
60
+ return { evidenceIndexPath, items };
61
+ }
@@ -63,7 +63,7 @@ export async function executeFlow(page, spec, sensors = {}) {
63
63
  };
64
64
  }
65
65
 
66
- async function executeStep(page, step, idx, spec, sensors, secretValues) {
66
+ async function executeStep(page, step, idx, spec, _sensors, _secretValues) {
67
67
  const baseResult = {
68
68
  stepIndex: idx,
69
69
  type: step.type,
@@ -131,7 +131,8 @@ async function stepGoto(page, step, result, spec) {
131
131
  return result;
132
132
  }
133
133
 
134
- await page.goto(url, { waitUntil: 'networkidle' });
134
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
135
+ await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
135
136
  await waitForSettle(page, { timeoutMs: 3000, settleMs: 500 });
136
137
 
137
138
  result.success = true;
@@ -111,10 +111,9 @@ export function validateFlowSpec(spec) {
111
111
  * Returns actual value or throws if not found.
112
112
  *
113
113
  * @param {string} value - String containing $ENV:VARNAME references
114
- * @param {Object} secrets - Map of secret keys to env var names
115
114
  * @returns {string} - Resolved value
116
115
  */
117
- export function resolveSecrets(value, secrets = {}) {
116
+ export function resolveSecrets(value, _secrets = {}) {
118
117
  if (typeof value !== 'string') return value;
119
118
 
120
119
  // Replace $ENV:VARNAME with actual env var value