@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
|
@@ -3,7 +3,17 @@
|
|
|
3
3
|
* Phase 3: supports multiple curated attempts
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const DEFAULT_ATTEMPTS = [
|
|
6
|
+
const DEFAULT_ATTEMPTS = [
|
|
7
|
+
'site_smoke',
|
|
8
|
+
'primary_ctas',
|
|
9
|
+
'contact_discovery_v2',
|
|
10
|
+
'contact_form',
|
|
11
|
+
'language_switch',
|
|
12
|
+
'newsletter_signup',
|
|
13
|
+
'signup',
|
|
14
|
+
'login',
|
|
15
|
+
'checkout'
|
|
16
|
+
];
|
|
7
17
|
|
|
8
18
|
const attemptDefinitions = {
|
|
9
19
|
contact_form: {
|
|
@@ -158,6 +168,37 @@ const attemptDefinitions = {
|
|
|
158
168
|
]
|
|
159
169
|
},
|
|
160
170
|
|
|
171
|
+
// Universal Attempts (no instrumentation required)
|
|
172
|
+
site_smoke: {
|
|
173
|
+
id: 'site_smoke',
|
|
174
|
+
name: 'Site Smoke Navigation',
|
|
175
|
+
goal: 'Probe homepage stability and internal navigation',
|
|
176
|
+
riskCategory: 'TRUST/UX',
|
|
177
|
+
source: 'universal',
|
|
178
|
+
baseSteps: [],
|
|
179
|
+
successConditions: []
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
primary_ctas: {
|
|
183
|
+
id: 'primary_ctas',
|
|
184
|
+
name: 'Primary CTA Coverage',
|
|
185
|
+
goal: 'Verify primary CTAs navigate correctly',
|
|
186
|
+
riskCategory: 'LEAD',
|
|
187
|
+
source: 'universal',
|
|
188
|
+
baseSteps: [],
|
|
189
|
+
successConditions: []
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
contact_discovery_v2: {
|
|
193
|
+
id: 'contact_discovery_v2',
|
|
194
|
+
name: 'Contact Discovery v2',
|
|
195
|
+
goal: 'Find contact surfaces (mailto or contact route)',
|
|
196
|
+
riskCategory: 'TRUST/UX',
|
|
197
|
+
source: 'universal',
|
|
198
|
+
baseSteps: [],
|
|
199
|
+
successConditions: []
|
|
200
|
+
},
|
|
201
|
+
|
|
161
202
|
// Universal Reality Pack: deterministic, zero-config safety checks
|
|
162
203
|
universal_reality: {
|
|
163
204
|
id: 'universal_reality',
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attempt Relevance Decision Engine
|
|
3
|
+
* Determines which attempts should run based on site introspection.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Relevance rules mapping attempt IDs to required introspection flags
|
|
8
|
+
*/
|
|
9
|
+
const RELEVANCE_RULES = {
|
|
10
|
+
'contact_form': { requires: 'hasContactForm', reason: 'No contact form detected' },
|
|
11
|
+
'language_switch': { requires: 'hasLanguageSwitch', reason: 'No language switch detected' },
|
|
12
|
+
'signup': { requires: 'hasSignup', reason: 'No signup elements detected' },
|
|
13
|
+
'login': { requires: 'hasLogin', reason: 'No login elements detected' },
|
|
14
|
+
'checkout': { requires: 'hasCheckout', reason: 'No checkout elements detected' },
|
|
15
|
+
'newsletter_signup': { requires: 'hasNewsletter', reason: 'No newsletter signup detected' }
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Filter attempts based on site introspection
|
|
20
|
+
*
|
|
21
|
+
* @param {Array} attempts - Array of attempt objects with {id, ...}
|
|
22
|
+
* @param {Object} introspection - Result from inspectSite()
|
|
23
|
+
* @returns {Object} { toRun: Array, toSkip: Array }
|
|
24
|
+
*/
|
|
25
|
+
function filterAttempts(attempts, introspection) {
|
|
26
|
+
const toRun = [];
|
|
27
|
+
const toSkip = [];
|
|
28
|
+
|
|
29
|
+
for (const attempt of attempts) {
|
|
30
|
+
const rule = RELEVANCE_RULES[attempt.id];
|
|
31
|
+
|
|
32
|
+
if (!rule) {
|
|
33
|
+
// No rule defined: always run (universal attempts)
|
|
34
|
+
toRun.push(attempt);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if introspection flag is true
|
|
39
|
+
const flagValue = introspection[rule.requires];
|
|
40
|
+
|
|
41
|
+
if (flagValue === true) {
|
|
42
|
+
toRun.push(attempt);
|
|
43
|
+
} else {
|
|
44
|
+
toSkip.push({
|
|
45
|
+
attempt: attempt.id,
|
|
46
|
+
reason: rule.reason
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { toRun, toSkip };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if an attempt should be skipped
|
|
56
|
+
*
|
|
57
|
+
* @param {string} attemptId - The attempt ID
|
|
58
|
+
* @param {Object} introspection - Result from inspectSite()
|
|
59
|
+
* @returns {Object|null} Skip info {reason} or null if should run
|
|
60
|
+
*/
|
|
61
|
+
function shouldSkipAttempt(attemptId, introspection) {
|
|
62
|
+
const rule = RELEVANCE_RULES[attemptId];
|
|
63
|
+
|
|
64
|
+
if (!rule) {
|
|
65
|
+
// No rule: never skip
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const flagValue = introspection[rule.requires];
|
|
70
|
+
|
|
71
|
+
if (flagValue === true) {
|
|
72
|
+
return null; // Should run
|
|
73
|
+
} else {
|
|
74
|
+
return { reason: rule.reason };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get human-readable summary of introspection results
|
|
80
|
+
*
|
|
81
|
+
* @param {Object} introspection - Result from inspectSite()
|
|
82
|
+
* @returns {string} Summary text
|
|
83
|
+
*/
|
|
84
|
+
function summarizeIntrospection(introspection) {
|
|
85
|
+
const detected = [];
|
|
86
|
+
|
|
87
|
+
if (introspection.hasLogin) detected.push('login');
|
|
88
|
+
if (introspection.hasSignup) detected.push('signup');
|
|
89
|
+
if (introspection.hasCheckout) detected.push('checkout');
|
|
90
|
+
if (introspection.hasNewsletter) detected.push('newsletter');
|
|
91
|
+
if (introspection.hasContactForm) detected.push('contact form');
|
|
92
|
+
if (introspection.hasLanguageSwitch) detected.push('language switch');
|
|
93
|
+
|
|
94
|
+
if (detected.length === 0) {
|
|
95
|
+
return 'No specific capabilities detected';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return `Detected: ${detected.join(', ')}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
RELEVANCE_RULES,
|
|
103
|
+
filterAttempts,
|
|
104
|
+
shouldSkipAttempt,
|
|
105
|
+
summarizeIntrospection
|
|
106
|
+
};
|
package/src/guardian/attempt.js
CHANGED
|
@@ -132,6 +132,28 @@ async function executeAttempt(config) {
|
|
|
132
132
|
const htmlPath = reporter.saveHtmlReport(htmlContent, runDir);
|
|
133
133
|
log(`✅ HTML report: ${path.basename(htmlPath)}`);
|
|
134
134
|
|
|
135
|
+
// Persist deterministic attempt evidence (attempt.json + steps log)
|
|
136
|
+
const attemptJsonPath = path.join(runDir, 'attempt.json');
|
|
137
|
+
const attemptSnapshot = {
|
|
138
|
+
attemptId,
|
|
139
|
+
baseUrl,
|
|
140
|
+
outcome: attemptResult.outcome,
|
|
141
|
+
startedAt: attemptResult.startedAt,
|
|
142
|
+
endedAt: attemptResult.endedAt,
|
|
143
|
+
totalDurationMs: attemptResult.totalDurationMs,
|
|
144
|
+
friction: attemptResult.friction,
|
|
145
|
+
validators: attemptResult.validators,
|
|
146
|
+
softFailures: attemptResult.softFailures,
|
|
147
|
+
successReason: attemptResult.successReason,
|
|
148
|
+
error: attemptResult.error,
|
|
149
|
+
steps: attemptResult.steps
|
|
150
|
+
};
|
|
151
|
+
fs.writeFileSync(attemptJsonPath, JSON.stringify(attemptSnapshot, null, 2));
|
|
152
|
+
|
|
153
|
+
const stepsLogPath = path.join(runDir, 'steps.jsonl');
|
|
154
|
+
const stepsLog = (attemptResult.steps || []).map(s => JSON.stringify(s)).join('\n');
|
|
155
|
+
fs.writeFileSync(stepsLogPath, stepsLog + (stepsLog ? '\n' : ''));
|
|
156
|
+
|
|
135
157
|
// Display summary
|
|
136
158
|
log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
137
159
|
|
|
@@ -204,6 +226,8 @@ async function executeAttempt(config) {
|
|
|
204
226
|
artifactsDir: runDir,
|
|
205
227
|
reportJsonPath: path.join(runDir, 'attempt-report.json'),
|
|
206
228
|
reportHtmlPath: path.join(runDir, 'attempt-report.html'),
|
|
229
|
+
attemptJsonPath,
|
|
230
|
+
stepsLogPath,
|
|
207
231
|
tracePath: enableTrace ? path.join(runDir, 'trace.zip') : null,
|
|
208
232
|
steps: attemptResult.steps,
|
|
209
233
|
friction: attemptResult.friction,
|
package/src/guardian/baseline.js
CHANGED
|
@@ -354,8 +354,9 @@ async function checkBaseline(options) {
|
|
|
354
354
|
|
|
355
355
|
// If baseline is not available, skip comparison and return early
|
|
356
356
|
if (!baseline || baselineStatus !== 'LOADED') {
|
|
357
|
+
const currentExitCode = typeof current.exitCode === 'number' ? current.exitCode : 0;
|
|
357
358
|
return {
|
|
358
|
-
exitCode:
|
|
359
|
+
exitCode: currentExitCode,
|
|
359
360
|
runDir: current.runDir,
|
|
360
361
|
overallRegressionVerdict: baselineStatus === 'NO_BASELINE' ? 'NO_BASELINE' : 'BASELINE_UNUSABLE',
|
|
361
362
|
comparisons: [],
|
|
@@ -479,10 +480,17 @@ async function checkBaseline(options) {
|
|
|
479
480
|
}
|
|
480
481
|
}
|
|
481
482
|
|
|
482
|
-
//
|
|
483
|
-
|
|
483
|
+
// Exit code reflects regression severity while preserving underlying run exit codes
|
|
484
|
+
const currentExitCode = typeof current.exitCode === 'number' ? current.exitCode : 0;
|
|
485
|
+
let exitCode = currentExitCode;
|
|
486
|
+
if (overallRegressionVerdict === 'REGRESSION_FAILURE') {
|
|
487
|
+
exitCode = Math.max(exitCode, 4);
|
|
488
|
+
} else if (overallRegressionVerdict === 'REGRESSION_FRICTION') {
|
|
489
|
+
exitCode = Math.max(exitCode, 3);
|
|
490
|
+
}
|
|
491
|
+
|
|
484
492
|
return {
|
|
485
|
-
exitCode
|
|
493
|
+
exitCode,
|
|
486
494
|
runDir: current.runDir,
|
|
487
495
|
reportJsonPath: jsonPath,
|
|
488
496
|
reportHtmlPath: htmlPath,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { scanJourney } = require('./journey-scanner');
|
|
4
|
+
const { detectSiteIntent, selectJourneyByIntent } = require('./intent-detector');
|
|
5
|
+
const { buildBaselineFromJourneyResult, compareAgainstBaseline, classifySeverity } = require('./drift-detector');
|
|
6
|
+
const { shouldEmitAlert } = require('./alert-ledger');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Run CI gate scan:
|
|
10
|
+
* - Single journey run
|
|
11
|
+
* - Compare against baseline if exists
|
|
12
|
+
* - Apply alert dedup + cooldown
|
|
13
|
+
* - Generate ci-result.json
|
|
14
|
+
* - Exit with stable codes
|
|
15
|
+
*/
|
|
16
|
+
async function runCIGate(config) {
|
|
17
|
+
const { baseUrl, artifactsDir, headless, timeout, preset, presetProvided } = config;
|
|
18
|
+
|
|
19
|
+
const outDir = path.resolve(artifactsDir, 'ci');
|
|
20
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
21
|
+
|
|
22
|
+
console.log(`[CI] Scanning ${baseUrl}...`);
|
|
23
|
+
|
|
24
|
+
let selectedPreset = preset;
|
|
25
|
+
let siteIntent = null;
|
|
26
|
+
|
|
27
|
+
// Auto-detect intent if no preset provided
|
|
28
|
+
if (!presetProvided) {
|
|
29
|
+
console.log('[CI] Auto-detecting site intent...');
|
|
30
|
+
try {
|
|
31
|
+
siteIntent = await detectSiteIntent(baseUrl, { headless, timeout });
|
|
32
|
+
selectedPreset = selectJourneyByIntent(siteIntent);
|
|
33
|
+
console.log(`[CI] Detected intent: ${siteIntent.type}, selected preset: ${selectedPreset}`);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('[CI] Intent detection failed:', err.message);
|
|
36
|
+
selectedPreset = 'saas';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Run journey
|
|
41
|
+
let result;
|
|
42
|
+
try {
|
|
43
|
+
result = await scanJourney({ baseUrl, preset: selectedPreset, headless, timeout });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error('[CI] Journey scan failed:', err.message);
|
|
46
|
+
const ciResult = {
|
|
47
|
+
decision: 'DO_NOT_LAUNCH',
|
|
48
|
+
severity: 'CRITICAL',
|
|
49
|
+
driftDetected: false,
|
|
50
|
+
goalReached: false,
|
|
51
|
+
intent: siteIntent ? siteIntent.type : 'unknown',
|
|
52
|
+
reasons: ['Journey scan failed: ' + err.message],
|
|
53
|
+
timestamp: new Date().toISOString()
|
|
54
|
+
};
|
|
55
|
+
fs.writeFileSync(path.join(outDir, 'ci-result.json'), JSON.stringify(ciResult, null, 2));
|
|
56
|
+
return 2;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check for baseline
|
|
60
|
+
const baselinePath = path.resolve(artifactsDir, 'baseline.json');
|
|
61
|
+
let driftInfo = null;
|
|
62
|
+
let severity = 'WARN';
|
|
63
|
+
|
|
64
|
+
if (fs.existsSync(baselinePath)) {
|
|
65
|
+
console.log('[CI] Comparing against baseline...');
|
|
66
|
+
const baselineData = JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
|
|
67
|
+
driftInfo = compareAgainstBaseline(result, baselineData);
|
|
68
|
+
|
|
69
|
+
if (driftInfo.hasDrift) {
|
|
70
|
+
severity = classifySeverity(driftInfo, result);
|
|
71
|
+
console.log(`[CI] Drift detected (${severity}): ${driftInfo.reasons.join(', ')}`);
|
|
72
|
+
|
|
73
|
+
// Check alert dedup and cooldown
|
|
74
|
+
const alertDecision = shouldEmitAlert(driftInfo.reasons, severity, artifactsDir);
|
|
75
|
+
if (!alertDecision.emit) {
|
|
76
|
+
console.log(`[CI] Alert suppressed: ${alertDecision.reason}`);
|
|
77
|
+
// Generate CI result showing suppression
|
|
78
|
+
const ciResult = {
|
|
79
|
+
decision: result.decision,
|
|
80
|
+
severity,
|
|
81
|
+
driftDetected: true,
|
|
82
|
+
goalReached: result.goalReached || false,
|
|
83
|
+
intent: result.intent || 'unknown',
|
|
84
|
+
reasons: driftInfo.reasons,
|
|
85
|
+
alertSuppressed: true,
|
|
86
|
+
suppressionReason: alertDecision.reason,
|
|
87
|
+
timestamp: new Date().toISOString()
|
|
88
|
+
};
|
|
89
|
+
fs.writeFileSync(path.join(outDir, 'ci-result.json'), JSON.stringify(ciResult, null, 2));
|
|
90
|
+
return severity === 'CRITICAL' ? 4 : 0;
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
console.log('[CI] No drift detected');
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
console.log('[CI] No baseline found, capturing new baseline...');
|
|
97
|
+
const baseline = buildBaselineFromJourneyResult(result);
|
|
98
|
+
fs.writeFileSync(baselinePath, JSON.stringify(baseline, null, 2));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Generate ci-result.json
|
|
102
|
+
const ciResult = {
|
|
103
|
+
decision: result.decision,
|
|
104
|
+
severity: driftInfo ? severity : (result.decision === 'DO_NOT_LAUNCH' ? 'CRITICAL' : 'WARN'),
|
|
105
|
+
driftDetected: driftInfo ? driftInfo.hasDrift : false,
|
|
106
|
+
goalReached: result.goalReached || false,
|
|
107
|
+
intent: result.intent || 'unknown',
|
|
108
|
+
reasons: driftInfo ? driftInfo.reasons : [],
|
|
109
|
+
timestamp: new Date().toISOString()
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
fs.writeFileSync(path.join(outDir, 'ci-result.json'), JSON.stringify(ciResult, null, 2));
|
|
113
|
+
console.log(`[CI] Result: ${ciResult.decision} (${ciResult.severity})`);
|
|
114
|
+
|
|
115
|
+
// Exit code based on decision and severity
|
|
116
|
+
if (ciResult.decision === 'DO_NOT_LAUNCH') return 2;
|
|
117
|
+
if (ciResult.decision === 'RISK' || ciResult.severity === 'WARN') return 1;
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { runCIGate };
|
|
@@ -17,8 +17,11 @@ function formatCiSummary({ flowResults = [], diffResult = null, baselineCreated
|
|
|
17
17
|
lines.push('CI MODE: ON');
|
|
18
18
|
lines.push(formatRunSummary({ flowResults, diffResult, baselineCreated, exitCode }, { label: 'Summary' }));
|
|
19
19
|
|
|
20
|
+
const verdict = exitCode === 0 ? 'OBSERVED' : exitCode === 1 ? 'PARTIAL' : 'INSUFFICIENT_DATA';
|
|
21
|
+
lines.push(`Result: ${verdict}`);
|
|
22
|
+
|
|
20
23
|
if (exitCode !== 0) {
|
|
21
|
-
lines.push('
|
|
24
|
+
lines.push('Observed issues:');
|
|
22
25
|
const troubled = flowResults.filter(f => f.outcome === 'FAILURE' || f.outcome === 'FRICTION');
|
|
23
26
|
troubled.slice(0, maxReasons).forEach(flow => {
|
|
24
27
|
const reason = pickReason(flow);
|
|
@@ -27,8 +30,6 @@ function formatCiSummary({ flowResults = [], diffResult = null, baselineCreated
|
|
|
27
30
|
if (troubled.length > maxReasons) {
|
|
28
31
|
lines.push(` - … ${troubled.length - maxReasons} more issues`);
|
|
29
32
|
}
|
|
30
|
-
} else {
|
|
31
|
-
lines.push('Result: PASS');
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
return lines.join('\n');
|
|
@@ -1,108 +1,95 @@
|
|
|
1
1
|
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* Shows critical info, top risks, and actionable next steps.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Generate final CLI summary
|
|
10
|
-
* @param {object} snapshot - Guardian snapshot
|
|
11
|
-
* @param {object} policyEval - Policy evaluation result
|
|
12
|
-
* @param {object} baselineCheckResult - Optional baseline check result
|
|
13
|
-
* @returns {string} Formatted CLI summary
|
|
14
|
-
*/
|
|
15
|
-
function generateCliSummary(snapshot, policyEval, baselineCheckResult) {
|
|
16
|
-
if (!snapshot) {
|
|
17
|
-
return 'No snapshot data available.';
|
|
18
|
-
}
|
|
19
|
-
|
|
2
|
+
// STRICT CLI SUMMARY: factual, artifact-traceable lines only
|
|
3
|
+
function generateCliSummary(snapshot, policyEval, baselineCheckResult, options = {}) {
|
|
4
|
+
if (!snapshot) return 'No snapshot data available.';
|
|
20
5
|
const meta = snapshot.meta || {};
|
|
21
|
-
const
|
|
22
|
-
const counts =
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const discovery = snapshot.discovery || {};
|
|
6
|
+
const coverage = snapshot.coverage || {};
|
|
7
|
+
const counts = coverage.counts || {};
|
|
8
|
+
const evidence = snapshot.evidenceMetrics || {};
|
|
9
|
+
const resolved = snapshot.resolved || {};
|
|
26
10
|
|
|
27
11
|
let output = '\n';
|
|
28
12
|
output += '━'.repeat(70) + '\n';
|
|
29
13
|
output += '🛡️ Guardian Reality Summary\n';
|
|
30
14
|
output += '━'.repeat(70) + '\n\n';
|
|
31
15
|
|
|
32
|
-
// Target URL
|
|
33
16
|
output += `Target: ${meta.url || 'unknown'}\n`;
|
|
34
17
|
output += `Run ID: ${meta.runId || 'unknown'}\n\n`;
|
|
35
18
|
|
|
36
|
-
|
|
37
|
-
output += '
|
|
38
|
-
output += `
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
output += '\n
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
19
|
+
const pe = snapshot.policyEvaluation || {};
|
|
20
|
+
output += `Policy Verdict: ${meta.result || (pe.passed ? 'PASSED' : pe.exitCode === 2 ? 'WARN' : 'FAILED')}\n`;
|
|
21
|
+
output += `Exit Code: ${pe.exitCode ?? 'unknown'}\n`;
|
|
22
|
+
|
|
23
|
+
const planned = coverage.total ?? (resolved.coverage?.total) ?? 'unknown';
|
|
24
|
+
const executed = counts.executedCount ?? (resolved.coverage?.executedCount) ?? coverage.executed ?? 'unknown';
|
|
25
|
+
output += `Executed / Planned: ${executed} / ${planned}\n`;
|
|
26
|
+
|
|
27
|
+
const completeness = evidence.completeness ?? resolved.evidenceMetrics?.completeness ?? 'unknown';
|
|
28
|
+
const integrity = evidence.integrity ?? resolved.evidenceMetrics?.integrity ?? 'unknown';
|
|
29
|
+
output += `Coverage Completeness: ${typeof completeness === 'number' ? completeness.toFixed(4) : completeness}\n`;
|
|
30
|
+
output += `Evidence Integrity: ${typeof integrity === 'number' ? integrity.toFixed(4) : integrity}\n`;
|
|
31
|
+
|
|
32
|
+
if (meta.attestation?.hash) {
|
|
33
|
+
output += `Attestation: ${meta.attestation.hash}\n`;
|
|
34
|
+
// STRICT CLI SUMMARY: factual, artifact-traceable lines only
|
|
35
|
+
function generateCliSummary(snapshot, policyEval, baselineCheckResult, options = {}) {
|
|
36
|
+
if (!snapshot) return 'No snapshot data available.';
|
|
37
|
+
const meta = snapshot.meta || {};
|
|
38
|
+
const coverage = snapshot.coverage || {};
|
|
39
|
+
const counts = coverage.counts || {};
|
|
40
|
+
const evidence = snapshot.evidenceMetrics || {};
|
|
41
|
+
const resolved = snapshot.resolved || {};
|
|
42
|
+
|
|
43
|
+
let output = '\n';
|
|
44
|
+
output += '━'.repeat(70) + '\n';
|
|
45
|
+
output += '🛡️ Guardian Reality Summary\n';
|
|
46
|
+
output += '━'.repeat(70) + '\n\n';
|
|
47
|
+
|
|
48
|
+
output += `Target: ${meta.url || 'unknown'}\n`;
|
|
49
|
+
output += `Run ID: ${meta.runId || 'unknown'}\n\n`;
|
|
50
|
+
|
|
51
|
+
const pe = snapshot.policyEvaluation || {};
|
|
52
|
+
output += `Policy Verdict: ${meta.result || (pe.passed ? 'PASSED' : pe.exitCode === 2 ? 'WARN' : 'FAILED')}\n`;
|
|
53
|
+
output += `Exit Code: ${pe.exitCode ?? 'unknown'}\n`;
|
|
54
|
+
|
|
55
|
+
const planned = coverage.total ?? (resolved.coverage?.total) ?? 'unknown';
|
|
56
|
+
const executed = counts.executedCount ?? (resolved.coverage?.executedCount) ?? coverage.executed ?? 'unknown';
|
|
57
|
+
output += `Executed / Planned: ${executed} / ${planned}\n`;
|
|
58
|
+
|
|
59
|
+
const completeness = evidence.completeness ?? resolved.evidenceMetrics?.completeness ?? 'unknown';
|
|
60
|
+
const integrity = evidence.integrity ?? resolved.evidenceMetrics?.integrity ?? 'unknown';
|
|
61
|
+
output += `Coverage Completeness: ${typeof completeness === 'number' ? completeness.toFixed(4) : completeness}\n`;
|
|
62
|
+
output += `Evidence Integrity: ${typeof integrity === 'number' ? integrity.toFixed(4) : integrity}\n`;
|
|
63
|
+
|
|
64
|
+
if (meta.attestation?.hash) {
|
|
65
|
+
output += `Attestation: ${meta.attestation.hash}\n`;
|
|
67
66
|
}
|
|
68
|
-
output += '\n';
|
|
69
|
-
}
|
|
70
67
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
// Audit Summary
|
|
69
|
+
const executedAttempts = (snapshot.attempts || []).filter(a => a.executed).map(a => a.attemptId);
|
|
70
|
+
output += '\nAudit Summary:\n';
|
|
71
|
+
output += ` Tested (${executedAttempts.length}): ${executedAttempts.join(', ') || 'none'}\n`;
|
|
72
|
+
const skippedDisabled = (coverage.skippedDisabledByPreset || []).map(s => s.attempt);
|
|
73
|
+
const skippedUserFiltered = (coverage.skippedUserFiltered || []).map(s => s.attempt);
|
|
74
|
+
const skippedNotApplicable = (coverage.skippedNotApplicable || []).map(s => s.attempt);
|
|
75
|
+
const skippedMissing = (coverage.skippedMissing || []).map(s => s.attempt);
|
|
76
|
+
output += ` Not Tested — DisabledByPreset (${skippedDisabled.length}): ${skippedDisabled.join(', ') || 'none'}\n`;
|
|
77
|
+
output += ` Not Tested — UserFiltered (${skippedUserFiltered.length}): ${skippedUserFiltered.join(', ') || 'none'}\n`;
|
|
78
|
+
output += ` Not Tested — NotApplicable (${skippedNotApplicable.length}): ${skippedNotApplicable.join(', ') || 'none'}\n`;
|
|
79
|
+
output += ` Not Tested — Missing (${skippedMissing.length}): ${skippedMissing.join(', ') || 'none'}\n`;
|
|
80
|
+
|
|
81
|
+
const reasons = Array.isArray(pe.reasons) ? pe.reasons : [];
|
|
82
|
+
if (reasons.length > 0) {
|
|
83
|
+
output += '\nPolicy Reasons:\n';
|
|
84
|
+
reasons.forEach(r => {
|
|
85
|
+
if (typeof r === 'string') output += ` • ${r}\n`; else if (r.message) output += ` • ${r.message}\n`; else output += ` • ${JSON.stringify(r)}\n`;
|
|
86
|
+
});
|
|
83
87
|
}
|
|
84
|
-
output += '\n\n';
|
|
85
|
-
}
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
output += '🚦 Submit Outcomes:\n';
|
|
92
|
-
flowsWithEval.slice(0, 5).forEach(f => {
|
|
93
|
-
const status = (f.successEval.status || 'unknown').toUpperCase();
|
|
94
|
-
const confidence = f.successEval.confidence || 'low';
|
|
95
|
-
output += ` ${f.flowName}: ${status} (confidence: ${confidence})\n`;
|
|
96
|
-
const reasons = (f.successEval.reasons || []).slice(0, 3);
|
|
97
|
-
if (reasons.length) {
|
|
98
|
-
output += ' Reasons:\n';
|
|
99
|
-
reasons.forEach(r => { output += ` - ${r}\n`; });
|
|
100
|
-
}
|
|
101
|
-
// Compact evidence summary
|
|
102
|
-
const ev = f.successEval.evidence || {};
|
|
103
|
-
const net = Array.isArray(ev.network) ? ev.network : [];
|
|
104
|
-
const primary = net.find(n => (n.method === 'POST' || n.method === 'PUT') && n.status != null) || net[0];
|
|
105
|
-
const reqLine = (() => {
|
|
89
|
+
output += '\n📁 Full report: ' + (meta.runId ? `artifacts/${meta.runId}/` : 'See artifacts/') + '\n\n';
|
|
90
|
+
output += '━'.repeat(70) + '\n';
|
|
91
|
+
return output;
|
|
92
|
+
}
|
|
106
93
|
if (!primary) return null;
|
|
107
94
|
try { const p = new URL(primary.url); return `request: ${primary.method} ${p.pathname} → ${primary.status}`; }
|
|
108
95
|
catch { return `request: ${primary.method} ${primary.url} → ${primary.status}`; }
|
|
@@ -243,8 +230,8 @@ function generateCliSummary(snapshot, policyEval, baselineCheckResult) {
|
|
|
243
230
|
/**
|
|
244
231
|
* Print summary to console
|
|
245
232
|
*/
|
|
246
|
-
function printCliSummary(snapshot, policyEval, baselineCheckResult) {
|
|
247
|
-
const summary = generateCliSummary(snapshot, policyEval, baselineCheckResult);
|
|
233
|
+
function printCliSummary(snapshot, policyEval, baselineCheckResult, options = {}) {
|
|
234
|
+
const summary = generateCliSummary(snapshot, policyEval, baselineCheckResult, options);
|
|
248
235
|
console.log(summary);
|
|
249
236
|
}
|
|
250
237
|
|