@odavl/guardian 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -2
- package/README.md +155 -97
- package/bin/guardian.js +1345 -60
- package/config/README.md +59 -0
- package/config/profiles/landing-demo.yaml +16 -0
- package/package.json +21 -11
- package/policies/landing-demo.json +22 -0
- package/src/enterprise/audit-logger.js +166 -0
- package/src/enterprise/pdf-exporter.js +267 -0
- package/src/enterprise/rbac-gate.js +142 -0
- package/src/enterprise/rbac.js +239 -0
- package/src/enterprise/site-manager.js +180 -0
- package/src/founder/feedback-system.js +156 -0
- package/src/founder/founder-tracker.js +213 -0
- package/src/founder/usage-signals.js +141 -0
- package/src/guardian/alert-ledger.js +121 -0
- package/src/guardian/attempt-engine.js +568 -7
- package/src/guardian/attempt-registry.js +42 -1
- package/src/guardian/attempt-relevance.js +106 -0
- package/src/guardian/attempt.js +24 -0
- package/src/guardian/baseline.js +12 -4
- package/src/guardian/breakage-intelligence.js +1 -0
- package/src/guardian/ci-cli.js +121 -0
- package/src/guardian/ci-output.js +4 -3
- package/src/guardian/cli-summary.js +79 -92
- package/src/guardian/config-loader.js +162 -0
- package/src/guardian/drift-detector.js +100 -0
- package/src/guardian/enhanced-html-reporter.js +221 -4
- package/src/guardian/env-guard.js +127 -0
- package/src/guardian/failure-intelligence.js +173 -0
- package/src/guardian/first-run-profile.js +89 -0
- package/src/guardian/first-run.js +6 -1
- package/src/guardian/flag-validator.js +17 -3
- package/src/guardian/html-reporter.js +2 -0
- package/src/guardian/human-reporter.js +431 -0
- package/src/guardian/index.js +22 -19
- package/src/guardian/init-command.js +9 -5
- package/src/guardian/intent-detector.js +146 -0
- package/src/guardian/journey-definitions.js +132 -0
- package/src/guardian/journey-scan-cli.js +145 -0
- package/src/guardian/journey-scanner.js +583 -0
- package/src/guardian/junit-reporter.js +18 -1
- package/src/guardian/live-cli.js +95 -0
- package/src/guardian/live-scheduler-runner.js +137 -0
- package/src/guardian/live-scheduler.js +146 -0
- package/src/guardian/market-reporter.js +341 -81
- package/src/guardian/pattern-analyzer.js +348 -0
- package/src/guardian/policy.js +80 -3
- package/src/guardian/preset-loader.js +9 -6
- package/src/guardian/reality.js +1278 -117
- package/src/guardian/reporter.js +27 -41
- package/src/guardian/run-artifacts.js +212 -0
- package/src/guardian/run-cleanup.js +207 -0
- package/src/guardian/run-latest.js +90 -0
- package/src/guardian/run-list.js +211 -0
- package/src/guardian/scan-presets.js +100 -11
- package/src/guardian/selector-fallbacks.js +394 -0
- package/src/guardian/semantic-contact-finder.js +2 -1
- package/src/guardian/site-introspection.js +257 -0
- package/src/guardian/smoke.js +2 -2
- package/src/guardian/snapshot-schema.js +25 -1
- package/src/guardian/snapshot.js +46 -2
- package/src/guardian/stability-scorer.js +169 -0
- package/src/guardian/template-command.js +184 -0
- package/src/guardian/text-formatters.js +426 -0
- package/src/guardian/verdict.js +320 -0
- package/src/guardian/verdicts.js +74 -0
- package/src/guardian/watch-runner.js +3 -7
- package/src/payments/stripe-checkout.js +169 -0
- package/src/plans/plan-definitions.js +148 -0
- package/src/plans/plan-manager.js +211 -0
- package/src/plans/usage-tracker.js +210 -0
- package/src/recipes/recipe-engine.js +188 -0
- package/src/recipes/recipe-failure-analysis.js +159 -0
- package/src/recipes/recipe-registry.js +134 -0
- package/src/recipes/recipe-runtime.js +507 -0
- package/src/recipes/recipe-store.js +410 -0
- package/guardian-contract-v1.md +0 -149
- /package/{guardian.config.json → config/guardian.config.json} +0 -0
- /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
- /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
- /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
- /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
- /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Verdict Builder
|
|
3
|
+
* Deterministic, explainable verdict and confidence scoring
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function clamp01(x) { return Math.max(0, Math.min(1, x)); }
|
|
7
|
+
|
|
8
|
+
const isExecutedAttempt = (attempt) => attempt && attempt.outcome !== 'SKIPPED';
|
|
9
|
+
|
|
10
|
+
function mapRunResultToVerdict(runResult) {
|
|
11
|
+
const r = (runResult || '').toUpperCase();
|
|
12
|
+
if (r === 'PASSED') return 'READY';
|
|
13
|
+
if (r === 'FAILED') return 'DO_NOT_LAUNCH';
|
|
14
|
+
if (r === 'INSUFFICIENT_EVIDENCE') return 'INSUFFICIENT_EVIDENCE';
|
|
15
|
+
return 'FRICTION'; // WARN or anything else
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function computeCoverageScore(snapshot) {
|
|
19
|
+
const disc = snapshot.discovery || {};
|
|
20
|
+
if (disc.interactionsDiscovered && disc.interactionsDiscovered > 0) {
|
|
21
|
+
return clamp01((disc.interactionsExecuted || 0) / (disc.interactionsDiscovered || 1));
|
|
22
|
+
}
|
|
23
|
+
const attempts = snapshot.attempts || [];
|
|
24
|
+
const executedAttempts = attempts.filter(isExecutedAttempt);
|
|
25
|
+
const executed = executedAttempts.length;
|
|
26
|
+
return executed > 0 ? 1 : 0; // if we have executed attempts, consider coverage adequate
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function computeEvidenceScore(snapshot) {
|
|
30
|
+
const e = snapshot.evidence || {};
|
|
31
|
+
const features = [
|
|
32
|
+
!!e.marketReportJson,
|
|
33
|
+
!!e.marketReportHtml,
|
|
34
|
+
!!e.traceZip,
|
|
35
|
+
!!(e.attemptArtifacts && Object.keys(e.attemptArtifacts).length > 0)
|
|
36
|
+
];
|
|
37
|
+
const present = features.filter(Boolean).length;
|
|
38
|
+
return clamp01(present / features.length);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function computePenalty(snapshot) {
|
|
42
|
+
const attempts = snapshot.attempts || [];
|
|
43
|
+
const failures = attempts.filter(a => a.outcome === 'FAILURE').length;
|
|
44
|
+
const frictions = attempts.filter(a => a.outcome === 'FRICTION').length;
|
|
45
|
+
const perFailure = 0.15;
|
|
46
|
+
const perFriction = 0.1;
|
|
47
|
+
const raw = failures * perFailure + frictions * perFriction;
|
|
48
|
+
return Math.min(0.5, raw);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function levelFromScore(score) {
|
|
52
|
+
if (score >= 0.66) return 'high';
|
|
53
|
+
if (score >= 0.33) return 'medium';
|
|
54
|
+
return 'low';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function collectKeyFindings(snapshot) {
|
|
58
|
+
const findings = [];
|
|
59
|
+
const attempts = snapshot.attempts || [];
|
|
60
|
+
const executedAttempts = attempts.filter(a => a.outcome !== 'SKIPPED');
|
|
61
|
+
const skippedAttempts = attempts.filter(a => a.outcome === 'SKIPPED');
|
|
62
|
+
const successful = executedAttempts.filter(a => a.outcome === 'SUCCESS').length;
|
|
63
|
+
const failed = executedAttempts.filter(a => a.outcome === 'FAILURE').length;
|
|
64
|
+
const friction = executedAttempts.filter(a => a.outcome === 'FRICTION').length;
|
|
65
|
+
|
|
66
|
+
// Attempt outcomes
|
|
67
|
+
if (executedAttempts.length > 0) {
|
|
68
|
+
findings.push(`${successful} of ${executedAttempts.length} executed attempts completed`);
|
|
69
|
+
if (failed > 0) findings.push(`${failed} attempt did not complete`);
|
|
70
|
+
if (friction > 0) findings.push(`${friction} attempt showed friction (slower or rougher than baseline)`);
|
|
71
|
+
if (skippedAttempts.length > 0) {
|
|
72
|
+
findings.push(`${skippedAttempts.length} attempt was not executed`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Market impact
|
|
77
|
+
const mi = snapshot.marketImpactSummary || {};
|
|
78
|
+
const counts = mi.countsBySeverity || { CRITICAL: 0, WARNING: 0, INFO: 0 };
|
|
79
|
+
if (counts.CRITICAL > 0 || counts.WARNING > 0) {
|
|
80
|
+
findings.push(`${counts.CRITICAL} critical risk and ${counts.WARNING} warning risk identified`);
|
|
81
|
+
} else {
|
|
82
|
+
findings.push(`No critical or warning risks identified`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Discovery coverage
|
|
86
|
+
const disc = snapshot.discovery || {};
|
|
87
|
+
if ((disc.interactionsExecuted || 0) > 0) {
|
|
88
|
+
findings.push(`${disc.interactionsExecuted}/${disc.interactionsDiscovered || 0} discovered interactions executed`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Evidence
|
|
92
|
+
const e = snapshot.evidence || {};
|
|
93
|
+
const hasReports = !!(e.marketReportJson && e.marketReportHtml);
|
|
94
|
+
const hasTraces = !!e.traceZip;
|
|
95
|
+
const hasScreenshots = !!(e.attemptArtifacts && Object.keys(e.attemptArtifacts).length > 0);
|
|
96
|
+
const evidenceCount = [hasReports, hasTraces, hasScreenshots].filter(Boolean).length;
|
|
97
|
+
if (evidenceCount === 3) {
|
|
98
|
+
findings.push(`Complete evidence: reports, traces, and screenshots captured`);
|
|
99
|
+
} else if (evidenceCount > 0) {
|
|
100
|
+
findings.push(`Partial evidence: ${[hasReports && 'reports', hasTraces && 'traces', hasScreenshots && 'screenshots'].filter(Boolean).join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return findings.slice(0, 7);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function collectEvidenceRefs(snapshot) {
|
|
107
|
+
const e = snapshot.evidence || {};
|
|
108
|
+
const reportPaths = [];
|
|
109
|
+
if (e.marketReportJson) reportPaths.push(e.marketReportJson);
|
|
110
|
+
if (e.marketReportHtml) reportPaths.push(e.marketReportHtml);
|
|
111
|
+
const traces = e.traceZip ? [e.traceZip] : [];
|
|
112
|
+
const screenshots = [];
|
|
113
|
+
if (e.attemptArtifacts) {
|
|
114
|
+
Object.values(e.attemptArtifacts).forEach(a => {
|
|
115
|
+
if (a.screenshotDir) screenshots.push(a.screenshotDir);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const affectedPages = (snapshot.discovery?.pagesVisited || []).slice(0, 10);
|
|
119
|
+
return { reportPaths, traces, screenshots, affectedPages };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function defaultLimits() {
|
|
123
|
+
return [
|
|
124
|
+
'Live-site variability may affect signals',
|
|
125
|
+
'Browser/viewport differences can change outcomes',
|
|
126
|
+
'Coverage limited to configured attempts and discovered interactions'
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function situationalLimits(snapshot) {
|
|
131
|
+
const limits = [];
|
|
132
|
+
const attempts = snapshot.attempts || [];
|
|
133
|
+
const skippedCount = attempts.filter(a => a.outcome === 'SKIPPED').length;
|
|
134
|
+
const disc = snapshot.discovery || {};
|
|
135
|
+
|
|
136
|
+
// Skipped attempts
|
|
137
|
+
if (skippedCount > 0) {
|
|
138
|
+
limits.push(`${skippedCount} additional attempt was not executed; verdict is therefore limited to fewer paths`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Discovery coverage gaps
|
|
142
|
+
const discovered = disc.interactionsDiscovered || 0;
|
|
143
|
+
const executed = disc.interactionsExecuted || 0;
|
|
144
|
+
if (discovered > 0 && executed < discovered) {
|
|
145
|
+
limits.push(`${discovered - executed} of ${discovered} discovered interactions were not tested`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Evidence gaps
|
|
149
|
+
const e = snapshot.evidence || {};
|
|
150
|
+
if (!e.traceZip) {
|
|
151
|
+
limits.push(`Detailed timing traces were not captured; timing-related issues may not be visible`);
|
|
152
|
+
}
|
|
153
|
+
if (!e.marketReportJson) {
|
|
154
|
+
limits.push(`Market risk analysis was not run; business impact is unassessed`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Environment variability
|
|
158
|
+
limits.push(`Live site behavior shifts over time; this verdict reflects this moment`);
|
|
159
|
+
|
|
160
|
+
// Browser/environment
|
|
161
|
+
limits.push(`Results are specific to this browser and viewport; other browsers may differ`);
|
|
162
|
+
|
|
163
|
+
return limits.slice(0, 6);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Build unified verdict from snapshot
|
|
168
|
+
*/
|
|
169
|
+
function deriveRunResult(snapshot) {
|
|
170
|
+
const meta = snapshot.meta || {};
|
|
171
|
+
if (meta.result) return meta.result;
|
|
172
|
+
const attempts = snapshot.attempts || [];
|
|
173
|
+
|
|
174
|
+
// Count different outcome types
|
|
175
|
+
const executedAttempts = attempts.filter(a => a.outcome !== 'SKIPPED' && a.outcome !== 'NOT_APPLICABLE');
|
|
176
|
+
const discoveryFailedAttempts = attempts.filter(a => a.outcome === 'DISCOVERY_FAILED');
|
|
177
|
+
const notApplicableAttempts = attempts.filter(a => a.outcome === 'NOT_APPLICABLE');
|
|
178
|
+
const skippedCount = attempts.filter(a => a.outcome === 'SKIPPED').length;
|
|
179
|
+
|
|
180
|
+
const executed = executedAttempts.length;
|
|
181
|
+
const successful = executedAttempts.filter(a => a.outcome === 'SUCCESS').length;
|
|
182
|
+
const failed = executedAttempts.filter(a => a.outcome === 'FAILURE').length;
|
|
183
|
+
|
|
184
|
+
// If NOTHING executed (all skipped, not applicable, or discovery failed), it's insufficient
|
|
185
|
+
if (executed === 0 && (discoveryFailedAttempts.length > 0 || skippedCount > 0)) {
|
|
186
|
+
return 'INSUFFICIENT_EVIDENCE';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (executed === 0) return 'WARN';
|
|
190
|
+
if (failed === 0 && successful === executed) return 'PASSED';
|
|
191
|
+
if (failed === executed) return 'FAILED';
|
|
192
|
+
return 'WARN';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildVerdictExplanation(snapshot, verdictStr, executedCount, successCount, failureCount, skippedCount, evidenceScore, penaltyApplied) {
|
|
196
|
+
let why = '';
|
|
197
|
+
|
|
198
|
+
if (verdictStr === 'READY') {
|
|
199
|
+
why = `All ${executedCount} executed attempts completed successfully without critical issues. `;
|
|
200
|
+
if (skippedCount > 0) {
|
|
201
|
+
why += `${skippedCount} additional attempt was not executed. `;
|
|
202
|
+
}
|
|
203
|
+
why += `Evidence is ${evidenceScore >= 0.75 ? 'complete' : 'sufficient'}. `;
|
|
204
|
+
why += `This differs from FRICTION because all attempts succeeded, and from DO_NOT_LAUNCH because no critical issues were found.`;
|
|
205
|
+
} else if (verdictStr === 'DO_NOT_LAUNCH') {
|
|
206
|
+
why = `${failureCount} critical issue was found, preventing safe launch. `;
|
|
207
|
+
why += `This differs from READY because critical issues must be resolved, and from FRICTION because severity exceeds acceptable limits.`;
|
|
208
|
+
} else if (verdictStr === 'INSUFFICIENT_EVIDENCE') {
|
|
209
|
+
why = `No meaningful attempts executed on this site. Most attempts were skipped (not applicable to site) or element discovery failed. `;
|
|
210
|
+
why += `This site may not match the tested user journeys, or it may be uninstrumented (missing expected UI elements/patterns). `;
|
|
211
|
+
why += `Verdict cannot be determined without successful execution of at least one relevant journey.`;
|
|
212
|
+
} else {
|
|
213
|
+
why = `Results show ${successCount} of ${executedCount} attempted success with some roughness. `;
|
|
214
|
+
if (failureCount > 0) {
|
|
215
|
+
why += `${failureCount} attempt did not complete. `;
|
|
216
|
+
}
|
|
217
|
+
why += `This differs from READY because outcomes were mixed, and from DO_NOT_LAUNCH because no critical failure was found.`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return why;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function deriveNextRunHint(snapshot, confidenceLevel) {
|
|
224
|
+
// Non-intrusive guard: suppress when confidence is high and limits are empty
|
|
225
|
+
const limits = situationalLimits(snapshot) || [];
|
|
226
|
+
if (confidenceLevel === 'high' && limits.length === 0) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// (a) Evidence completeness
|
|
231
|
+
const e = snapshot.evidence || {};
|
|
232
|
+
const hasReports = !!(e.marketReportJson && e.marketReportHtml);
|
|
233
|
+
const hasTrace = !!e.traceZip;
|
|
234
|
+
const hasScreenshots = !!(e.attemptArtifacts && Object.keys(e.attemptArtifacts).length > 0);
|
|
235
|
+
if (!hasTrace || !hasScreenshots) {
|
|
236
|
+
return 'Capture additional evidence (e.g., traces) to strengthen timing and flow visibility.';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// (b) Coverage completeness
|
|
240
|
+
const disc = snapshot.discovery || {};
|
|
241
|
+
const discovered = disc.interactionsDiscovered || 0;
|
|
242
|
+
const executed = disc.interactionsExecuted || 0;
|
|
243
|
+
if (discovered > 0 && executed < discovered) {
|
|
244
|
+
return 'Expand coverage to include additional discovered interactions.';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// (c) Recurring timeouts/flakiness within this run
|
|
248
|
+
const attempts = snapshot.attempts || [];
|
|
249
|
+
const flows = snapshot.flows || [];
|
|
250
|
+
const timeoutRegex = /(timeout|timed out)/i;
|
|
251
|
+
const timeoutCount = (
|
|
252
|
+
attempts.filter(a => typeof a.error === 'string' && timeoutRegex.test(a.error)).length +
|
|
253
|
+
flows.filter(f => typeof f.error === 'string' && timeoutRegex.test(f.error)).length
|
|
254
|
+
);
|
|
255
|
+
const frictionCount = attempts.filter(a => a.friction && a.friction.isFriction).length;
|
|
256
|
+
if (timeoutCount >= 2 || frictionCount >= 2) {
|
|
257
|
+
return 'Use a conservative timeout profile to reduce flakiness.';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// (d) Otherwise: no hint
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function buildVerdict(snapshot) {
|
|
265
|
+
const runRes = deriveRunResult(snapshot);
|
|
266
|
+
const verdictStr = mapRunResultToVerdict(runRes || 'WARN');
|
|
267
|
+
|
|
268
|
+
const outcomeScore = verdictStr === 'READY' ? 1 : verdictStr === 'DO_NOT_LAUNCH' ? 0.2 : 0.5;
|
|
269
|
+
const coverageScore = computeCoverageScore(snapshot); // 0..1
|
|
270
|
+
const evidenceScore = computeEvidenceScore(snapshot); // 0..1
|
|
271
|
+
const penalty = computePenalty(snapshot); // 0..0.5
|
|
272
|
+
|
|
273
|
+
const raw = 0.5 * outcomeScore + 0.2 * coverageScore + 0.3 * evidenceScore - penalty;
|
|
274
|
+
const score = clamp01(raw);
|
|
275
|
+
const level = levelFromScore(score);
|
|
276
|
+
|
|
277
|
+
// Detailed metrics for explanation
|
|
278
|
+
const attempts = snapshot.attempts || [];
|
|
279
|
+
const executedAttempts = attempts.filter(a => a.outcome !== 'SKIPPED');
|
|
280
|
+
const executedCount = executedAttempts.length;
|
|
281
|
+
const successCount = executedAttempts.filter(a => a.outcome === 'SUCCESS').length;
|
|
282
|
+
const failureCount = executedAttempts.filter(a => a.outcome === 'FAILURE').length;
|
|
283
|
+
const skippedCount = attempts.filter(a => a.outcome === 'SKIPPED').length;
|
|
284
|
+
|
|
285
|
+
const reasons = [];
|
|
286
|
+
if (successCount === executedCount && executedCount > 0) {
|
|
287
|
+
reasons.push(`All ${executedCount} executed attempts completed`);
|
|
288
|
+
} else if (successCount > 0) {
|
|
289
|
+
reasons.push(`${successCount} of ${executedCount} executed attempts completed`);
|
|
290
|
+
}
|
|
291
|
+
if (coverageScore > 0.66) reasons.push('Good coverage from discovered interactions');
|
|
292
|
+
if (evidenceScore >= 0.75) reasons.push('Complete evidence captured (reports, traces, screenshots)');
|
|
293
|
+
if (penalty > 0) reasons.push(`${failureCount > 0 ? 'Incomplete attempts' : 'Friction signals'} observed (${(penalty * 100).toFixed(0)}% impact)`);
|
|
294
|
+
// Do not include 'not executed' in drivers; it appears in findings/limits
|
|
295
|
+
|
|
296
|
+
const why = buildVerdictExplanation(
|
|
297
|
+
snapshot,
|
|
298
|
+
verdictStr,
|
|
299
|
+
executedCount,
|
|
300
|
+
successCount,
|
|
301
|
+
failureCount,
|
|
302
|
+
skippedCount,
|
|
303
|
+
evidenceScore,
|
|
304
|
+
penalty > 0
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const nextRunHint = deriveNextRunHint(snapshot, level);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
verdict: verdictStr,
|
|
311
|
+
confidence: { level, score, reasons },
|
|
312
|
+
why,
|
|
313
|
+
keyFindings: collectKeyFindings(snapshot),
|
|
314
|
+
evidence: collectEvidenceRefs(snapshot),
|
|
315
|
+
limits: situationalLimits(snapshot),
|
|
316
|
+
nextRunHint
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = { buildVerdict };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verdict mapping utilities
|
|
3
|
+
* Canonical user-facing verdicts: READY, FRICTION, DO_NOT_LAUNCH
|
|
4
|
+
* Internal engine verdicts (legacy): OBSERVED, PARTIAL, INSUFFICIENT_DATA
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function toCanonicalVerdict(internalVerdict) {
|
|
8
|
+
switch (String(internalVerdict || '').toUpperCase()) {
|
|
9
|
+
// Internal engine verdicts
|
|
10
|
+
case 'OBSERVED':
|
|
11
|
+
return 'READY';
|
|
12
|
+
case 'PARTIAL':
|
|
13
|
+
return 'FRICTION';
|
|
14
|
+
case 'INSUFFICIENT_DATA':
|
|
15
|
+
return 'DO_NOT_LAUNCH';
|
|
16
|
+
// Synonyms from other components
|
|
17
|
+
case 'PASS':
|
|
18
|
+
case 'SUCCESS':
|
|
19
|
+
return 'READY';
|
|
20
|
+
case 'WARN':
|
|
21
|
+
case 'WARNING':
|
|
22
|
+
case 'FRICTION':
|
|
23
|
+
return 'FRICTION';
|
|
24
|
+
case 'FAIL':
|
|
25
|
+
case 'FAILURE':
|
|
26
|
+
case 'DO_NOT_LAUNCH':
|
|
27
|
+
return 'DO_NOT_LAUNCH';
|
|
28
|
+
default:
|
|
29
|
+
return 'DO_NOT_LAUNCH';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toInternalVerdict(canonicalVerdict) {
|
|
34
|
+
switch (String(canonicalVerdict || '').toUpperCase()) {
|
|
35
|
+
case 'READY':
|
|
36
|
+
return 'OBSERVED';
|
|
37
|
+
case 'FRICTION':
|
|
38
|
+
return 'PARTIAL';
|
|
39
|
+
case 'DO_NOT_LAUNCH':
|
|
40
|
+
return 'INSUFFICIENT_DATA';
|
|
41
|
+
default:
|
|
42
|
+
return 'INSUFFICIENT_DATA';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mapExitCodeFromCanonical(canonicalVerdict) {
|
|
47
|
+
switch (String(canonicalVerdict || '').toUpperCase()) {
|
|
48
|
+
case 'READY':
|
|
49
|
+
return 0;
|
|
50
|
+
case 'FRICTION':
|
|
51
|
+
return 1;
|
|
52
|
+
case 'DO_NOT_LAUNCH':
|
|
53
|
+
default:
|
|
54
|
+
return 2;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { toCanonicalVerdict, toInternalVerdict, mapExitCodeFromCanonical };
|
|
59
|
+
|
|
60
|
+
// Journey scan mapping (SAFE/RISK/DO_NOT_LAUNCH → canonical)
|
|
61
|
+
function toCanonicalJourneyVerdict(journeyVerdict) {
|
|
62
|
+
switch (String(journeyVerdict || '').toUpperCase()) {
|
|
63
|
+
case 'SAFE':
|
|
64
|
+
return 'READY';
|
|
65
|
+
case 'RISK':
|
|
66
|
+
return 'FRICTION';
|
|
67
|
+
case 'DO_NOT_LAUNCH':
|
|
68
|
+
return 'DO_NOT_LAUNCH';
|
|
69
|
+
default:
|
|
70
|
+
return 'DO_NOT_LAUNCH';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports.toCanonicalJourneyVerdict = toCanonicalJourneyVerdict;
|
|
@@ -19,17 +19,13 @@ function isIgnored(filePath, artifactsDir = './artifacts') {
|
|
|
19
19
|
if (artifactsDir) {
|
|
20
20
|
ignorePrefixes.push(path.normalize(artifactsDir));
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
// Ignore any changes within artifacts directory (run outputs)
|
|
23
|
+
return parts.some(p => ignorePrefixes.includes(p));
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
function collectWatchPaths(config) {
|
|
26
27
|
const roots = [
|
|
27
|
-
'
|
|
28
|
-
'guardian.policy.json',
|
|
29
|
-
'guardian.profile.docs.yaml',
|
|
30
|
-
'guardian.profile.ecommerce.yaml',
|
|
31
|
-
'guardian.profile.marketing.yaml',
|
|
32
|
-
'guardian.profile.saas.yaml',
|
|
28
|
+
'config',
|
|
33
29
|
'flows',
|
|
34
30
|
'policies',
|
|
35
31
|
'data',
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ODAVL Guardian Stripe Integration
|
|
3
|
+
* Real Stripe checkout for PRO and BUSINESS plans
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { PLANS } = require('../plans/plan-definitions');
|
|
7
|
+
const { setCurrentPlan } = require('../plans/plan-manager');
|
|
8
|
+
|
|
9
|
+
// Stripe configuration
|
|
10
|
+
const STRIPE_PUBLIC_KEY = process.env.STRIPE_PUBLIC_KEY || '';
|
|
11
|
+
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY || '';
|
|
12
|
+
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET || '';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create Stripe checkout session
|
|
16
|
+
* This is called from the website when user clicks "Upgrade"
|
|
17
|
+
*/
|
|
18
|
+
async function createCheckoutSession(planId, successUrl, cancelUrl) {
|
|
19
|
+
if (!STRIPE_SECRET_KEY) {
|
|
20
|
+
throw new Error('Stripe is not configured. Set STRIPE_SECRET_KEY environment variable.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const stripe = require('stripe')(STRIPE_SECRET_KEY);
|
|
24
|
+
const plan = PLANS[planId.toUpperCase()];
|
|
25
|
+
|
|
26
|
+
if (!plan || !plan.stripePriceId) {
|
|
27
|
+
throw new Error(`Invalid plan: ${planId}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const session = await stripe.checkout.sessions.create({
|
|
31
|
+
mode: 'subscription',
|
|
32
|
+
payment_method_types: ['card'],
|
|
33
|
+
line_items: [
|
|
34
|
+
{
|
|
35
|
+
price: plan.stripePriceId,
|
|
36
|
+
quantity: 1,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
success_url: successUrl,
|
|
40
|
+
cancel_url: cancelUrl,
|
|
41
|
+
metadata: {
|
|
42
|
+
planId: plan.id,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
sessionId: session.id,
|
|
48
|
+
url: session.url,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Handle Stripe webhook events
|
|
54
|
+
* Called when payment succeeds or subscription changes
|
|
55
|
+
*/
|
|
56
|
+
async function handleWebhook(requestBody, signature) {
|
|
57
|
+
if (!STRIPE_SECRET_KEY || !STRIPE_WEBHOOK_SECRET) {
|
|
58
|
+
throw new Error('Stripe webhooks not configured');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const stripe = require('stripe')(STRIPE_SECRET_KEY);
|
|
62
|
+
|
|
63
|
+
let event;
|
|
64
|
+
try {
|
|
65
|
+
event = stripe.webhooks.constructEvent(
|
|
66
|
+
requestBody,
|
|
67
|
+
signature,
|
|
68
|
+
STRIPE_WEBHOOK_SECRET
|
|
69
|
+
);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
throw new Error(`Webhook signature verification failed: ${err.message}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle the event
|
|
75
|
+
switch (event.type) {
|
|
76
|
+
case 'checkout.session.completed': {
|
|
77
|
+
const session = event.data.object;
|
|
78
|
+
const planId = session.metadata.planId;
|
|
79
|
+
const customerId = session.customer;
|
|
80
|
+
const subscriptionId = session.subscription;
|
|
81
|
+
|
|
82
|
+
// Activate the plan
|
|
83
|
+
setCurrentPlan(planId, {
|
|
84
|
+
customerId,
|
|
85
|
+
subscriptionId,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
success: true,
|
|
90
|
+
planId,
|
|
91
|
+
customerId,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case 'customer.subscription.updated':
|
|
96
|
+
case 'customer.subscription.deleted': {
|
|
97
|
+
const subscription = event.data.object;
|
|
98
|
+
const customerId = subscription.customer;
|
|
99
|
+
|
|
100
|
+
if (subscription.status === 'active') {
|
|
101
|
+
// Keep subscription active
|
|
102
|
+
return { success: true, status: 'active' };
|
|
103
|
+
} else if (['canceled', 'unpaid'].includes(subscription.status)) {
|
|
104
|
+
// Downgrade to FREE
|
|
105
|
+
setCurrentPlan('free');
|
|
106
|
+
return { success: true, status: 'downgraded' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { success: true, status: subscription.status };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
default:
|
|
113
|
+
console.log(`Unhandled event type: ${event.type}`);
|
|
114
|
+
return { success: true, ignored: true };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get Stripe public key for client-side
|
|
120
|
+
*/
|
|
121
|
+
function getStripePublicKey() {
|
|
122
|
+
return STRIPE_PUBLIC_KEY;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Create customer portal session
|
|
127
|
+
* Allows users to manage their subscription
|
|
128
|
+
*/
|
|
129
|
+
async function createPortalSession(customerId, returnUrl) {
|
|
130
|
+
if (!STRIPE_SECRET_KEY) {
|
|
131
|
+
throw new Error('Stripe is not configured');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const stripe = require('stripe')(STRIPE_SECRET_KEY);
|
|
135
|
+
|
|
136
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
137
|
+
customer: customerId,
|
|
138
|
+
return_url: returnUrl,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
url: session.url,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Verify Stripe is configured
|
|
148
|
+
*/
|
|
149
|
+
function isStripeConfigured() {
|
|
150
|
+
return !!(STRIPE_SECRET_KEY && STRIPE_PUBLIC_KEY);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get checkout URL for a plan
|
|
155
|
+
* Helper for CLI
|
|
156
|
+
*/
|
|
157
|
+
function getCheckoutUrl(planId) {
|
|
158
|
+
const baseUrl = process.env.GUARDIAN_WEBSITE_URL || 'https://guardian.odavl.com';
|
|
159
|
+
return `${baseUrl}/checkout?plan=${planId}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
createCheckoutSession,
|
|
164
|
+
handleWebhook,
|
|
165
|
+
getStripePublicKey,
|
|
166
|
+
createPortalSession,
|
|
167
|
+
isStripeConfigured,
|
|
168
|
+
getCheckoutUrl,
|
|
169
|
+
};
|