@odavl/guardian 2.0.0 → 2.0.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/CHANGELOG.md +210 -210
- package/LICENSE +21 -21
- package/README.md +297 -184
- package/bin/guardian.js +2242 -2221
- package/config/README.md +59 -59
- package/config/guardian.config.json +54 -54
- package/config/guardian.policy.json +12 -12
- package/config/profiles/docs.yaml +18 -18
- package/config/profiles/ecommerce.yaml +17 -17
- package/config/profiles/landing-demo.yaml +16 -16
- package/config/profiles/marketing.yaml +18 -18
- package/config/profiles/saas.yaml +21 -21
- package/flows/example-login-flow.json +36 -36
- package/flows/example-signup-flow.json +44 -44
- package/package.json +124 -116
- package/policies/enterprise.json +12 -12
- package/policies/landing-demo.json +22 -22
- package/policies/saas.json +12 -12
- package/policies/startup.json +12 -12
- package/src/enterprise/audit-logger.js +166 -166
- package/src/enterprise/pdf-exporter.js +267 -267
- package/src/enterprise/rbac-gate.js +142 -142
- package/src/enterprise/rbac.js +239 -239
- package/src/enterprise/site-manager.js +180 -180
- package/src/founder/feedback-system.js +156 -156
- package/src/founder/founder-tracker.js +213 -213
- package/src/founder/usage-signals.js +141 -141
- package/src/guardian/action-hints.js +439 -439
- package/src/guardian/alert-ledger.js +121 -121
- package/src/guardian/artifact-sanitizer.js +56 -56
- package/src/guardian/attempt-engine.js +1069 -1029
- package/src/guardian/attempt-registry.js +267 -267
- package/src/guardian/attempt-relevance.js +106 -106
- package/src/guardian/attempt-reporter.js +513 -507
- package/src/guardian/attempt.js +274 -273
- package/src/guardian/attempts-filter.js +63 -63
- package/src/guardian/auto-attempt-builder.js +283 -283
- package/src/guardian/baseline-registry.js +177 -177
- package/src/guardian/baseline-reporter.js +143 -143
- package/src/guardian/baseline-storage.js +285 -285
- package/src/guardian/baseline.js +535 -534
- package/src/guardian/behavioral-signals.js +261 -261
- package/src/guardian/breakage-intelligence.js +224 -224
- package/src/guardian/browser-pool.js +131 -131
- package/src/guardian/browser.js +119 -119
- package/src/guardian/canonical-truth.js +308 -308
- package/src/guardian/ci-cli.js +121 -121
- package/src/guardian/ci-gate.js +96 -96
- package/src/guardian/ci-mode.js +15 -15
- package/src/guardian/ci-output.js +55 -38
- package/src/guardian/cli-summary.js +102 -102
- package/src/guardian/confidence-signals.js +251 -251
- package/src/guardian/config-loader.js +161 -161
- package/src/guardian/config-validator.js +285 -283
- package/src/guardian/coverage-model.js +239 -239
- package/src/guardian/coverage-packs.js +58 -58
- package/src/guardian/crawler.js +142 -142
- package/src/guardian/data-guardian-detector.js +189 -189
- package/src/guardian/decision-authority.js +746 -725
- package/src/guardian/detection-layers.js +271 -271
- package/src/guardian/determinism.js +146 -146
- package/src/guardian/discovery-engine.js +661 -661
- package/src/guardian/drift-detector.js +100 -100
- package/src/guardian/enhanced-html-reporter.js +522 -522
- package/src/guardian/env-guard.js +128 -127
- package/src/guardian/error-clarity.js +399 -399
- package/src/guardian/export-contract.js +196 -196
- package/src/guardian/fail-safe.js +212 -212
- package/src/guardian/failure-intelligence.js +173 -173
- package/src/guardian/failure-taxonomy.js +169 -169
- package/src/guardian/final-outcome.js +206 -206
- package/src/guardian/first-run-profile.js +89 -89
- package/src/guardian/first-run.js +65 -67
- package/src/guardian/flag-validator.js +111 -111
- package/src/guardian/flow-executor.js +641 -639
- package/src/guardian/flow-registry.js +67 -67
- package/src/guardian/honesty.js +394 -394
- package/src/guardian/html-reporter.js +416 -416
- package/src/guardian/human-intent-resolver.js +296 -296
- package/src/guardian/human-interaction-model.js +351 -351
- package/src/guardian/human-journey-context.js +184 -184
- package/src/guardian/human-navigator.js +544 -544
- package/src/guardian/human-reporter.js +435 -431
- package/src/guardian/index.js +226 -221
- package/src/guardian/init-command.js +143 -143
- package/src/guardian/intent-detector.js +148 -146
- package/src/guardian/journey-definitions.js +132 -132
- package/src/guardian/journey-scan-cli.js +142 -145
- package/src/guardian/journey-scanner.js +583 -583
- package/src/guardian/junit-reporter.js +281 -281
- package/src/guardian/language-detection.js +99 -99
- package/src/guardian/live-alert.js +56 -56
- package/src/guardian/live-baseline-compare.js +146 -146
- package/src/guardian/live-cli.js +95 -95
- package/src/guardian/live-guardian.js +210 -210
- package/src/guardian/live-scheduler-runner.js +137 -137
- package/src/guardian/live-scheduler-state.js +167 -168
- package/src/guardian/live-scheduler.js +146 -146
- package/src/guardian/live-state.js +110 -110
- package/src/guardian/market-criticality.js +335 -335
- package/src/guardian/market-reporter.js +577 -577
- package/src/guardian/network-trace.js +178 -178
- package/src/guardian/obs-logger.js +110 -110
- package/src/guardian/observed-capabilities.js +427 -427
- package/src/guardian/output-contract.js +154 -0
- package/src/guardian/output-readability.js +264 -264
- package/src/guardian/parallel-executor.js +116 -116
- package/src/guardian/path-safety.js +56 -56
- package/src/guardian/pattern-analyzer.js +348 -348
- package/src/guardian/policy.js +432 -434
- package/src/guardian/prelaunch-gate.js +193 -193
- package/src/guardian/prerequisite-checker.js +101 -101
- package/src/guardian/preset-loader.js +152 -157
- package/src/guardian/profile-loader.js +96 -96
- package/src/guardian/reality.js +3025 -2826
- package/src/guardian/realworld-scenarios.js +94 -94
- package/src/guardian/reporter.js +167 -167
- package/src/guardian/retry-policy.js +123 -123
- package/src/guardian/root-cause-analysis.js +171 -171
- package/src/guardian/rules-engine.js +558 -558
- package/src/guardian/run-artifacts.js +212 -212
- package/src/guardian/run-cleanup.js +207 -207
- package/src/guardian/run-export.js +522 -522
- package/src/guardian/run-latest.js +90 -90
- package/src/guardian/run-list.js +211 -211
- package/src/guardian/run-summary.js +20 -20
- package/src/guardian/runtime-root.js +246 -246
- package/src/guardian/safety.js +248 -248
- package/src/guardian/scan-presets.js +133 -149
- package/src/guardian/screenshot.js +152 -152
- package/src/guardian/secret-hygiene.js +44 -44
- package/src/guardian/selector-fallbacks.js +394 -394
- package/src/guardian/semantic-contact-detection.js +255 -255
- package/src/guardian/semantic-contact-finder.js +201 -201
- package/src/guardian/semantic-targets.js +234 -234
- package/src/guardian/site-intelligence.js +588 -588
- package/src/guardian/site-introspection.js +257 -257
- package/src/guardian/sitemap.js +225 -225
- package/src/guardian/smoke.js +283 -258
- package/src/guardian/snapshot-schema.js +177 -290
- package/src/guardian/snapshot.js +430 -397
- package/src/guardian/stability-scorer.js +169 -169
- package/src/guardian/success-evaluator.js +214 -214
- package/src/guardian/template-command.js +184 -184
- package/src/guardian/text-formatters.js +426 -426
- package/src/guardian/timeout-profiles.js +57 -57
- package/src/guardian/truth/attempt.contract.js +158 -0
- package/src/guardian/truth/decision.contract.js +275 -0
- package/src/guardian/truth/snapshot.contract.js +363 -0
- package/src/guardian/validators.js +323 -323
- package/src/guardian/verdict-card.js +474 -474
- package/src/guardian/verdict-clarity.js +298 -298
- package/src/guardian/verdict-policy.js +363 -363
- package/src/guardian/verdict.js +333 -333
- package/src/guardian/verdicts.js +79 -74
- package/src/guardian/visual-diff.js +247 -247
- package/src/guardian/wait-for-outcome.js +119 -119
- package/src/guardian/watch-runner.js +181 -181
- package/src/guardian/watchdog-diff.js +167 -167
- package/src/guardian/webhook.js +206 -206
- package/src/payments/stripe-checkout.js +169 -169
- package/src/plans/plan-definitions.js +148 -148
- package/src/plans/plan-manager.js +211 -211
- package/src/plans/usage-tracker.js +210 -210
- package/src/recipes/recipe-engine.js +188 -188
- package/src/recipes/recipe-failure-analysis.js +159 -159
- package/src/recipes/recipe-registry.js +134 -134
- package/src/recipes/recipe-runtime.js +507 -507
- package/src/recipes/recipe-store.js +410 -410
- package/SECURITY.md +0 -77
- package/VERSIONING.md +0 -100
- package/guardian-contract-v1.md +0 -502
|
@@ -1,308 +1,308 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
|
|
3
|
-
function safeReadJson(filePath) {
|
|
4
|
-
try {
|
|
5
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
6
|
-
} catch (_) {
|
|
7
|
-
return null;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function deriveAttemptSummary(attempts = []) {
|
|
12
|
-
const summary = {
|
|
13
|
-
total: 0,
|
|
14
|
-
executed: 0,
|
|
15
|
-
skipped: 0,
|
|
16
|
-
notApplicable: 0,
|
|
17
|
-
failed: 0,
|
|
18
|
-
friction: 0,
|
|
19
|
-
success: 0
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
for (const attempt of attempts) {
|
|
23
|
-
if (!attempt) continue;
|
|
24
|
-
const outcome = (attempt.outcome || '').toUpperCase();
|
|
25
|
-
summary.total += 1;
|
|
26
|
-
|
|
27
|
-
const executedFlag =
|
|
28
|
-
attempt.executed === true ||
|
|
29
|
-
(attempt.executed === undefined && ['SUCCESS', 'FAILURE', 'FRICTION'].includes(outcome));
|
|
30
|
-
|
|
31
|
-
if (executedFlag) {
|
|
32
|
-
summary.executed += 1;
|
|
33
|
-
if (outcome === 'SUCCESS') summary.success += 1;
|
|
34
|
-
if (outcome === 'FAILURE') summary.failed += 1;
|
|
35
|
-
if (outcome === 'FRICTION') summary.friction += 1;
|
|
36
|
-
} else if (outcome === 'SKIPPED') {
|
|
37
|
-
summary.skipped += 1;
|
|
38
|
-
} else if (outcome === 'NOT_APPLICABLE' || outcome === 'DISCOVERY_FAILED') {
|
|
39
|
-
summary.notApplicable += 1;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return summary;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function deriveCoveragePercent(snapshotCoverage = {}, attemptSummary = null) {
|
|
47
|
-
const counts = snapshotCoverage?.counts || snapshotCoverage || {};
|
|
48
|
-
const attemptBased = attemptSummary && attemptSummary.total > 0
|
|
49
|
-
? Math.max(0, Math.min(100, Math.round((attemptSummary.executed / attemptSummary.total) * 100)))
|
|
50
|
-
: null;
|
|
51
|
-
|
|
52
|
-
if (counts.percent !== null) {
|
|
53
|
-
// Override obviously-wrong zero when we have executed attempts and a computed percent
|
|
54
|
-
if (counts.percent === 0 && attemptBased !== null && attemptSummary.executed > 0) {
|
|
55
|
-
return attemptBased;
|
|
56
|
-
}
|
|
57
|
-
return counts.percent;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (attemptBased !== null) {
|
|
61
|
-
return attemptBased;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const executed = counts.executedCount ?? attemptSummary?.executed ?? 0;
|
|
65
|
-
const enabled = counts.enabledPlannedCount ?? attemptSummary?.total ?? 0;
|
|
66
|
-
const excluded = counts.excludedNotApplicableFromTotal ?? counts.skippedNotApplicable ?? attemptSummary?.notApplicable ?? 0;
|
|
67
|
-
const userFiltered = counts.skippedUserFiltered ?? attemptSummary?.skipped ?? 0;
|
|
68
|
-
|
|
69
|
-
const denominator = Math.max(enabled - excluded - userFiltered, 0);
|
|
70
|
-
if (denominator === 0) return 0;
|
|
71
|
-
return Math.max(0, Math.min(100, Math.round((executed / denominator) * 100)));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function deriveUntested(snapshotCoverage = {}, attempts = []) {
|
|
75
|
-
const details = [];
|
|
76
|
-
const addDetail = (items, label) => {
|
|
77
|
-
(items || []).forEach(item => {
|
|
78
|
-
if (item?.attempt) {
|
|
79
|
-
details.push(`${label}:${item.attempt}${item.reason ? ` (${item.reason})` : ''}`);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
addDetail(snapshotCoverage.skippedNotApplicable, 'n/a');
|
|
85
|
-
addDetail(snapshotCoverage.skippedUserFiltered, 'skipped');
|
|
86
|
-
addDetail(snapshotCoverage.skippedDisabledByPreset, 'disabled');
|
|
87
|
-
addDetail(snapshotCoverage.skippedMissing, 'missing');
|
|
88
|
-
|
|
89
|
-
if (details.length === 0 && attempts.length > 0) {
|
|
90
|
-
attempts.filter(a => !a.executed && a.outcome === 'SKIPPED').forEach(a => details.push(`skipped:${a.attemptId}`));
|
|
91
|
-
attempts.filter(a => a.outcome === 'NOT_APPLICABLE').forEach(a => details.push(`n/a:${a.attemptId}`));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return details;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function deriveFromSnapshot(snapshot) {
|
|
98
|
-
if (!snapshot) return null;
|
|
99
|
-
const verdict = snapshot.verdict?.verdict || snapshot.meta?.result || null;
|
|
100
|
-
if (!verdict) return null;
|
|
101
|
-
|
|
102
|
-
const attempts = snapshot.attempts || [];
|
|
103
|
-
const attemptSummary = deriveAttemptSummary(attempts);
|
|
104
|
-
const coveragePercent = deriveCoveragePercent(snapshot.meta?.coverage || snapshot.coverage || {}, attemptSummary);
|
|
105
|
-
const evidenceCompleteness = snapshot.meta?.evidenceMetrics?.completeness ?? snapshot.evidenceMetrics?.completeness ?? null;
|
|
106
|
-
const confidenceScore = snapshot.verdict?.confidence?.score ?? null;
|
|
107
|
-
const confidenceBasis = snapshot.verdict?.confidence?.basis ?? '';
|
|
108
|
-
const exitCode = snapshot.meta?.exitCode ?? null;
|
|
109
|
-
const untested = deriveUntested(snapshot.meta?.coverage || snapshot.coverage || {}, attempts);
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
source: 'snapshot',
|
|
113
|
-
finalVerdict: verdict,
|
|
114
|
-
exitCode,
|
|
115
|
-
coveragePercent,
|
|
116
|
-
confidenceScore,
|
|
117
|
-
confidenceBasis,
|
|
118
|
-
evidenceCompleteness,
|
|
119
|
-
attemptSummary,
|
|
120
|
-
untested
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function deriveFromDecision(decision) {
|
|
125
|
-
if (!decision) return null;
|
|
126
|
-
const verdict = decision.finalVerdict || null;
|
|
127
|
-
if (!verdict) return null;
|
|
128
|
-
const attempts = Array.isArray(decision.outcomes?.attempts) ? decision.outcomes.attempts : [];
|
|
129
|
-
const attemptSummary = deriveAttemptSummary(attempts);
|
|
130
|
-
const coverageCounts = decision.coverage || {};
|
|
131
|
-
const coveragePercent = deriveCoveragePercent(coverageCounts, attemptSummary);
|
|
132
|
-
const confidenceScore = decision.confidence?.score ?? null;
|
|
133
|
-
const evidenceCompleteness = decision.inputs?.policy?.evidenceCompleteness ?? null;
|
|
134
|
-
const untested = deriveUntested(coverageCounts, attempts);
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
source: 'decision',
|
|
138
|
-
finalVerdict: verdict,
|
|
139
|
-
exitCode: decision.exitCode ?? null,
|
|
140
|
-
coveragePercent,
|
|
141
|
-
confidenceScore,
|
|
142
|
-
confidenceBasis: decision.confidence?.basis || '',
|
|
143
|
-
evidenceCompleteness,
|
|
144
|
-
attemptSummary,
|
|
145
|
-
untested
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function buildCanonicalTruth({ snapshotPath, decisionPath, snapshot, decision }) {
|
|
150
|
-
const snapCandidates = [];
|
|
151
|
-
if (snapshot) snapCandidates.push(snapshot);
|
|
152
|
-
if (snapshotPath) {
|
|
153
|
-
const snapFromDisk = safeReadJson(snapshotPath);
|
|
154
|
-
if (snapFromDisk) snapCandidates.push(snapFromDisk);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let snapCanonical = null;
|
|
158
|
-
for (const candidate of snapCandidates) {
|
|
159
|
-
const derived = deriveFromSnapshot(candidate);
|
|
160
|
-
if (!derived) continue;
|
|
161
|
-
if (!snapCanonical || (derived.attemptSummary?.total || 0) > (snapCanonical.attemptSummary?.total || 0)) {
|
|
162
|
-
snapCanonical = derived;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const decObj = decision || (decisionPath ? safeReadJson(decisionPath) : null);
|
|
167
|
-
|
|
168
|
-
let decCanonical = deriveFromDecision(decObj);
|
|
169
|
-
if (!decCanonical && decisionPath && !decision) {
|
|
170
|
-
decCanonical = deriveFromDecision(safeReadJson(decisionPath));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
let canonical = snapCanonical || decCanonical;
|
|
174
|
-
|
|
175
|
-
// Prefer the source with richer attempt data when both exist
|
|
176
|
-
if (snapCanonical && decCanonical) {
|
|
177
|
-
const snapAttempts = snapCanonical.attemptSummary?.total || 0;
|
|
178
|
-
const decAttempts = decCanonical.attemptSummary?.total || 0;
|
|
179
|
-
canonical = snapAttempts >= decAttempts ? snapCanonical : decCanonical;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (!canonical) {
|
|
183
|
-
throw new Error('Canonical truth could not be derived: missing snapshot/decision verdict');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Fill gaps from the other source if available
|
|
187
|
-
if (canonical === snapCanonical && decCanonical) {
|
|
188
|
-
canonical.exitCode = canonical.exitCode ?? decCanonical.exitCode;
|
|
189
|
-
const decCoverage = decCanonical.coveragePercent ?? 0;
|
|
190
|
-
if (canonical.coveragePercent === null || (canonical.coveragePercent === 0 && decCoverage > 0)) {
|
|
191
|
-
canonical.coveragePercent = decCoverage;
|
|
192
|
-
}
|
|
193
|
-
const decConfidence = decCanonical.confidenceScore ?? 0;
|
|
194
|
-
if (canonical.confidenceScore === null || (canonical.confidenceScore === 0 && decConfidence > 0)) {
|
|
195
|
-
canonical.confidenceScore = decConfidence;
|
|
196
|
-
}
|
|
197
|
-
canonical.confidenceBasis = canonical.confidenceBasis || decCanonical.confidenceBasis || '';
|
|
198
|
-
canonical.evidenceCompleteness = canonical.evidenceCompleteness ?? decCanonical.evidenceCompleteness;
|
|
199
|
-
if (!canonical.attemptSummary || canonical.attemptSummary.total === 0) {
|
|
200
|
-
canonical.attemptSummary = decCanonical.attemptSummary;
|
|
201
|
-
}
|
|
202
|
-
if ((!canonical.untested || canonical.untested.length === 0) && decCanonical.untested?.length) {
|
|
203
|
-
canonical.untested = decCanonical.untested;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
if (canonical === decCanonical && snapCanonical) {
|
|
207
|
-
canonical.coveragePercent = canonical.coveragePercent ?? snapCanonical.coveragePercent;
|
|
208
|
-
if (canonical.confidenceScore === null || (canonical.confidenceScore === 0 && (snapCanonical.confidenceScore ?? 0) > 0)) {
|
|
209
|
-
canonical.confidenceScore = snapCanonical.confidenceScore;
|
|
210
|
-
}
|
|
211
|
-
if (canonical.evidenceCompleteness === null && snapCanonical.evidenceCompleteness !== null) {
|
|
212
|
-
canonical.evidenceCompleteness = snapCanonical.evidenceCompleteness;
|
|
213
|
-
}
|
|
214
|
-
canonical.confidenceBasis = canonical.confidenceBasis || snapCanonical.confidenceBasis || '';
|
|
215
|
-
if (!canonical.attemptSummary || canonical.attemptSummary.total === 0) {
|
|
216
|
-
canonical.attemptSummary = snapCanonical.attemptSummary;
|
|
217
|
-
}
|
|
218
|
-
if ((!canonical.untested || canonical.untested.length === 0) && snapCanonical.untested?.length) {
|
|
219
|
-
canonical.untested = snapCanonical.untested;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Final enrichment from on-disk snapshot if present
|
|
224
|
-
if (snapshotPath) {
|
|
225
|
-
const snapFromDisk = deriveFromSnapshot(safeReadJson(snapshotPath));
|
|
226
|
-
if (snapFromDisk) {
|
|
227
|
-
if (canonical.confidenceScore === null || (canonical.confidenceScore === 0 && (snapFromDisk.confidenceScore ?? 0) > 0)) {
|
|
228
|
-
canonical.confidenceScore = snapFromDisk.confidenceScore;
|
|
229
|
-
}
|
|
230
|
-
if (!canonical.confidenceBasis && snapFromDisk.confidenceBasis) {
|
|
231
|
-
canonical.confidenceBasis = snapFromDisk.confidenceBasis;
|
|
232
|
-
}
|
|
233
|
-
if (canonical.evidenceCompleteness === null && snapFromDisk.evidenceCompleteness !== null) {
|
|
234
|
-
canonical.evidenceCompleteness = snapFromDisk.evidenceCompleteness;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Choose the strongest available confidence and evidence signals
|
|
240
|
-
const confidenceCandidates = [
|
|
241
|
-
canonical.confidenceScore,
|
|
242
|
-
snapCanonical?.confidenceScore,
|
|
243
|
-
decCanonical?.confidenceScore
|
|
244
|
-
].filter(v => v !== null && v !== undefined);
|
|
245
|
-
|
|
246
|
-
if (confidenceCandidates.length > 0) {
|
|
247
|
-
// Prefer non-zero confidence; fallback to first available
|
|
248
|
-
const bestConfidence = confidenceCandidates.find(v => v > 0);
|
|
249
|
-
canonical.confidenceScore = bestConfidence !== undefined ? bestConfidence : confidenceCandidates[0];
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Merge confidence basis from richest source
|
|
253
|
-
if (!canonical.confidenceBasis || canonical.confidenceBasis === '') {
|
|
254
|
-
canonical.confidenceBasis = snapCanonical?.confidenceBasis || decCanonical?.confidenceBasis || '';
|
|
255
|
-
}
|
|
256
|
-
if (canonical.evidenceCompleteness === null) {
|
|
257
|
-
const completenessCandidates = [snapCanonical?.evidenceCompleteness, decCanonical?.evidenceCompleteness].filter(v => v !== null);
|
|
258
|
-
if (completenessCandidates.length > 0) {
|
|
259
|
-
canonical.evidenceCompleteness = completenessCandidates[0];
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Normalize required fields (preserve non-zero confidence)
|
|
264
|
-
canonical.coveragePercent = canonical.coveragePercent ?? 0;
|
|
265
|
-
// CRITICAL: Confidence must exist; default to 0 only if truly absent everywhere
|
|
266
|
-
if (canonical.confidenceScore === null || canonical.confidenceScore === undefined) {
|
|
267
|
-
canonical.confidenceScore = 0;
|
|
268
|
-
}
|
|
269
|
-
canonical.attemptSummary = canonical.attemptSummary || deriveAttemptSummary();
|
|
270
|
-
canonical.untested = canonical.untested || [];
|
|
271
|
-
|
|
272
|
-
// CRITICAL: Preserve computed confidence in frozen object
|
|
273
|
-
const frozenCanonical = {
|
|
274
|
-
source: canonical.source,
|
|
275
|
-
finalVerdict: canonical.finalVerdict,
|
|
276
|
-
exitCode: canonical.exitCode,
|
|
277
|
-
coveragePercent: canonical.coveragePercent,
|
|
278
|
-
confidenceScore: canonical.confidenceScore,
|
|
279
|
-
confidenceBasis: canonical.confidenceBasis || '',
|
|
280
|
-
evidenceCompleteness: canonical.evidenceCompleteness,
|
|
281
|
-
attemptSummary: Object.freeze({ ...canonical.attemptSummary }),
|
|
282
|
-
untested: Object.freeze([...(canonical.untested || [])])
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
return Object.freeze(frozenCanonical);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function loadCanonicalTruth(params) {
|
|
289
|
-
return buildCanonicalTruth(params || {});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function assertCanonicalConsistency(canonical, proposedVerdict, context = 'report') {
|
|
293
|
-
if (!canonical) {
|
|
294
|
-
throw new Error('Canonical truth missing for consistency check');
|
|
295
|
-
}
|
|
296
|
-
if (proposedVerdict && proposedVerdict !== canonical.finalVerdict) {
|
|
297
|
-
throw new Error(`Canonical verdict mismatch in ${context}: ${proposedVerdict} !== ${canonical.finalVerdict}`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
module.exports = {
|
|
302
|
-
buildCanonicalTruth,
|
|
303
|
-
loadCanonicalTruth,
|
|
304
|
-
assertCanonicalConsistency,
|
|
305
|
-
deriveAttemptSummary,
|
|
306
|
-
deriveFromSnapshot,
|
|
307
|
-
deriveFromDecision
|
|
308
|
-
};
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
function safeReadJson(filePath) {
|
|
4
|
+
try {
|
|
5
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
6
|
+
} catch (_) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function deriveAttemptSummary(attempts = []) {
|
|
12
|
+
const summary = {
|
|
13
|
+
total: 0,
|
|
14
|
+
executed: 0,
|
|
15
|
+
skipped: 0,
|
|
16
|
+
notApplicable: 0,
|
|
17
|
+
failed: 0,
|
|
18
|
+
friction: 0,
|
|
19
|
+
success: 0
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
for (const attempt of attempts) {
|
|
23
|
+
if (!attempt) continue;
|
|
24
|
+
const outcome = (attempt.outcome || '').toUpperCase();
|
|
25
|
+
summary.total += 1;
|
|
26
|
+
|
|
27
|
+
const executedFlag =
|
|
28
|
+
attempt.executed === true ||
|
|
29
|
+
(attempt.executed === undefined && ['SUCCESS', 'FAILURE', 'FRICTION'].includes(outcome));
|
|
30
|
+
|
|
31
|
+
if (executedFlag) {
|
|
32
|
+
summary.executed += 1;
|
|
33
|
+
if (outcome === 'SUCCESS') summary.success += 1;
|
|
34
|
+
if (outcome === 'FAILURE') summary.failed += 1;
|
|
35
|
+
if (outcome === 'FRICTION') summary.friction += 1;
|
|
36
|
+
} else if (outcome === 'SKIPPED') {
|
|
37
|
+
summary.skipped += 1;
|
|
38
|
+
} else if (outcome === 'NOT_APPLICABLE' || outcome === 'DISCOVERY_FAILED') {
|
|
39
|
+
summary.notApplicable += 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return summary;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function deriveCoveragePercent(snapshotCoverage = {}, attemptSummary = null) {
|
|
47
|
+
const counts = snapshotCoverage?.counts || snapshotCoverage || {};
|
|
48
|
+
const attemptBased = attemptSummary && attemptSummary.total > 0
|
|
49
|
+
? Math.max(0, Math.min(100, Math.round((attemptSummary.executed / attemptSummary.total) * 100)))
|
|
50
|
+
: null;
|
|
51
|
+
|
|
52
|
+
if (counts.percent !== null) {
|
|
53
|
+
// Override obviously-wrong zero when we have executed attempts and a computed percent
|
|
54
|
+
if (counts.percent === 0 && attemptBased !== null && attemptSummary.executed > 0) {
|
|
55
|
+
return attemptBased;
|
|
56
|
+
}
|
|
57
|
+
return counts.percent;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (attemptBased !== null) {
|
|
61
|
+
return attemptBased;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const executed = counts.executedCount ?? attemptSummary?.executed ?? 0;
|
|
65
|
+
const enabled = counts.enabledPlannedCount ?? attemptSummary?.total ?? 0;
|
|
66
|
+
const excluded = counts.excludedNotApplicableFromTotal ?? counts.skippedNotApplicable ?? attemptSummary?.notApplicable ?? 0;
|
|
67
|
+
const userFiltered = counts.skippedUserFiltered ?? attemptSummary?.skipped ?? 0;
|
|
68
|
+
|
|
69
|
+
const denominator = Math.max(enabled - excluded - userFiltered, 0);
|
|
70
|
+
if (denominator === 0) return 0;
|
|
71
|
+
return Math.max(0, Math.min(100, Math.round((executed / denominator) * 100)));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function deriveUntested(snapshotCoverage = {}, attempts = []) {
|
|
75
|
+
const details = [];
|
|
76
|
+
const addDetail = (items, label) => {
|
|
77
|
+
(items || []).forEach(item => {
|
|
78
|
+
if (item?.attempt) {
|
|
79
|
+
details.push(`${label}:${item.attempt}${item.reason ? ` (${item.reason})` : ''}`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
addDetail(snapshotCoverage.skippedNotApplicable, 'n/a');
|
|
85
|
+
addDetail(snapshotCoverage.skippedUserFiltered, 'skipped');
|
|
86
|
+
addDetail(snapshotCoverage.skippedDisabledByPreset, 'disabled');
|
|
87
|
+
addDetail(snapshotCoverage.skippedMissing, 'missing');
|
|
88
|
+
|
|
89
|
+
if (details.length === 0 && attempts.length > 0) {
|
|
90
|
+
attempts.filter(a => !a.executed && a.outcome === 'SKIPPED').forEach(a => details.push(`skipped:${a.attemptId}`));
|
|
91
|
+
attempts.filter(a => a.outcome === 'NOT_APPLICABLE').forEach(a => details.push(`n/a:${a.attemptId}`));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return details;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function deriveFromSnapshot(snapshot) {
|
|
98
|
+
if (!snapshot) return null;
|
|
99
|
+
const verdict = snapshot.verdict?.verdict || snapshot.meta?.result || null;
|
|
100
|
+
if (!verdict) return null;
|
|
101
|
+
|
|
102
|
+
const attempts = snapshot.attempts || [];
|
|
103
|
+
const attemptSummary = deriveAttemptSummary(attempts);
|
|
104
|
+
const coveragePercent = deriveCoveragePercent(snapshot.meta?.coverage || snapshot.coverage || {}, attemptSummary);
|
|
105
|
+
const evidenceCompleteness = snapshot.meta?.evidenceMetrics?.completeness ?? snapshot.evidenceMetrics?.completeness ?? null;
|
|
106
|
+
const confidenceScore = snapshot.verdict?.confidence?.score ?? null;
|
|
107
|
+
const confidenceBasis = snapshot.verdict?.confidence?.basis ?? '';
|
|
108
|
+
const exitCode = snapshot.meta?.exitCode ?? null;
|
|
109
|
+
const untested = deriveUntested(snapshot.meta?.coverage || snapshot.coverage || {}, attempts);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
source: 'snapshot',
|
|
113
|
+
finalVerdict: verdict,
|
|
114
|
+
exitCode,
|
|
115
|
+
coveragePercent,
|
|
116
|
+
confidenceScore,
|
|
117
|
+
confidenceBasis,
|
|
118
|
+
evidenceCompleteness,
|
|
119
|
+
attemptSummary,
|
|
120
|
+
untested
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function deriveFromDecision(decision) {
|
|
125
|
+
if (!decision) return null;
|
|
126
|
+
const verdict = decision.finalVerdict || null;
|
|
127
|
+
if (!verdict) return null;
|
|
128
|
+
const attempts = Array.isArray(decision.outcomes?.attempts) ? decision.outcomes.attempts : [];
|
|
129
|
+
const attemptSummary = deriveAttemptSummary(attempts);
|
|
130
|
+
const coverageCounts = decision.coverage || {};
|
|
131
|
+
const coveragePercent = deriveCoveragePercent(coverageCounts, attemptSummary);
|
|
132
|
+
const confidenceScore = decision.confidence?.score ?? null;
|
|
133
|
+
const evidenceCompleteness = decision.inputs?.policy?.evidenceCompleteness ?? null;
|
|
134
|
+
const untested = deriveUntested(coverageCounts, attempts);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
source: 'decision',
|
|
138
|
+
finalVerdict: verdict,
|
|
139
|
+
exitCode: decision.exitCode ?? null,
|
|
140
|
+
coveragePercent,
|
|
141
|
+
confidenceScore,
|
|
142
|
+
confidenceBasis: decision.confidence?.basis || '',
|
|
143
|
+
evidenceCompleteness,
|
|
144
|
+
attemptSummary,
|
|
145
|
+
untested
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildCanonicalTruth({ snapshotPath, decisionPath, snapshot, decision }) {
|
|
150
|
+
const snapCandidates = [];
|
|
151
|
+
if (snapshot) snapCandidates.push(snapshot);
|
|
152
|
+
if (snapshotPath) {
|
|
153
|
+
const snapFromDisk = safeReadJson(snapshotPath);
|
|
154
|
+
if (snapFromDisk) snapCandidates.push(snapFromDisk);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let snapCanonical = null;
|
|
158
|
+
for (const candidate of snapCandidates) {
|
|
159
|
+
const derived = deriveFromSnapshot(candidate);
|
|
160
|
+
if (!derived) continue;
|
|
161
|
+
if (!snapCanonical || (derived.attemptSummary?.total || 0) > (snapCanonical.attemptSummary?.total || 0)) {
|
|
162
|
+
snapCanonical = derived;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const decObj = decision || (decisionPath ? safeReadJson(decisionPath) : null);
|
|
167
|
+
|
|
168
|
+
let decCanonical = deriveFromDecision(decObj);
|
|
169
|
+
if (!decCanonical && decisionPath && !decision) {
|
|
170
|
+
decCanonical = deriveFromDecision(safeReadJson(decisionPath));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let canonical = snapCanonical || decCanonical;
|
|
174
|
+
|
|
175
|
+
// Prefer the source with richer attempt data when both exist
|
|
176
|
+
if (snapCanonical && decCanonical) {
|
|
177
|
+
const snapAttempts = snapCanonical.attemptSummary?.total || 0;
|
|
178
|
+
const decAttempts = decCanonical.attemptSummary?.total || 0;
|
|
179
|
+
canonical = snapAttempts >= decAttempts ? snapCanonical : decCanonical;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!canonical) {
|
|
183
|
+
throw new Error('Canonical truth could not be derived: missing snapshot/decision verdict');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Fill gaps from the other source if available
|
|
187
|
+
if (canonical === snapCanonical && decCanonical) {
|
|
188
|
+
canonical.exitCode = canonical.exitCode ?? decCanonical.exitCode;
|
|
189
|
+
const decCoverage = decCanonical.coveragePercent ?? 0;
|
|
190
|
+
if (canonical.coveragePercent === null || (canonical.coveragePercent === 0 && decCoverage > 0)) {
|
|
191
|
+
canonical.coveragePercent = decCoverage;
|
|
192
|
+
}
|
|
193
|
+
const decConfidence = decCanonical.confidenceScore ?? 0;
|
|
194
|
+
if (canonical.confidenceScore === null || (canonical.confidenceScore === 0 && decConfidence > 0)) {
|
|
195
|
+
canonical.confidenceScore = decConfidence;
|
|
196
|
+
}
|
|
197
|
+
canonical.confidenceBasis = canonical.confidenceBasis || decCanonical.confidenceBasis || '';
|
|
198
|
+
canonical.evidenceCompleteness = canonical.evidenceCompleteness ?? decCanonical.evidenceCompleteness;
|
|
199
|
+
if (!canonical.attemptSummary || canonical.attemptSummary.total === 0) {
|
|
200
|
+
canonical.attemptSummary = decCanonical.attemptSummary;
|
|
201
|
+
}
|
|
202
|
+
if ((!canonical.untested || canonical.untested.length === 0) && decCanonical.untested?.length) {
|
|
203
|
+
canonical.untested = decCanonical.untested;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (canonical === decCanonical && snapCanonical) {
|
|
207
|
+
canonical.coveragePercent = canonical.coveragePercent ?? snapCanonical.coveragePercent;
|
|
208
|
+
if (canonical.confidenceScore === null || (canonical.confidenceScore === 0 && (snapCanonical.confidenceScore ?? 0) > 0)) {
|
|
209
|
+
canonical.confidenceScore = snapCanonical.confidenceScore;
|
|
210
|
+
}
|
|
211
|
+
if (canonical.evidenceCompleteness === null && snapCanonical.evidenceCompleteness !== null) {
|
|
212
|
+
canonical.evidenceCompleteness = snapCanonical.evidenceCompleteness;
|
|
213
|
+
}
|
|
214
|
+
canonical.confidenceBasis = canonical.confidenceBasis || snapCanonical.confidenceBasis || '';
|
|
215
|
+
if (!canonical.attemptSummary || canonical.attemptSummary.total === 0) {
|
|
216
|
+
canonical.attemptSummary = snapCanonical.attemptSummary;
|
|
217
|
+
}
|
|
218
|
+
if ((!canonical.untested || canonical.untested.length === 0) && snapCanonical.untested?.length) {
|
|
219
|
+
canonical.untested = snapCanonical.untested;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Final enrichment from on-disk snapshot if present
|
|
224
|
+
if (snapshotPath) {
|
|
225
|
+
const snapFromDisk = deriveFromSnapshot(safeReadJson(snapshotPath));
|
|
226
|
+
if (snapFromDisk) {
|
|
227
|
+
if (canonical.confidenceScore === null || (canonical.confidenceScore === 0 && (snapFromDisk.confidenceScore ?? 0) > 0)) {
|
|
228
|
+
canonical.confidenceScore = snapFromDisk.confidenceScore;
|
|
229
|
+
}
|
|
230
|
+
if (!canonical.confidenceBasis && snapFromDisk.confidenceBasis) {
|
|
231
|
+
canonical.confidenceBasis = snapFromDisk.confidenceBasis;
|
|
232
|
+
}
|
|
233
|
+
if (canonical.evidenceCompleteness === null && snapFromDisk.evidenceCompleteness !== null) {
|
|
234
|
+
canonical.evidenceCompleteness = snapFromDisk.evidenceCompleteness;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Choose the strongest available confidence and evidence signals
|
|
240
|
+
const confidenceCandidates = [
|
|
241
|
+
canonical.confidenceScore,
|
|
242
|
+
snapCanonical?.confidenceScore,
|
|
243
|
+
decCanonical?.confidenceScore
|
|
244
|
+
].filter(v => v !== null && v !== undefined);
|
|
245
|
+
|
|
246
|
+
if (confidenceCandidates.length > 0) {
|
|
247
|
+
// Prefer non-zero confidence; fallback to first available
|
|
248
|
+
const bestConfidence = confidenceCandidates.find(v => v > 0);
|
|
249
|
+
canonical.confidenceScore = bestConfidence !== undefined ? bestConfidence : confidenceCandidates[0];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Merge confidence basis from richest source
|
|
253
|
+
if (!canonical.confidenceBasis || canonical.confidenceBasis === '') {
|
|
254
|
+
canonical.confidenceBasis = snapCanonical?.confidenceBasis || decCanonical?.confidenceBasis || '';
|
|
255
|
+
}
|
|
256
|
+
if (canonical.evidenceCompleteness === null) {
|
|
257
|
+
const completenessCandidates = [snapCanonical?.evidenceCompleteness, decCanonical?.evidenceCompleteness].filter(v => v !== null);
|
|
258
|
+
if (completenessCandidates.length > 0) {
|
|
259
|
+
canonical.evidenceCompleteness = completenessCandidates[0];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Normalize required fields (preserve non-zero confidence)
|
|
264
|
+
canonical.coveragePercent = canonical.coveragePercent ?? 0;
|
|
265
|
+
// CRITICAL: Confidence must exist; default to 0 only if truly absent everywhere
|
|
266
|
+
if (canonical.confidenceScore === null || canonical.confidenceScore === undefined) {
|
|
267
|
+
canonical.confidenceScore = 0;
|
|
268
|
+
}
|
|
269
|
+
canonical.attemptSummary = canonical.attemptSummary || deriveAttemptSummary();
|
|
270
|
+
canonical.untested = canonical.untested || [];
|
|
271
|
+
|
|
272
|
+
// CRITICAL: Preserve computed confidence in frozen object
|
|
273
|
+
const frozenCanonical = {
|
|
274
|
+
source: canonical.source,
|
|
275
|
+
finalVerdict: canonical.finalVerdict,
|
|
276
|
+
exitCode: canonical.exitCode,
|
|
277
|
+
coveragePercent: canonical.coveragePercent,
|
|
278
|
+
confidenceScore: canonical.confidenceScore,
|
|
279
|
+
confidenceBasis: canonical.confidenceBasis || '',
|
|
280
|
+
evidenceCompleteness: canonical.evidenceCompleteness,
|
|
281
|
+
attemptSummary: Object.freeze({ ...canonical.attemptSummary }),
|
|
282
|
+
untested: Object.freeze([...(canonical.untested || [])])
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return Object.freeze(frozenCanonical);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function loadCanonicalTruth(params) {
|
|
289
|
+
return buildCanonicalTruth(params || {});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function assertCanonicalConsistency(canonical, proposedVerdict, context = 'report') {
|
|
293
|
+
if (!canonical) {
|
|
294
|
+
throw new Error('Canonical truth missing for consistency check');
|
|
295
|
+
}
|
|
296
|
+
if (proposedVerdict && proposedVerdict !== canonical.finalVerdict) {
|
|
297
|
+
throw new Error(`Canonical verdict mismatch in ${context}: ${proposedVerdict} !== ${canonical.finalVerdict}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = {
|
|
302
|
+
buildCanonicalTruth,
|
|
303
|
+
loadCanonicalTruth,
|
|
304
|
+
assertCanonicalConsistency,
|
|
305
|
+
deriveAttemptSummary,
|
|
306
|
+
deriveFromSnapshot,
|
|
307
|
+
deriveFromDecision
|
|
308
|
+
};
|