@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/README.md +155 -97
  3. package/bin/guardian.js +1345 -60
  4. package/config/README.md +59 -0
  5. package/config/profiles/landing-demo.yaml +16 -0
  6. package/package.json +21 -11
  7. package/policies/landing-demo.json +22 -0
  8. package/src/enterprise/audit-logger.js +166 -0
  9. package/src/enterprise/pdf-exporter.js +267 -0
  10. package/src/enterprise/rbac-gate.js +142 -0
  11. package/src/enterprise/rbac.js +239 -0
  12. package/src/enterprise/site-manager.js +180 -0
  13. package/src/founder/feedback-system.js +156 -0
  14. package/src/founder/founder-tracker.js +213 -0
  15. package/src/founder/usage-signals.js +141 -0
  16. package/src/guardian/alert-ledger.js +121 -0
  17. package/src/guardian/attempt-engine.js +568 -7
  18. package/src/guardian/attempt-registry.js +42 -1
  19. package/src/guardian/attempt-relevance.js +106 -0
  20. package/src/guardian/attempt.js +24 -0
  21. package/src/guardian/baseline.js +12 -4
  22. package/src/guardian/breakage-intelligence.js +1 -0
  23. package/src/guardian/ci-cli.js +121 -0
  24. package/src/guardian/ci-output.js +4 -3
  25. package/src/guardian/cli-summary.js +79 -92
  26. package/src/guardian/config-loader.js +162 -0
  27. package/src/guardian/drift-detector.js +100 -0
  28. package/src/guardian/enhanced-html-reporter.js +221 -4
  29. package/src/guardian/env-guard.js +127 -0
  30. package/src/guardian/failure-intelligence.js +173 -0
  31. package/src/guardian/first-run-profile.js +89 -0
  32. package/src/guardian/first-run.js +6 -1
  33. package/src/guardian/flag-validator.js +17 -3
  34. package/src/guardian/html-reporter.js +2 -0
  35. package/src/guardian/human-reporter.js +431 -0
  36. package/src/guardian/index.js +22 -19
  37. package/src/guardian/init-command.js +9 -5
  38. package/src/guardian/intent-detector.js +146 -0
  39. package/src/guardian/journey-definitions.js +132 -0
  40. package/src/guardian/journey-scan-cli.js +145 -0
  41. package/src/guardian/journey-scanner.js +583 -0
  42. package/src/guardian/junit-reporter.js +18 -1
  43. package/src/guardian/live-cli.js +95 -0
  44. package/src/guardian/live-scheduler-runner.js +137 -0
  45. package/src/guardian/live-scheduler.js +146 -0
  46. package/src/guardian/market-reporter.js +341 -81
  47. package/src/guardian/pattern-analyzer.js +348 -0
  48. package/src/guardian/policy.js +80 -3
  49. package/src/guardian/preset-loader.js +9 -6
  50. package/src/guardian/reality.js +1278 -117
  51. package/src/guardian/reporter.js +27 -41
  52. package/src/guardian/run-artifacts.js +212 -0
  53. package/src/guardian/run-cleanup.js +207 -0
  54. package/src/guardian/run-latest.js +90 -0
  55. package/src/guardian/run-list.js +211 -0
  56. package/src/guardian/scan-presets.js +100 -11
  57. package/src/guardian/selector-fallbacks.js +394 -0
  58. package/src/guardian/semantic-contact-finder.js +2 -1
  59. package/src/guardian/site-introspection.js +257 -0
  60. package/src/guardian/smoke.js +2 -2
  61. package/src/guardian/snapshot-schema.js +25 -1
  62. package/src/guardian/snapshot.js +46 -2
  63. package/src/guardian/stability-scorer.js +169 -0
  64. package/src/guardian/template-command.js +184 -0
  65. package/src/guardian/text-formatters.js +426 -0
  66. package/src/guardian/verdict.js +320 -0
  67. package/src/guardian/verdicts.js +74 -0
  68. package/src/guardian/watch-runner.js +3 -7
  69. package/src/payments/stripe-checkout.js +169 -0
  70. package/src/plans/plan-definitions.js +148 -0
  71. package/src/plans/plan-manager.js +211 -0
  72. package/src/plans/usage-tracker.js +210 -0
  73. package/src/recipes/recipe-engine.js +188 -0
  74. package/src/recipes/recipe-failure-analysis.js +159 -0
  75. package/src/recipes/recipe-registry.js +134 -0
  76. package/src/recipes/recipe-runtime.js +507 -0
  77. package/src/recipes/recipe-store.js +410 -0
  78. package/guardian-contract-v1.md +0 -149
  79. /package/{guardian.config.json → config/guardian.config.json} +0 -0
  80. /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
  81. /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
  82. /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
  83. /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
  84. /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 = ['contact_form', 'language_switch', 'newsletter_signup', 'signup', 'login', 'checkout'];
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
+ };
@@ -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,
@@ -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: 0, // Exit code based on flows only, not baseline
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
- // CRITICAL: Exit code is ALWAYS 0 from baseline check
483
- // Exit codes come from flow outcomes only (Phase 2.x policy)
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: 0,
493
+ exitCode,
486
494
  runDir: current.runDir,
487
495
  reportJsonPath: jsonPath,
488
496
  reportHtmlPath: htmlPath,
@@ -34,6 +34,7 @@ function analyzeFailure(item, isFlow = false) {
34
34
  id: isFlow ? item.flowId : item.attemptId,
35
35
  name: isFlow ? item.flowName : item.attemptName,
36
36
  outcome: item.outcome,
37
+ source: isFlow ? 'flow' : 'attempt',
37
38
  breakType,
38
39
  domain,
39
40
  severity,
@@ -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('Why CI failed:');
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
- * CLI Summary Module
3
- *
4
- * Generate human-friendly CLI summaries at the end of Guardian runs.
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 marketImpact = snapshot.marketImpactSummary || {};
22
- const counts = marketImpact.countsBySeverity || { CRITICAL: 0, WARNING: 0, INFO: 0 };
23
- const topRisks = marketImpact.topRisks || [];
24
- const attempts = snapshot.attempts || [];
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
- // Risk Counts
37
- output += '📊 Risk Summary:\n';
38
- output += ` 🚨 CRITICAL: ${counts.CRITICAL}`;
39
- if (counts.CRITICAL > 0) output += ' (Revenue impact)';
40
- output += '\n';
41
- output += ` ⚠️ WARNING: ${counts.WARNING}`;
42
- if (counts.WARNING > 0) output += ' (User experience)';
43
- output += '\n';
44
- output += ` ℹ️ INFO: ${counts.INFO}`;
45
- if (counts.INFO > 0) output += ' (Minor issues)';
46
- output += '\n\n';
47
-
48
- // Top Risks (up to 3)
49
- if (topRisks.length > 0) {
50
- // Preserve legacy label for backward compatibility
51
- output += '🔥 Top Risk:\n';
52
- // New: compact list of top 3 issues
53
- output += ' (Top Issues)\n';
54
- topRisks.slice(0, 3).forEach((risk, idx) => {
55
- output += ` ${idx + 1}. ${risk.humanReadableReason || 'Unknown issue'}\n`;
56
- output += ` Impact: ${risk.impactScore || 0} (${risk.category || 'UNKNOWN'}) | Severity: ${risk.severity || 'INFO'}\n`;
57
- });
58
-
59
- // Link evidence if available for the top-most
60
- const topRisk = topRisks[0];
61
- const relatedAttempt = attempts.find(a =>
62
- a.attemptId === topRisk.attemptId ||
63
- (topRisk.humanReadableReason || '').toLowerCase().includes(a.attemptName?.toLowerCase() || '')
64
- );
65
- if (relatedAttempt && relatedAttempt.evidence) {
66
- output += ` Evidence: ${relatedAttempt.evidence.screenshotPath || 'See report'}\n`;
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
- // Attempt Summary
72
- // Phase 7.4: Include SKIPPED in summary
73
- const successfulAttempts = attempts.filter(a => a.outcome === 'SUCCESS').length;
74
- const skippedAttempts = attempts.filter(a => a.outcome === 'SKIPPED').length;
75
- const totalAttempts = attempts.length;
76
- if (totalAttempts > 0) {
77
- output += '🎯 Attempts:\n';
78
- output += ` ${successfulAttempts}/${totalAttempts} successful`;
79
- if (successfulAttempts < totalAttempts) {
80
- const failed = totalAttempts - successfulAttempts - skippedAttempts;
81
- if (failed > 0) output += ` (${failed} failed)`;
82
- if (skippedAttempts > 0) output += ` (${skippedAttempts} skipped)`;
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
- // Flow Submit Outcomes (Wave 1.3)
88
- const flows = snapshot.flows || [];
89
- const flowsWithEval = flows.filter(f => f.successEval);
90
- if (flowsWithEval.length > 0) {
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