@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.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +24 -36
- package/src/cli/commands/default.js +681 -0
- package/src/cli/commands/doctor.js +197 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +586 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +297 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +110 -0
- package/src/cli/util/expectation-extractor.js +388 -0
- package/src/cli/util/findings-writer.js +32 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +412 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +30 -0
- package/src/cli/util/project-discovery.js +297 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +43 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +111 -0
- package/src/verax/cli/wizard.js +109 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +432 -0
- package/src/verax/core/incremental-store.js +245 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +523 -0
- package/src/verax/detect/comparison.js +7 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +127 -0
- package/src/verax/detect/expectation-model.js +241 -168
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +41 -12
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +200 -288
- package/src/verax/detect/interactive-findings.js +612 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +561 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +103 -15
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +642 -0
- package/src/verax/intel/vue-router-extractor.js +325 -0
- package/src/verax/learn/action-contract-extractor.js +338 -104
- package/src/verax/learn/ast-contract-extractor.js +148 -6
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +122 -58
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +28 -97
- package/src/verax/learn/route-validator.js +8 -7
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +119 -10
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +30 -6
- package/src/verax/observe/console-sensor.js +2 -18
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +513 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +660 -273
- package/src/verax/observe/index.js +910 -26
- package/src/verax/observe/interaction-discovery.js +378 -15
- package/src/verax/observe/interaction-runner.js +562 -197
- package/src/verax/observe/loading-sensor.js +145 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +38 -17
- package/src/verax/observe/state-sensor.js +393 -0
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +73 -21
- package/src/verax/observe/ui-signal-sensor.js +143 -17
- package/src/verax/scan-summary-writer.js +80 -15
- package/src/verax/shared/artifact-manager.js +111 -9
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +169 -0
- package/src/verax/shared/dynamic-route-utils.js +224 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +9 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +66 -0
- 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,
|
|
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: '
|
|
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,
|
|
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
|