@odavl/guardian 0.1.0-rc1 → 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 (101) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +155 -97
  3. package/bin/guardian.js +1544 -55
  4. package/config/README.md +59 -0
  5. package/config/profiles/landing-demo.yaml +16 -0
  6. package/package.json +26 -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 +587 -12
  18. package/src/guardian/attempt-registry.js +42 -1
  19. package/src/guardian/attempt-relevance.js +106 -0
  20. package/src/guardian/attempt.js +85 -39
  21. package/src/guardian/attempts-filter.js +63 -0
  22. package/src/guardian/baseline.js +50 -8
  23. package/src/guardian/breakage-intelligence.js +1 -0
  24. package/src/guardian/browser-pool.js +131 -0
  25. package/src/guardian/browser.js +28 -1
  26. package/src/guardian/ci-cli.js +121 -0
  27. package/src/guardian/ci-mode.js +15 -0
  28. package/src/guardian/ci-output.js +38 -0
  29. package/src/guardian/cli-summary.js +167 -67
  30. package/src/guardian/config-loader.js +162 -0
  31. package/src/guardian/data-guardian-detector.js +189 -0
  32. package/src/guardian/detection-layers.js +271 -0
  33. package/src/guardian/drift-detector.js +100 -0
  34. package/src/guardian/enhanced-html-reporter.js +221 -4
  35. package/src/guardian/env-guard.js +127 -0
  36. package/src/guardian/failure-intelligence.js +173 -0
  37. package/src/guardian/first-run-profile.js +89 -0
  38. package/src/guardian/first-run.js +54 -0
  39. package/src/guardian/flag-validator.js +111 -0
  40. package/src/guardian/flow-executor.js +309 -44
  41. package/src/guardian/html-reporter.js +2 -0
  42. package/src/guardian/human-reporter.js +431 -0
  43. package/src/guardian/index.js +22 -19
  44. package/src/guardian/init-command.js +9 -5
  45. package/src/guardian/intent-detector.js +146 -0
  46. package/src/guardian/journey-definitions.js +132 -0
  47. package/src/guardian/journey-scan-cli.js +145 -0
  48. package/src/guardian/journey-scanner.js +583 -0
  49. package/src/guardian/junit-reporter.js +18 -1
  50. package/src/guardian/language-detection.js +99 -0
  51. package/src/guardian/live-cli.js +95 -0
  52. package/src/guardian/live-scheduler-runner.js +137 -0
  53. package/src/guardian/live-scheduler.js +146 -0
  54. package/src/guardian/market-reporter.js +357 -82
  55. package/src/guardian/parallel-executor.js +116 -0
  56. package/src/guardian/pattern-analyzer.js +348 -0
  57. package/src/guardian/policy.js +80 -3
  58. package/src/guardian/prerequisite-checker.js +101 -0
  59. package/src/guardian/preset-loader.js +27 -18
  60. package/src/guardian/profile-loader.js +96 -0
  61. package/src/guardian/reality.js +1612 -115
  62. package/src/guardian/reporter.js +27 -41
  63. package/src/guardian/run-artifacts.js +212 -0
  64. package/src/guardian/run-cleanup.js +207 -0
  65. package/src/guardian/run-latest.js +90 -0
  66. package/src/guardian/run-list.js +211 -0
  67. package/src/guardian/run-summary.js +20 -0
  68. package/src/guardian/scan-presets.js +100 -11
  69. package/src/guardian/selector-fallbacks.js +394 -0
  70. package/src/guardian/semantic-contact-detection.js +255 -0
  71. package/src/guardian/semantic-contact-finder.js +201 -0
  72. package/src/guardian/semantic-targets.js +234 -0
  73. package/src/guardian/site-introspection.js +257 -0
  74. package/src/guardian/smoke.js +258 -0
  75. package/src/guardian/snapshot-schema.js +25 -1
  76. package/src/guardian/snapshot.js +69 -3
  77. package/src/guardian/stability-scorer.js +169 -0
  78. package/src/guardian/success-evaluator.js +214 -0
  79. package/src/guardian/template-command.js +184 -0
  80. package/src/guardian/text-formatters.js +426 -0
  81. package/src/guardian/timeout-profiles.js +57 -0
  82. package/src/guardian/verdict.js +320 -0
  83. package/src/guardian/verdicts.js +74 -0
  84. package/src/guardian/wait-for-outcome.js +120 -0
  85. package/src/guardian/watch-runner.js +181 -0
  86. package/src/payments/stripe-checkout.js +169 -0
  87. package/src/plans/plan-definitions.js +148 -0
  88. package/src/plans/plan-manager.js +211 -0
  89. package/src/plans/usage-tracker.js +210 -0
  90. package/src/recipes/recipe-engine.js +188 -0
  91. package/src/recipes/recipe-failure-analysis.js +159 -0
  92. package/src/recipes/recipe-registry.js +134 -0
  93. package/src/recipes/recipe-runtime.js +507 -0
  94. package/src/recipes/recipe-store.js +410 -0
  95. package/guardian-contract-v1.md +0 -149
  96. /package/{guardian.config.json → config/guardian.config.json} +0 -0
  97. /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
  98. /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
  99. /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
  100. /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
  101. /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
package/bin/guardian.js CHANGED
@@ -1,16 +1,121 @@
1
1
  #!/usr/bin/env node
2
+ // Windows UTF-8 encoding initialization
3
+ if (process.platform === 'win32') {
4
+ process.stdout.setEncoding('utf-8');
5
+ process.stderr.setEncoding('utf-8');
6
+ }
7
+
8
+ // Minimal DX: handle --version/-v immediately (before any other work)
9
+ const args = process.argv.slice(2);
10
+ if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
11
+ try {
12
+ const pkg = require('../package.json');
13
+ console.log(pkg.version);
14
+ process.exit(0);
15
+ } catch (e) {
16
+ console.error('Version unavailable');
17
+ process.exit(1);
18
+ }
19
+ }
20
+
21
+ // GLOBAL HELP (Level 1): provide a real, working guardian --help
22
+ function printGlobalHelp() {
23
+ console.log(`
24
+ ODAVL Guardian — Level 1
25
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
26
+
27
+ Golden Path (Reality Check)
28
+ guardian --url <url>
29
+ guardian reality --url <url>
30
+
31
+ What happens
32
+ - Opens a real browser
33
+ - Runs curated reality attempts
34
+ - Writes reports to ./.odavlguardian/<run>/
35
+
36
+ Reports (Level 1)
37
+ - decision.json (finalVerdict, exitCode, explanation)
38
+ - summary.md (human-readable summary)
39
+
40
+ Canonical Verdicts
41
+ READY | FRICTION | DO_NOT_LAUNCH
42
+
43
+ VS Code (Level 1)
44
+ Command Palette → "Guardian: Run Reality Check"
45
+ - Uses ./.odavlguardian as artifacts directory
46
+ - Shows verdict and opens summary.md
47
+
48
+ Advanced (Level 2+)
49
+ guardian scan <url> One-command full scan (advanced)
50
+ guardian journey-scan <url> Single flow journey (advanced)
51
+ guardian smoke <url> Fast sanity (advanced)
52
+ guardian baseline <save|check> Baselines (advanced)
53
+ guardian list|cleanup Artifacts mgmt (advanced)
54
+
55
+ Tips
56
+ - Pass --artifacts <dir> to override artifacts directory
57
+ - Local config: guardian.config.json (CLI only)
58
+ `);
59
+ }
60
+
61
+ // Handle global --help (or no args) before heavy loads
62
+ if (args.length === 0 || (args.length === 1 && (args[0] === '--help' || args[0] === '-h'))) {
63
+ printGlobalHelp();
64
+ process.exit(0);
65
+ }
66
+
67
+ // PHASE 6: Early flag validation (before heavy module loads)
68
+ const { validateFlags, reportFlagError } = require('../src/guardian/flag-validator');
69
+ const validation = validateFlags(process.argv);
70
+ if (!validation.valid) {
71
+ reportFlagError(validation);
72
+ process.exit(2);
73
+ }
74
+
75
+ // PHASE 6: First-run detection (lightweight)
76
+ const { isFirstRun, markAsRun, printWelcome, printFirstRunHint } = require('../src/guardian/first-run');
77
+
2
78
  const { runAttemptCLI } = require('../src/guardian/attempt');
3
79
  const { runRealityCLI } = require('../src/guardian/reality');
80
+ const { runSmokeCLI } = require('../src/guardian/smoke');
4
81
  const { runGuardian } = require('../src/guardian');
82
+ const { runJourneyScanCLI } = require('../src/guardian/journey-scan-cli');
83
+ const { runLiveCLI } = require('../src/guardian/live-cli');
5
84
  const { saveBaseline, checkBaseline } = require('../src/guardian/baseline');
6
85
  const { getDefaultAttemptIds } = require('../src/guardian/attempt-registry');
7
86
  const { initGuardian } = require('../src/guardian/init-command');
8
87
  const { printPresets } = require('../src/guardian/preset-loader');
88
+ const { listRuns } = require('../src/guardian/run-list');
89
+ const { cleanup } = require('../src/guardian/run-cleanup');
90
+ const { generateTemplate, listTemplates } = require('../src/guardian/template-command');
91
+
92
+ // Phase 8: Plan enforcement
93
+ const { checkCanScan, performScan, checkFeatureAllowed, getPlanSummary, getUpgradeMessage } = require('../src/plans/plan-manager');
94
+
95
+ // Phase 10: Founder tracking and feedback
96
+ const { registerUser, getFounderMessage, isFoundingUser } = require('../src/founder/founder-tracker');
97
+ const { runFeedbackSession } = require('../src/founder/feedback-system');
98
+ const { recordFirstScan, recordFirstLive, recordFirstUpgrade } = require('../src/founder/usage-signals');
99
+
100
+ // Phase 11: Enterprise features
101
+ const { addSite, removeSite, getSite, getSites, getSitesByProject, listProjects } = require('../src/enterprise/site-manager');
102
+ const { addUser, removeUser, getUsers, getCurrentUser, requirePermission, listRoles } = require('../src/enterprise/rbac');
103
+ const { logAudit, readAuditLogs, getAuditSummary, AUDIT_ACTIONS } = require('../src/enterprise/audit-logger');
104
+ const { exportReportToPDF, listAvailableReports } = require('../src/enterprise/pdf-exporter');
105
+
106
+ // Phase 12.1: Recipes
107
+ const { getAllRecipes, getRecipe, getRecipesByPlatform, addRecipe, removeRecipe, importRecipes, exportRecipes, exportRecipeWithMetadata, importRecipeWithMetadata } = require('../src/recipes/recipe-store');
108
+ const { validateRecipe, formatRecipe } = require('../src/recipes/recipe-engine');
109
+ const { getRegistryEntry, computeRecipeChecksum } = require('../src/recipes/recipe-registry');
110
+ const { resolveScanPreset } = require('../src/guardian/scan-presets');
9
111
 
10
112
  function parseArgs(argv) {
11
113
  const args = argv.slice(2);
12
114
  const subcommand = args[0];
13
115
 
116
+ // Note: Early flag validation in main() catches unknown commands
117
+ // so we don't need to re-validate here
118
+
14
119
  if (subcommand === 'init') {
15
120
  return { subcommand: 'init', config: parseInitArgs(args.slice(1)) };
16
121
  }
@@ -23,6 +128,18 @@ function parseArgs(argv) {
23
128
  return { subcommand: 'presets', config: {} };
24
129
  }
25
130
 
131
+ if (subcommand === 'template') {
132
+ return { subcommand: 'template', config: parseTemplateArgs(args.slice(1)) };
133
+ }
134
+
135
+ if (subcommand === 'list') {
136
+ return { subcommand: 'list', config: parseListArgs(args.slice(1)) };
137
+ }
138
+
139
+ if (subcommand === 'cleanup') {
140
+ return { subcommand: 'cleanup', config: parseCleanupArgs(args.slice(1)) };
141
+ }
142
+
26
143
  if (subcommand === 'attempt') {
27
144
  return { subcommand: 'attempt', config: parseAttemptArgs(args.slice(1)) };
28
145
  }
@@ -31,6 +148,14 @@ function parseArgs(argv) {
31
148
  return { subcommand: 'reality', config: parseRealityArgs(args.slice(1)) };
32
149
  }
33
150
 
151
+ if (subcommand === 'smoke') {
152
+ return { subcommand: 'smoke', config: parseSmokeArgs(args.slice(1)) };
153
+ }
154
+
155
+ if (subcommand === 'check') {
156
+ return { subcommand: 'smoke', config: parseSmokeArgs(args.slice(1)) };
157
+ }
158
+
34
159
  if (subcommand === 'baseline') {
35
160
  const action = args[1];
36
161
  if (action === 'save') {
@@ -43,13 +168,87 @@ function parseArgs(argv) {
43
168
  process.exit(0);
44
169
  }
45
170
 
171
+ // MVP: Human Journey Scan
172
+ if (subcommand === 'journey-scan' || subcommand === 'journey') {
173
+ return { subcommand: 'journey-scan', config: parseJourneyScanArgs(args.slice(1)) };
174
+ }
175
+
176
+ if (subcommand === 'live') {
177
+ // Support scheduler subcommands: start|stop|status
178
+ const action = args[1];
179
+ if (action === 'start') {
180
+ return { subcommand: 'live-start', config: parseLiveStartArgs(args.slice(2)) };
181
+ }
182
+ if (action === 'stop') {
183
+ return { subcommand: 'live-stop', config: parseLiveStopArgs(args.slice(2)) };
184
+ }
185
+ if (action === 'status') {
186
+ return { subcommand: 'live-status', config: {} };
187
+ }
188
+ return { subcommand: 'live', config: parseLiveArgs(args.slice(1)) };
189
+ }
190
+
191
+ if (subcommand === 'ci') {
192
+ return { subcommand: 'ci', config: parseJourneyScanArgs(args.slice(1)) };
193
+ }
194
+
46
195
  // Phase 6: Productized one-command scan
47
196
  if (subcommand === 'scan') {
48
197
  return { subcommand: 'scan', config: parseScanArgs(args.slice(1)) };
49
198
  }
50
199
 
51
- // Legacy default: crawl
52
- return { subcommand: 'crawl', config: parseCrawlArgs(args) };
200
+ // Phase 8: Plan management
201
+ if (subcommand === 'plan') {
202
+ return { subcommand: 'plan', config: {} };
203
+ }
204
+
205
+ if (subcommand === 'upgrade') {
206
+ const targetPlan = args[1];
207
+ return { subcommand: 'upgrade', config: { plan: targetPlan } };
208
+ }
209
+
210
+ // Phase 10: Feedback command
211
+ if (subcommand === 'feedback') {
212
+ return { subcommand: 'feedback', config: {} };
213
+ }
214
+
215
+ // Phase 11: Enterprise commands
216
+ if (subcommand === 'sites') {
217
+ return { subcommand: 'sites', config: parseSitesArgs(args.slice(1)) };
218
+ }
219
+
220
+ if (subcommand === 'users') {
221
+ return { subcommand: 'users', config: parseUsersArgs(args.slice(1)) };
222
+ }
223
+
224
+ if (subcommand === 'audit') {
225
+ return { subcommand: 'audit', config: parseAuditArgs(args.slice(1)) };
226
+ }
227
+
228
+ if (subcommand === 'export') {
229
+ return { subcommand: 'export', config: parseExportArgs(args.slice(1)) };
230
+ }
231
+
232
+ // Phase 12.1: Recipes
233
+ if (subcommand === 'recipe') {
234
+ return { subcommand: 'recipe', config: parseRecipeArgs(args.slice(1)) };
235
+ }
236
+
237
+ // LEVEL 1 GOLDEN PATH: guardian --url routes to Reality (not legacy crawl)
238
+ // If first arg is --url, treat it as guardian reality --url
239
+ if (args.length > 0 && args[0] === '--url') {
240
+ return { subcommand: 'reality', config: parseRealityArgs(args) };
241
+ }
242
+
243
+ // Legacy crawl command (explicit only)
244
+ if (subcommand === 'crawl') {
245
+ return { subcommand: 'crawl', config: parseCrawlArgs(args.slice(1)) };
246
+ }
247
+
248
+ // Unknown command
249
+ console.error(`Unknown command: ${subcommand}`);
250
+ console.error('Run "guardian --help" for Level 1 usage.');
251
+ process.exit(2);
53
252
  }
54
253
 
55
254
  function parseCrawlArgs(args) {
@@ -57,7 +256,7 @@ function parseCrawlArgs(args) {
57
256
  maxPages: 25,
58
257
  maxDepth: 3,
59
258
  timeout: 20000,
60
- artifactsDir: './artifacts'
259
+ artifactsDir: './.odavlguardian'
61
260
  };
62
261
 
63
262
  for (let i = 0; i < args.length; i++) {
@@ -96,12 +295,162 @@ function parseCrawlArgs(args) {
96
295
  return config;
97
296
  }
98
297
 
298
+ // Apply preset configuration deterministically across commands
299
+ function applyPresetConfig(config) {
300
+ const presetName = config.preset || 'landing';
301
+ let preset;
302
+ try {
303
+ preset = resolveScanPreset(presetName);
304
+ } catch (err) {
305
+ console.error(`Error: ${err.message}`);
306
+ process.exit(2);
307
+ }
308
+
309
+ const applied = { ...config };
310
+ applied.preset = preset.id;
311
+ applied.attempts = preset.attempts;
312
+ applied.disabledAttempts = preset.disabledAttempts || [];
313
+ applied.flows = preset.flows;
314
+ applied.policy = applied.policy || preset.policy;
315
+ if (applied.failFast === undefined) {
316
+ applied.failFast = preset.failFast;
317
+ }
318
+ applied.evidencePreset = preset.evidence || {};
319
+ return applied;
320
+ }
321
+
322
+ // MVP: Journey Scan command parser
323
+ function parseJourneyScanArgs(args) {
324
+ const config = {
325
+ baseUrl: undefined,
326
+ preset: 'saas',
327
+ artifactsDir: './.odavlguardian',
328
+ headless: true,
329
+ timeout: 20000,
330
+ presetProvided: false
331
+ };
332
+
333
+ // First arg is URL if it doesn't start with --
334
+ if (args.length > 0 && !args[0].startsWith('--')) {
335
+ config.baseUrl = args[0];
336
+ args = args.slice(1);
337
+ }
338
+
339
+ for (let i = 0; i < args.length; i++) {
340
+ const a = args[i];
341
+ if (a === '--url' && args[i + 1]) { config.baseUrl = args[i + 1]; i++; }
342
+ else if (a === '--preset' && args[i + 1]) { config.preset = args[i + 1]; config.presetProvided = true; i++; }
343
+ else if (a === '--out' && args[i + 1]) { config.artifactsDir = args[i + 1]; i++; }
344
+ else if (a === '--artifacts' && args[i + 1]) { config.artifactsDir = args[i + 1]; i++; }
345
+ else if (a === '--timeout' && args[i + 1]) { config.timeout = parseInt(args[i + 1], 10); i++; }
346
+ else if (a === '--headful') { config.headless = false; }
347
+ else if (a === '--help' || a === '-h') { printHelpJourneyScan(); process.exit(0); }
348
+ }
349
+
350
+ if (!config.baseUrl) {
351
+ console.error('Error: <url> is required');
352
+ console.error('Usage: guardian journey-scan <url> [--preset saas|shop|landing] [--out dir]');
353
+ process.exit(2);
354
+ }
355
+
356
+ return config;
357
+ }
358
+
359
+ function parseLiveArgs(args) {
360
+ const config = {
361
+ baseUrl: undefined,
362
+ artifactsDir: './.odavlguardian',
363
+ headless: true,
364
+ timeout: 20000,
365
+ intervalMinutes: null,
366
+ preset: 'saas',
367
+ presetProvided: false,
368
+ cooldownMinutes: 60
369
+ };
370
+
371
+ if (args.length > 0 && !args[0].startsWith('--')) {
372
+ config.baseUrl = args[0];
373
+ args = args.slice(1);
374
+ }
375
+
376
+ for (let i = 0; i < args.length; i++) {
377
+ const a = args[i];
378
+ if (a === '--url' && args[i + 1]) { config.baseUrl = args[i + 1]; i++; }
379
+ else if (a === '--out' && args[i + 1]) { config.artifactsDir = args[i + 1]; i++; }
380
+ else if (a === '--artifacts' && args[i + 1]) { config.artifactsDir = args[i + 1]; i++; }
381
+ else if (a === '--interval' && args[i + 1]) { config.intervalMinutes = parseFloat(args[i + 1]); i++; }
382
+ else if (a === '--cooldown' && args[i + 1]) { config.cooldownMinutes = parseFloat(args[i + 1]); i++; }
383
+ else if (a === '--timeout' && args[i + 1]) { config.timeout = parseInt(args[i + 1], 10); i++; }
384
+ else if (a === '--preset' && args[i + 1]) { config.preset = args[i + 1]; config.presetProvided = true; i++; }
385
+ else if (a === '--headful') { config.headless = false; }
386
+ else if (a === '--help' || a === '-h') { printHelpLive(); process.exit(0); }
387
+ }
388
+
389
+ if (!config.baseUrl) {
390
+ console.error('Error: <url> is required');
391
+ console.error('Usage: guardian live <url> [--interval <minutes>] [--out dir]');
392
+ process.exit(2);
393
+ }
394
+
395
+ return config;
396
+ }
397
+
398
+ // Scheduler start args
399
+ function parseLiveStartArgs(args) {
400
+ const config = {
401
+ baseUrl: undefined,
402
+ preset: 'saas',
403
+ intervalMinutes: undefined,
404
+ };
405
+
406
+ // First arg is URL if present
407
+ if (args.length > 0 && !String(args[0]).startsWith('--')) {
408
+ config.baseUrl = args[0];
409
+ args = args.slice(1);
410
+ }
411
+
412
+ for (let i = 0; i < args.length; i++) {
413
+ const a = args[i];
414
+ if (a === '--url' && args[i + 1]) { config.baseUrl = args[i + 1]; i++; }
415
+ else if (a === '--preset' && args[i + 1]) { config.preset = args[i + 1]; i++; }
416
+ else if (a === '--interval' && args[i + 1]) { config.intervalMinutes = parseFloat(args[i + 1]); i++; }
417
+ else if (a === '--help' || a === '-h') {
418
+ console.log('\nUsage: guardian live start --url <url> --interval <minutes> [--preset saas|shop|landing]');
419
+ process.exit(0);
420
+ }
421
+ }
422
+
423
+ if (!config.baseUrl || !config.intervalMinutes || config.intervalMinutes <= 0) {
424
+ console.error('Error: --url and --interval <minutes> are required');
425
+ process.exit(2);
426
+ }
427
+ return config;
428
+ }
429
+
430
+ // Scheduler stop args
431
+ function parseLiveStopArgs(args) {
432
+ const config = { id: undefined };
433
+ if (args.length > 0) {
434
+ config.id = args[0];
435
+ }
436
+ for (let i = 0; i < args.length; i++) {
437
+ const a = args[i];
438
+ if ((a === '--id' || a === '-i') && args[i + 1]) { config.id = args[i + 1]; i++; }
439
+ }
440
+ if (!config.id) {
441
+ console.error('Error: schedule id is required');
442
+ console.error('Usage: guardian live stop <id>');
443
+ process.exit(2);
444
+ }
445
+ return config;
446
+ }
447
+
99
448
  // Phase 6: Scan command (one-command value)
100
449
  function parseScanArgs(args) {
101
450
  const config = {
102
451
  // core
103
452
  baseUrl: undefined,
104
- artifactsDir: './artifacts',
453
+ artifactsDir: './.odavlguardian',
105
454
  // enable full pipeline
106
455
  enableCrawl: true,
107
456
  enableDiscovery: true,
@@ -117,7 +466,15 @@ function parseScanArgs(args) {
117
466
  enableTrace: true,
118
467
  enableScreenshots: true,
119
468
  // preset
120
- preset: 'landing'
469
+ preset: 'landing',
470
+ watch: false,
471
+ // Phase 7.1: Performance modes
472
+ timeoutProfile: 'default',
473
+ failFast: false,
474
+ fast: false,
475
+ attemptsFilter: null,
476
+ // Phase 7.2: Parallel execution
477
+ parallel: 1
121
478
  };
122
479
 
123
480
  // First arg is URL if it doesn't start with --
@@ -136,43 +493,113 @@ function parseScanArgs(args) {
136
493
  else if (a === '--headful') { config.headful = true; }
137
494
  else if (a === '--no-trace') { config.enableTrace = false; }
138
495
  else if (a === '--no-screenshots') { config.enableScreenshots = false; }
496
+ else if (a === '--watch' || a === '-w') { config.watch = true; }
497
+ // Phase 7.1: Performance flags
498
+ else if (a === '--fast') { config.fast = true; config.timeoutProfile = 'fast'; config.enableScreenshots = false; }
499
+ else if (a === '--fail-fast') { config.failFast = true; }
500
+ else if (a === '--timeout-profile' && args[i + 1]) { config.timeoutProfile = args[i + 1]; i++; }
501
+ else if (a === '--attempts' && args[i + 1]) { config.attemptsFilter = args[i + 1]; i++; }
502
+ // Phase 7.2: Parallel execution
503
+ else if (a === '--parallel' && args[i + 1]) { config.parallel = args[i + 1]; i++; }
139
504
  else if (a === '--help' || a === '-h') { printHelpScan(); process.exit(0); }
140
505
  }
141
506
 
142
507
  if (!config.baseUrl) {
143
508
  console.error('Error: <url> is required');
144
- console.error('Usage: guardian scan <url> [--preset <landing|saas|shop>]');
509
+ console.error('Usage: guardian scan <url> [--preset <landing|landing-demo|saas|shop>]');
145
510
  process.exit(2);
146
511
  }
147
512
 
148
- // Apply scan preset overrides
149
- try {
150
- const { resolveScanPreset } = require('../src/guardian/scan-presets');
151
- const presetCfg = resolveScanPreset(config.preset);
152
- config.attempts = presetCfg.attempts;
153
- config.flows = presetCfg.flows;
154
- if (presetCfg.policy) {
155
- // Allow users to override via --policy
156
- config.policy = config.policy || presetCfg.policy;
513
+ return applyPresetConfig(config);
514
+ }
515
+
516
+ function parseListArgs(args) {
517
+ const config = {
518
+ artifactsDir: './.odavlguardian',
519
+ filters: {}
520
+ };
521
+
522
+ for (let i = 0; i < args.length; i++) {
523
+ if (args[i] === '--artifacts' && args[i + 1]) {
524
+ config.artifactsDir = args[i + 1];
525
+ i++;
526
+ }
527
+ if (args[i] === '--failed') {
528
+ config.filters.failed = true;
529
+ }
530
+ if (args[i] === '--site' && args[i + 1]) {
531
+ config.filters.site = args[i + 1];
532
+ i++;
533
+ }
534
+ if (args[i] === '--limit' && args[i + 1]) {
535
+ config.filters.limit = parseInt(args[i + 1], 10);
536
+ i++;
537
+ }
538
+ if (args[i] === '--help' || args[i] === '-h') {
539
+ printHelpList();
540
+ process.exit(0);
157
541
  }
158
- } catch (e) {
159
- // If presets not available, proceed with defaults
160
542
  }
161
543
 
162
544
  return config;
163
545
  }
164
546
 
165
- function parseRealityArgs(args) {
547
+ function parseCleanupArgs(args) {
166
548
  const config = {
167
549
  artifactsDir: './artifacts',
550
+ olderThan: null,
551
+ keepLatest: null,
552
+ failedOnly: false
553
+ };
554
+
555
+ for (let i = 0; i < args.length; i++) {
556
+ if (args[i] === '--artifacts' && args[i + 1]) {
557
+ config.artifactsDir = args[i + 1];
558
+ i++;
559
+ }
560
+ if (args[i] === '--older-than' && args[i + 1]) {
561
+ config.olderThan = args[i + 1];
562
+ i++;
563
+ }
564
+ if (args[i] === '--keep-latest' && args[i + 1]) {
565
+ config.keepLatest = parseInt(args[i + 1], 10);
566
+ i++;
567
+ }
568
+ if (args[i] === '--failed-only') {
569
+ config.failedOnly = true;
570
+ }
571
+ if (args[i] === '--help' || args[i] === '-h') {
572
+ printHelpCleanup();
573
+ process.exit(0);
574
+ }
575
+ }
576
+
577
+ return config;
578
+ }
579
+
580
+ function parseRealityArgs(args) {
581
+ const config = {
582
+ artifactsDir: undefined,
168
583
  attempts: getDefaultAttemptIds(),
584
+ disabledAttempts: [],
169
585
  headful: false,
170
586
  enableTrace: true,
171
587
  enableScreenshots: true,
172
588
  enableDiscovery: false,
173
589
  includeUniversal: false,
590
+ preset: 'landing',
174
591
  policy: null,
175
- webhook: null
592
+ webhook: null,
593
+ watch: false,
594
+ // Phase 7.1: Performance modes
595
+
596
+ timeoutProfile: 'default',
597
+ failFast: false,
598
+ fast: false,
599
+ attemptsFilter: null,
600
+ // Phase 7.2: Parallel execution
601
+ parallel: 1,
602
+ _cliSource: {}
176
603
  };
177
604
 
178
605
  for (let i = 0; i < args.length; i++) {
@@ -181,11 +608,32 @@ function parseRealityArgs(args) {
181
608
  i++;
182
609
  }
183
610
  if (args[i] === '--attempts' && args[i + 1]) {
184
- config.attempts = args[i + 1].split(',').map(s => s.trim()).filter(Boolean);
611
+ // This becomes attemptsFilter for Phase 7.1
612
+ config.attemptsFilter = args[i + 1];
185
613
  i++;
186
614
  }
187
615
  if (args[i] === '--artifacts' && args[i + 1]) {
188
616
  config.artifactsDir = args[i + 1];
617
+ config._cliSource.artifactsDir = true;
618
+ i++;
619
+ }
620
+ if (args[i] === '--max-pages' && args[i + 1]) {
621
+ config.maxPages = parseInt(args[i + 1], 10);
622
+ config._cliSource.maxPages = true;
623
+ i++;
624
+ }
625
+ if (args[i] === '--max-depth' && args[i + 1]) {
626
+ config.maxDepth = parseInt(args[i + 1], 10);
627
+ config._cliSource.maxDepth = true;
628
+ i++;
629
+ }
630
+ if (args[i] === '--timeout' && args[i + 1]) {
631
+ config.timeout = parseInt(args[i + 1], 10);
632
+ config._cliSource.timeout = true;
633
+ i++;
634
+ }
635
+ if (args[i] === '--preset' && args[i + 1]) {
636
+ config.preset = args[i + 1];
189
637
  i++;
190
638
  }
191
639
  if (args[i] === '--policy' && args[i + 1]) {
@@ -205,12 +653,33 @@ function parseRealityArgs(args) {
205
653
  if (args[i] === '--headful') {
206
654
  config.headful = true;
207
655
  }
656
+ if (args[i] === '--watch' || args[i] === '-w') {
657
+ config.watch = true;
658
+ }
208
659
  if (args[i] === '--no-trace') {
209
660
  config.enableTrace = false;
210
661
  }
211
662
  if (args[i] === '--no-screenshots') {
212
663
  config.enableScreenshots = false;
213
664
  }
665
+ // Phase 7.1: Performance flags
666
+ if (args[i] === '--fast') {
667
+ config.fast = true;
668
+ config.timeoutProfile = 'fast';
669
+ config.enableScreenshots = false;
670
+ }
671
+ if (args[i] === '--fail-fast') {
672
+ config.failFast = true;
673
+ }
674
+ if (args[i] === '--timeout-profile' && args[i + 1]) {
675
+ config.timeoutProfile = args[i + 1];
676
+ i++;
677
+ }
678
+ // Phase 7.2: Parallel execution
679
+ if (args[i] === '--parallel' && args[i + 1]) {
680
+ config.parallel = args[i + 1];
681
+ i++;
682
+ }
214
683
  if (args[i] === '--help' || args[i] === '-h') {
215
684
  printHelpReality();
216
685
  process.exit(0);
@@ -223,6 +692,36 @@ function parseRealityArgs(args) {
223
692
  process.exit(2);
224
693
  }
225
694
 
695
+ return applyPresetConfig(config);
696
+ }
697
+
698
+ function parseSmokeArgs(args) {
699
+ const config = {
700
+ baseUrl: undefined,
701
+ headful: false,
702
+ timeBudgetMs: null
703
+ };
704
+
705
+ // First arg may be URL
706
+ if (args.length > 0 && !args[0].startsWith('--')) {
707
+ config.baseUrl = args[0];
708
+ args = args.slice(1);
709
+ }
710
+
711
+ for (let i = 0; i < args.length; i++) {
712
+ const a = args[i];
713
+ if (a === '--url' && args[i + 1]) { config.baseUrl = args[i + 1]; i++; }
714
+ else if (a === '--headful') { config.headful = true; }
715
+ else if (a === '--budget-ms' && args[i + 1]) { config.timeBudgetMs = parseInt(args[i + 1], 10); i++; }
716
+ else if (a === '--help' || a === '-h') { printHelpSmoke(); process.exit(0); }
717
+ }
718
+
719
+ if (!config.baseUrl) {
720
+ console.error('Error: <url> is required');
721
+ console.error('Usage: guardian smoke <url>');
722
+ process.exit(2);
723
+ }
724
+
226
725
  return config;
227
726
  }
228
727
 
@@ -245,15 +744,165 @@ function parseInitArgs(args) {
245
744
  return config;
246
745
  }
247
746
 
747
+ function parseTemplateArgs(args) {
748
+ const config = {
749
+ template: args[0] || null,
750
+ output: null
751
+ };
752
+
753
+ for (let i = 0; i < args.length; i++) {
754
+ if (args[i] === '--output' && args[i + 1]) {
755
+ config.output = args[i + 1];
756
+ i++;
757
+ }
758
+ if (args[i] === '--help' || args[i] === '-h') {
759
+ printHelpTemplate();
760
+ process.exit(0);
761
+ }
762
+ }
763
+
764
+ return config;
765
+ }
766
+
767
+ // Phase 11: Enterprise command parsers
768
+ function parseSitesArgs(args) {
769
+ const config = {
770
+ action: args[0] || 'list',
771
+ name: null,
772
+ url: null,
773
+ project: 'default'
774
+ };
775
+
776
+ if (config.action === 'add') {
777
+ config.name = args[1];
778
+ config.url = args[2];
779
+ for (let i = 3; i < args.length; i++) {
780
+ if (args[i] === '--project' && args[i + 1]) {
781
+ config.project = args[i + 1];
782
+ i++;
783
+ }
784
+ }
785
+ } else if (config.action === 'remove') {
786
+ config.name = args[1];
787
+ }
788
+
789
+ return config;
790
+ }
791
+
792
+ function parseUsersArgs(args) {
793
+ const config = {
794
+ action: args[0] || 'list',
795
+ username: args[1] || null,
796
+ role: args[2] || 'VIEWER'
797
+ };
798
+
799
+ return config;
800
+ }
801
+
802
+ function parseAuditArgs(args) {
803
+ const config = {
804
+ action: args[0] || 'list',
805
+ limit: 100,
806
+ actionFilter: null,
807
+ user: null
808
+ };
809
+
810
+ for (let i = 1; i < args.length; i++) {
811
+ if (args[i] === '--limit' && args[i + 1]) {
812
+ config.limit = parseInt(args[i + 1], 10);
813
+ i++;
814
+ }
815
+ if (args[i] === '--action' && args[i + 1]) {
816
+ config.actionFilter = args[i + 1];
817
+ i++;
818
+ }
819
+ if (args[i] === '--user' && args[i + 1]) {
820
+ config.user = args[i + 1];
821
+ i++;
822
+ }
823
+ }
824
+
825
+ return config;
826
+ }
827
+
828
+ function parseExportArgs(args) {
829
+ const config = {
830
+ reportId: args[0] || null,
831
+ format: 'pdf',
832
+ output: null
833
+ };
834
+
835
+ for (let i = 1; i < args.length; i++) {
836
+ if (args[i] === '--format' && args[i + 1]) {
837
+ config.format = args[i + 1];
838
+ i++;
839
+ }
840
+ if (args[i] === '--output' && args[i + 1]) {
841
+ config.output = args[i + 1];
842
+ i++;
843
+ }
844
+ }
845
+
846
+ return config;
847
+ }
848
+
849
+ // Phase 12.1: Recipe parser
850
+ function parseRecipeArgs(args) {
851
+ const config = {
852
+ action: args[0] || 'list',
853
+ id: args[1] || null,
854
+ url: null,
855
+ file: null,
856
+ out: null,
857
+ force: false,
858
+ };
859
+
860
+ // Positional file support for import
861
+ if (config.action === 'import' && config.id && !config.id.startsWith('--')) {
862
+ config.file = config.id;
863
+ config.id = null;
864
+ }
865
+
866
+ for (let i = 2; i < args.length; i++) {
867
+ if (args[i] === '--url' && args[i + 1]) {
868
+ config.url = args[i + 1];
869
+ i++;
870
+ }
871
+ if (args[i] === '--file' && args[i + 1]) {
872
+ config.file = args[i + 1];
873
+ i++;
874
+ }
875
+ if (args[i] === '--out' && args[i + 1]) {
876
+ config.out = args[i + 1];
877
+ i++;
878
+ }
879
+ if (args[i] === '--force') {
880
+ config.force = true;
881
+ }
882
+ }
883
+
884
+ return config;
885
+ }
886
+
248
887
  function parseProtectArgs(args) {
249
888
  const config = {
250
889
  artifactsDir: './artifacts',
251
890
  attempts: getDefaultAttemptIds(),
891
+ disabledAttempts: [],
252
892
  headful: false,
253
893
  enableTrace: true,
254
894
  enableScreenshots: true,
255
- policy: 'preset:startup',
256
- webhook: null
895
+ policy: null,
896
+ preset: 'startup',
897
+ webhook: null,
898
+ watch: false,
899
+ // Phase 7.1: Performance modes
900
+ timeoutProfile: 'default',
901
+ failFast: false,
902
+ fast: false,
903
+ attemptsFilter: null,
904
+ // Phase 7.2: Parallel execution
905
+ parallel: 1
257
906
  };
258
907
 
259
908
  // First arg is URL if it doesn't start with --
@@ -267,6 +916,10 @@ function parseProtectArgs(args) {
267
916
  config.baseUrl = args[i + 1];
268
917
  i++;
269
918
  }
919
+ if (args[i] === '--preset' && args[i + 1]) {
920
+ config.preset = args[i + 1];
921
+ i++;
922
+ }
270
923
  if (args[i] === '--policy' && args[i + 1]) {
271
924
  config.policy = args[i + 1];
272
925
  i++;
@@ -275,6 +928,31 @@ function parseProtectArgs(args) {
275
928
  config.webhook = args[i + 1];
276
929
  i++;
277
930
  }
931
+ if (args[i] === '--watch' || args[i] === '-w') {
932
+ config.watch = true;
933
+ }
934
+ // Phase 7.1: Performance flags
935
+ if (args[i] === '--fast') {
936
+ config.fast = true;
937
+ config.timeoutProfile = 'fast';
938
+ config.enableScreenshots = false;
939
+ }
940
+ if (args[i] === '--fail-fast') {
941
+ config.failFast = true;
942
+ }
943
+ if (args[i] === '--timeout-profile' && args[i + 1]) {
944
+ config.timeoutProfile = args[i + 1];
945
+ i++;
946
+ }
947
+ if (args[i] === '--attempts' && args[i + 1]) {
948
+ config.attemptsFilter = args[i + 1];
949
+ i++;
950
+ }
951
+ // Phase 7.2: Parallel execution
952
+ if (args[i] === '--parallel' && args[i + 1]) {
953
+ config.parallel = args[i + 1];
954
+ i++;
955
+ }
278
956
  if (args[i] === '--help' || args[i] === '-h') {
279
957
  printHelpProtect();
280
958
  process.exit(0);
@@ -287,7 +965,7 @@ function parseProtectArgs(args) {
287
965
  process.exit(2);
288
966
  }
289
967
 
290
- return config;
968
+ return applyPresetConfig(config);
291
969
  }
292
970
 
293
971
  function parseAttemptArgs(args) {
@@ -413,7 +1091,6 @@ WHAT IT DOES:
413
1091
 
414
1092
  OPTIONS:
415
1093
  --url <url> Target URL (required)
416
- --attempts <id1,id2> Comma-separated attempt IDs (default: contact_form, language_switch, newsletter_signup)
417
1094
  --artifacts <dir> Artifacts directory (default: ./artifacts)
418
1095
  --discover Run deterministic CLI discovery and include in snapshot
419
1096
  --universal Include Universal Reality Pack attempt
@@ -422,12 +1099,19 @@ OPTIONS:
422
1099
  --headful Run headed browser (default: headless)
423
1100
  --no-trace Disable trace recording
424
1101
  --no-screenshots Disable screenshots
1102
+
1103
+ PERFORMANCE (Phase 7.1):
1104
+ --fast Fast mode (timeout-profile=fast + no screenshots)
1105
+ --fail-fast Stop on FAILURE (not FRICTION)
1106
+ --timeout-profile <name> fast | default | slow
1107
+ --attempts <id1,id2> Comma-separated attempt IDs (default: contact_form, language_switch, newsletter_signup)
1108
+
425
1109
  --help Show this help message
426
1110
 
427
1111
  EXIT CODES:
428
- 0 Success (first run baseline created, or no regressions)
429
- 1 FAILURE (regression detected or policy failed)
430
- 2 FRICTION (drift without critical failure or soft policy failure)
1112
+ 0 READY
1113
+ 1 FRICTION
1114
+ 2 DO_NOT_LAUNCH
431
1115
 
432
1116
  EXAMPLES:
433
1117
  First run (baseline auto-created):
@@ -436,8 +1120,8 @@ EXAMPLES:
436
1120
  With policy preset:
437
1121
  guardian reality --url https://example.com --policy preset:saas
438
1122
 
439
- With custom policy:
440
- guardian reality --url https://example.com --policy ./my-policy.json
1123
+ Fast mode (performance):
1124
+ guardian reality --url https://example.com --fast --fail-fast
441
1125
  `);
442
1126
  }
443
1127
 
@@ -447,7 +1131,7 @@ Usage: guardian init [options]
447
1131
 
448
1132
  WHAT IT DOES:
449
1133
  Initialize Guardian in the current directory:
450
- - Creates guardian.policy.json (default: startup preset)
1134
+ - Creates config/guardian.policy.json (default: startup preset)
451
1135
  - Updates .gitignore to exclude Guardian artifacts
452
1136
  - Prints next steps
453
1137
 
@@ -462,23 +1146,144 @@ EXAMPLE:
462
1146
  `);
463
1147
  }
464
1148
 
1149
+ function printHelpTemplate() {
1150
+ console.log(`
1151
+ Usage: guardian template [template] [options]
1152
+
1153
+ WHAT IT DOES:
1154
+ Generate a minimal config template for common site types.
1155
+ Templates include sample journeys and policy settings.
1156
+
1157
+ AVAILABLE TEMPLATES:
1158
+ saas SaaS startup flow (signup, login, dashboard)
1159
+ shop E-commerce shop flow (browse, cart, checkout)
1160
+ landing Landing page flow (load, CTA validation)
1161
+
1162
+ OPTIONS:
1163
+ --output <file> Output file name (default: guardian-<template>.json)
1164
+ --help Show this help message
1165
+
1166
+ EXAMPLES:
1167
+ guardian template saas
1168
+ guardian template shop --output my-config.json
1169
+ guardian template landing
1170
+ `);
1171
+ }
1172
+
1173
+ function printHelpList() {
1174
+ console.log(`
1175
+ Usage: guardian list [options]
1176
+
1177
+ WHAT IT DOES:
1178
+ List all completed Guardian runs with metadata.
1179
+ Scans the artifacts directory for runs with META.json files and displays
1180
+ them in a table sorted by most recent first.
1181
+
1182
+ OPTIONS:
1183
+ --artifacts <dir> Path to artifacts directory
1184
+ Default: ./.odavlguardian
1185
+ --help Show this help message
1186
+
1187
+ OUTPUT COLUMNS:
1188
+ Time Run execution timestamp (YYYY-MM-DD HH:MM:SS)
1189
+ Site Target site slug (extracted from URL)
1190
+ Policy Policy/profile used for the run
1191
+ Result Run result: PASSED, FAILED, or WARN
1192
+ Duration Wall-clock execution time
1193
+ Path Run directory name
1194
+
1195
+ EXAMPLE:
1196
+ guardian list
1197
+ guardian list --artifacts ./.odavlguardian
1198
+ guardian list --failed
1199
+ guardian list --site github-com --limit 5
1200
+ `);
1201
+ }
1202
+
1203
+ function printHelpCleanup() {
1204
+ console.log(`
1205
+ Usage: guardian cleanup [options]
1206
+
1207
+ WHAT IT DOES:
1208
+ Manage and delete old or failed Guardian runs.
1209
+ Safely removes run directories while optionally preserving recent runs.
1210
+
1211
+ OPTIONS:
1212
+ --artifacts <dir> Path to artifacts directory
1213
+ Default: ./.odavlguardian
1214
+ --older-than <duration> Delete runs older than duration
1215
+ Format: <num>[d|h|m] (e.g., 7d, 24h, 30m)
1216
+ --keep-latest <num> Keep the N most recent runs per site
1217
+ Applied per site, deletes older runs
1218
+ --failed-only Only delete failed runs (result === FAILED)
1219
+ --help Show this help message
1220
+
1221
+ BEHAVIOR:
1222
+ - Filters are applied independently and compose
1223
+ - --failed-only filters to only FAILED status before other filters
1224
+ - --keep-latest keeps N newest per site, regardless of status
1225
+ - Combine flags: guardian cleanup --older-than 7d --failed-only
1226
+ - Deletes using real fs.rmSync() with recursive flag
1227
+
1228
+ EXAMPLE:
1229
+ guardian cleanup --older-than 7d
1230
+ guardian cleanup --keep-latest 3
1231
+ guardian cleanup --older-than 30d --failed-only
1232
+ guardian cleanup --artifacts ./.odavlguardian --keep-latest 5
1233
+ `);
1234
+ }
1235
+
465
1236
  function printHelpProtect() {
466
1237
  console.log(`
467
1238
  Usage: guardian protect <url> [options]
468
1239
 
469
1240
  WHAT IT DOES:
470
- Quick shortcut for reality check with startup policy.
471
- Equivalent to: guardian reality --url <url> --policy preset:startup
1241
+ Full market reality test with startup policy.
1242
+ Deeper than smoke; runs full discovery, attempts, and baseline comparison.
472
1243
 
473
1244
  OPTIONS:
474
1245
  <url> Target URL (required)
475
1246
  --policy <path|preset> Override policy (default: preset:startup)
476
1247
  --webhook <url> Webhook URL for notifications
1248
+
1249
+ PERFORMANCE (Phase 7.1):
1250
+ --fast Fast mode (timeout-profile=fast + no screenshots)
1251
+ --fail-fast Stop on FAILURE (not FRICTION)
1252
+ --timeout-profile <name> fast | default | slow
1253
+ --attempts <id1,id2> Comma-separated attempt IDs (filter)
1254
+
477
1255
  --help Show this help message
478
1256
 
479
1257
  EXAMPLES:
480
1258
  guardian protect https://example.com
481
1259
  guardian protect https://example.com --policy preset:enterprise
1260
+ guardian protect https://example.com --fast --fail-fast
1261
+ `);
1262
+ }
1263
+
1264
+ function printHelpSmoke() {
1265
+ console.log(`
1266
+ Usage: guardian smoke <url>
1267
+
1268
+ WHAT IT DOES:
1269
+ Fast market sanity check (<30s).
1270
+ Runs only critical paths: homepage reachability, navigation probe,
1271
+ auth (login or signup), and contact/support if present.
1272
+
1273
+ FORCED SETTINGS:
1274
+ timeout-profile=fast, fail-fast=on, parallel=2, browser reuse on,
1275
+ retries=minimal, no baseline compare.
1276
+
1277
+ EXIT CODES:
1278
+ 0 Smoke PASS
1279
+ 1 Smoke FRICTION
1280
+ 2 Smoke FAIL (including time budget exceeded)
1281
+
1282
+ Options:
1283
+ <url> Target URL (required)
1284
+ --headful Run headed browser (default: headless)
1285
+ --budget-ms <n> Override time budget in ms (primarily for CI/tests)
1286
+ --help, -h Show this help message
482
1287
  `);
483
1288
  }
484
1289
 
@@ -503,7 +1308,7 @@ Usage: guardian attempt --url <baseUrl> --attempt <id> [options]
503
1308
  Options:
504
1309
  --url <url> Target URL (required)
505
1310
  --attempt <id> Attempt ID (default: contact_form)
506
- --artifacts <dir> Artifacts directory (default: ./artifacts)
1311
+ --artifacts <dir> Artifacts directory (default: ./.odavlguardian)
507
1312
  --headful Run with visible browser (default: headless)
508
1313
  --no-trace Disable trace recording
509
1314
  --no-screenshots Disable screenshot capture
@@ -516,6 +1321,56 @@ Exit Codes:
516
1321
  `);
517
1322
  }
518
1323
 
1324
+ function printHelpJourneyScan() {
1325
+ console.log(`
1326
+ Usage: guardian journey-scan <url> [options]
1327
+
1328
+ WHAT IT DOES:
1329
+ Human journey scan — Tests a single critical user flow end-to-end.
1330
+ Opens a real browser, follows a predetermined journey, captures evidence,
1331
+ and outputs a human-readable report with a single decision:
1332
+
1333
+ ✅ SAFE (all steps succeeded)
1334
+ ⚠️ RISK (partial success with failures)
1335
+ 🚫 DO_NOT_LAUNCH (complete failure)
1336
+
1337
+ OPTIONS:
1338
+ <url> Target URL (required)
1339
+ --preset <name> saas | shop | landing (default: saas)
1340
+ --out <dir> Output directory (default: ./.odavlguardian)
1341
+ --timeout <ms> Step timeout in milliseconds (default: 20000)
1342
+ --headful Run headed browser (show UI)
1343
+ --help Show help
1344
+
1345
+ EXAMPLES:
1346
+ guardian journey-scan https://example.com
1347
+ guardian journey-scan https://example.com --preset shop --out ./results
1348
+ guardian journey-scan https://example.com --preset landing --headful
1349
+
1350
+ OUTPUT:
1351
+ SUMMARY.txt Human-readable summary
1352
+ summary.md Markdown summary
1353
+ report.json Full journey results
1354
+ screenshots/ Evidence screenshots per step
1355
+ metadata.json Scan metadata
1356
+
1357
+ EXIT CODES:
1358
+ 0 READY (all steps succeeded)
1359
+ 1 FRICTION (partial failures)
1360
+ 2 DO_NOT_LAUNCH (complete failure)
1361
+ `);
1362
+ }
1363
+
1364
+ function printHelpLive() {
1365
+ console.log('Usage: guardian live <url> [options]');
1366
+ console.log('Options:');
1367
+ console.log(' --interval <minutes> Run periodically; omit to run once');
1368
+ console.log(' --out <dir> Output directory');
1369
+ console.log(' --preset <name> Override journey preset');
1370
+ console.log(' --headful Run with visible browser');
1371
+ console.log(' --timeout <ms> Step timeout');
1372
+ }
1373
+
519
1374
  function printHelpScan() {
520
1375
  console.log(`
521
1376
  Usage: guardian scan <url> [options]
@@ -530,18 +1385,26 @@ WHAT IT DOES:
530
1385
 
531
1386
  OPTIONS:
532
1387
  <url> Target URL (required)
533
- --preset <name> landing | saas | shop (opinionated defaults)
1388
+ --preset <name> landing | landing-demo | saas | shop (opinionated defaults)
534
1389
  --policy <path|preset> Override policy file or preset:name
535
1390
  --artifacts <dir> Artifacts directory (default: ./artifacts)
536
1391
  --headful Run headed browser
537
1392
  --no-trace Disable trace
538
1393
  --no-screenshots Disable screenshots
1394
+
1395
+ PERFORMANCE (Phase 7.1):
1396
+ --fast Fast mode (timeout-profile=fast + no screenshots)
1397
+ --fail-fast Stop on FAILURE (not FRICTION)
1398
+ --timeout-profile <name> fast | default | slow
1399
+ --attempts <list> Comma-separated attempt IDs (filter)
1400
+
539
1401
  --help Show help
540
1402
 
541
1403
  EXAMPLES:
542
1404
  guardian scan https://example.com --preset landing
1405
+ guardian scan https://example.com --preset landing-demo
543
1406
  guardian scan https://example.com --preset saas
544
- guardian scan https://example.com --preset shop
1407
+ guardian scan https://example.com --fast --fail-fast
545
1408
  `);
546
1409
  }
547
1410
 
@@ -565,7 +1428,7 @@ Options:
565
1428
  --url <url> Target URL (required)
566
1429
  --attempts <id1,id2> Comma-separated attempt IDs (default: curated 3)
567
1430
  --name <baselineName> Baseline name (default: baseline)
568
- --artifacts <dir> Artifacts directory (default: ./artifacts)
1431
+ --artifacts <dir> Artifacts directory (default: ./.odavlguardian)
569
1432
  --headful Run headed browser (default: headless)
570
1433
  --no-trace Disable trace recording
571
1434
  --no-screenshots Disable screenshots
@@ -584,7 +1447,7 @@ Options:
584
1447
  --url <url> Target URL (required)
585
1448
  --name <baselineName> Baseline name to compare against (required)
586
1449
  --attempts <id1,id2> Comma-separated attempt IDs (default: curated 3)
587
- --artifacts <dir> Artifacts directory (default: ./artifacts)
1450
+ --artifacts <dir> Artifacts directory (default: ./.odavlguardian)
588
1451
  --headful Run headed browser (default: headless)
589
1452
  --no-trace Disable trace recording
590
1453
  --no-screenshots Disable screenshots
@@ -602,34 +1465,156 @@ Exit Codes:
602
1465
  async function main() {
603
1466
  const args = process.argv.slice(2);
604
1467
 
1468
+ // Phase 8: Helper to check plan before scan
1469
+ function checkPlanBeforeScan(config, options = {}) {
1470
+ const { recordUsage = true } = options;
1471
+ try {
1472
+ const url = config.url || config.baseUrl || '';
1473
+ if (!url) return; // No URL to check
1474
+
1475
+ // Phase 10: Register user on first scan
1476
+ registerUser();
1477
+
1478
+ const check = checkCanScan(url);
1479
+ if (!check.allowed) {
1480
+ // LEVEL 1 TRANSPARENT GATING: emit clear message + artifacts
1481
+ const fs = require('fs');
1482
+ const path = require('path');
1483
+ const artifactsDir = config.artifactsDir || './.odavlguardian';
1484
+ try { if (!fs.existsSync(artifactsDir)) fs.mkdirSync(artifactsDir, { recursive: true }); } catch (_) {}
1485
+
1486
+ const now = new Date().toISOString().replace(/[:\-]/g, '').substring(0, 15).replace('T', '-');
1487
+ const runId = `gated-${now}`;
1488
+ const runDir = path.join(artifactsDir, runId);
1489
+ try { fs.mkdirSync(runDir, { recursive: true }); } catch (_) {}
1490
+
1491
+ const upgradeMsg = getUpgradeMessage();
1492
+
1493
+ // Write decision.json with canonical verdict and next steps
1494
+ const decision = {
1495
+ runId,
1496
+ url,
1497
+ timestamp: new Date().toISOString(),
1498
+ preset: config.preset || 'default',
1499
+ policyName: 'Plan Gate',
1500
+ finalVerdict: 'FRICTION',
1501
+ exitCode: 1,
1502
+ reasons: [
1503
+ { code: 'PLAN_GATE', message: check.message },
1504
+ { code: 'NEXT_STEPS', message: upgradeMsg }
1505
+ ],
1506
+ gating: {
1507
+ reason: check.message,
1508
+ nextSteps: upgradeMsg
1509
+ }
1510
+ };
1511
+ try { fs.writeFileSync(path.join(runDir, 'decision.json'), JSON.stringify(decision, null, 2), 'utf8'); } catch (_) {}
1512
+
1513
+ // Write summary.md with friendly next steps
1514
+ const lines = [];
1515
+ lines.push('# Guardian Reality Summary');
1516
+ lines.push('');
1517
+ lines.push('## Final Verdict');
1518
+ lines.push('- Verdict: FRICTION (exit 1)');
1519
+ lines.push('- Why this verdict: Reality run was gated by plan limits.');
1520
+ lines.push('');
1521
+ lines.push('## What Happened');
1522
+ lines.push(`- ${check.message}`);
1523
+ lines.push('');
1524
+ lines.push('## What To Do Next');
1525
+ lines.push(`- ${upgradeMsg}`);
1526
+ try { fs.writeFileSync(path.join(runDir, 'summary.md'), lines.join('\n'), 'utf8'); } catch (_) {}
1527
+
1528
+ // Crystal-clear CLI message
1529
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1530
+ console.log('Guardian Reality was gated by plan limits');
1531
+ console.log('');
1532
+ console.log(`Why: ${check.message}`);
1533
+ console.log(`Next steps: ${upgradeMsg}`);
1534
+ console.log('');
1535
+ console.log(`Artifacts: ${runDir}`);
1536
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
1537
+ process.exit(1);
1538
+ }
1539
+
1540
+ // Show usage info
1541
+ if (check.usage && check.usage.scansRemaining !== 'Unlimited') {
1542
+ console.log(`ℹ️ Scans remaining this month: ${check.usage.scansRemaining}\n`);
1543
+ }
1544
+
1545
+ // Record the scan
1546
+ if (recordUsage) {
1547
+ performScan(url);
1548
+
1549
+ // Phase 10: Track first scan signal
1550
+ recordFirstScan();
1551
+ }
1552
+ } catch (error) {
1553
+ console.error(`\n❌ ${error.message}`);
1554
+ console.log(getUpgradeMessage());
1555
+ process.exit(1);
1556
+ }
1557
+ }
1558
+
1559
+ // Minimal release flag: print version and exit
1560
+ // PHASE 6: First-run welcome (only once)
1561
+ if (args.length > 0 && !['--help', '-h', 'init', 'presets', 'template'].includes(args[0])) {
1562
+ if (isFirstRun('.odavl-guardian')) {
1563
+ printWelcome('ODAVL Guardian');
1564
+ printFirstRunHint();
1565
+ markAsRun('.odavl-guardian');
1566
+ }
1567
+ }
1568
+
605
1569
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
606
1570
  console.log(`
607
1571
  🛡️ ODAVL Guardian — Market Reality Testing Engine
608
1572
 
609
1573
  Usage: guardian <subcommand> [options]
610
1574
 
611
- QUICK START:
612
- init Initialize Guardian in current directory
613
- protect <url> Quick reality check with startup policy
614
- reality Full Market Reality Snapshot
1575
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1576
+ LEVEL 1 GOLDEN PATH (Reality Only)
1577
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1578
+
1579
+ reality --url <URL> Full Market Reality Check
1580
+ → Canonical Verdicts: READY / FRICTION / DO_NOT_LAUNCH
1581
+ → Outputs: summary.md, decision.json, market-report.html
1582
+ → Artifacts: ./.odavlguardian/
1583
+
1584
+ --url <URL> Alias for: guardian reality --url <URL>
1585
+
1586
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1587
+ QUICK START
1588
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1589
+
1590
+ # Run Reality Check (Level 1 Golden Path)
1591
+ guardian reality --url https://example.com
615
1592
 
616
- OTHER COMMANDS:
1593
+ # Same, using alias
1594
+ guardian --url https://example.com
1595
+
1596
+ # List completed runs
1597
+ guardian list
1598
+
1599
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1600
+ ADVANCED COMMANDS (Level 2+)
1601
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1602
+
1603
+ scan <url> One-command product scan
1604
+ journey-scan <url> Human journey scan
617
1605
  attempt Execute a single user attempt
618
- baseline save (Legacy) Manually save baseline
619
- baseline check (Legacy) Manually check against baseline
1606
+ smoke <url> Fast market sanity check (<30s)
1607
+ baseline save/check Baseline management
1608
+ list List completed runs
1609
+ cleanup Manage and delete old runs
1610
+ init Initialize Guardian
1611
+ template <type> Generate config template
1612
+ plan / upgrade Show plan or upgrade
1613
+ sites / users / audit Enterprise management
1614
+ export Export reports (PDF)
620
1615
  presets List available policy presets
621
1616
 
622
- EXAMPLES:
623
- # Initialize Guardian
624
- guardian init
625
-
626
- # Quick protect (uses startup policy)
627
- guardian protect https://example.com
628
-
629
- # Full reality check with policy
630
- guardian reality --url https://example.com --policy preset:saas
631
-
632
- Run 'guardian <subcommand> --help' for more information.
1617
+ Run 'guardian <subcommand> --help' for detailed command help.
633
1618
  `);
634
1619
  process.exit(0);
635
1620
  }
@@ -645,13 +1630,517 @@ Run 'guardian <subcommand> --help' for more information.
645
1630
  } else if (parsed.subcommand === 'presets') {
646
1631
  printPresets();
647
1632
  process.exit(0);
1633
+ } else if (parsed.subcommand === 'template') {
1634
+ const template = config.template;
1635
+ if (!template) {
1636
+ console.log('\n📋 Guardian Templates\n');
1637
+ const templates = listTemplates();
1638
+ templates.forEach(t => {
1639
+ console.log(` ${t.name}: ${t.description} (${t.journeys} journeys)`);
1640
+ });
1641
+ console.log('\nUsage: guardian template <saas|shop|landing> [--output file.json]\n');
1642
+ process.exit(0);
1643
+ }
1644
+ try {
1645
+ const result = generateTemplate(template, { output: config.output });
1646
+ console.log(`\n✅ ${result.message}`);
1647
+ console.log(` Generated config ready to use with: guardian reality --config ${result.outputPath}\n`);
1648
+ process.exit(0);
1649
+ } catch (err) {
1650
+ console.error(`\n❌ ${err.message}\n`);
1651
+ process.exit(1);
1652
+ }
1653
+ } else if (parsed.subcommand === 'plan') {
1654
+ // Phase 8: Show current plan and usage
1655
+ const summary = getPlanSummary();
1656
+ console.log(`\n🛡️ ODAVL Guardian Plan\n`);
1657
+
1658
+ // Phase 10: Show founder status
1659
+ const founderMsg = getFounderMessage();
1660
+ if (founderMsg) {
1661
+ console.log(founderMsg);
1662
+ console.log();
1663
+ }
1664
+
1665
+ console.log(`Current Plan: ${summary.plan.name.toUpperCase()}`);
1666
+ if (summary.plan.price > 0) {
1667
+ console.log(`Price: $${summary.plan.price}/month`);
1668
+ }
1669
+ console.log();
1670
+ console.log(`Usage This Month:`);
1671
+ console.log(` Scans: ${summary.limits.scans.used}/${summary.limits.scans.max === -1 ? 'Unlimited' : summary.limits.scans.max} (${summary.limits.scans.remaining} remaining)`);
1672
+ console.log(` Sites: ${summary.limits.sites.used}/${summary.limits.sites.max === -1 ? 'Unlimited' : summary.limits.sites.max}`);
1673
+ console.log();
1674
+ console.log(`Features:`);
1675
+ console.log(` Live Guardian: ${summary.features.liveGuardian ? '✅' : '❌'}`);
1676
+ console.log(` CI/CD Mode: ${summary.features.ciMode ? '✅' : '❌'}`);
1677
+ console.log(` Alerts: ${summary.features.alerts ? '✅' : '❌'}`);
1678
+ console.log();
1679
+ if (summary.plan.id === 'free') {
1680
+ console.log(`Upgrade to Pro for unlimited scans and advanced features.`);
1681
+ console.log(`Run: guardian upgrade pro\n`);
1682
+ }
1683
+ process.exit(0);
1684
+ } else if (parsed.subcommand === 'upgrade') {
1685
+ // Phase 8: Upgrade to a plan
1686
+ const targetPlan = config.plan;
1687
+ if (!targetPlan || !['pro', 'business'].includes(targetPlan.toLowerCase())) {
1688
+ console.error('\n❌ Please specify a valid plan: pro or business');
1689
+ console.log('\nUsage: guardian upgrade <pro|business>\n');
1690
+ process.exit(1);
1691
+ }
1692
+ const { getCheckoutUrl } = require('../src/payments/stripe-checkout');
1693
+ const checkoutUrl = getCheckoutUrl(targetPlan);
1694
+ console.log(`\n🚀 Upgrade to ${targetPlan.toUpperCase()}\n`);
1695
+ console.log(`Open this URL to complete your upgrade:\n`);
1696
+ console.log(` ${checkoutUrl}\n`);
1697
+ console.log(`After payment, your plan will be automatically activated.\n`);
1698
+
1699
+ // Phase 10: Record upgrade signal
1700
+ recordFirstUpgrade(targetPlan.toLowerCase());
1701
+
1702
+ process.exit(0);
1703
+ } else if (parsed.subcommand === 'feedback') {
1704
+ // Phase 10: Feedback session
1705
+ try {
1706
+ await runFeedbackSession();
1707
+ process.exit(0);
1708
+ } catch (err) {
1709
+ console.error(`\n❌ Feedback error: ${err.message}\n`);
1710
+ process.exit(1);
1711
+ }
1712
+ } else if (parsed.subcommand === 'sites') {
1713
+ // Phase 11: Multi-site management
1714
+ try {
1715
+ requirePermission('site:view', 'manage sites');
1716
+
1717
+ if (config.action === 'add') {
1718
+ requirePermission('site:add', 'add sites');
1719
+ if (!config.name || !config.url) {
1720
+ console.error('Usage: guardian sites add <name> <url> [--project <name>]');
1721
+ process.exit(2);
1722
+ }
1723
+ const site = addSite(config.name, config.url, config.project);
1724
+ console.log(`✓ Site added: ${site.name} (${site.project})`);
1725
+ logAudit(AUDIT_ACTIONS.SITE_ADD, { name: site.name, url: site.url, project: site.project });
1726
+ } else if (config.action === 'remove') {
1727
+ requirePermission('site:remove', 'remove sites');
1728
+ if (!config.name) {
1729
+ console.error('Usage: guardian sites remove <name>');
1730
+ process.exit(2);
1731
+ }
1732
+ const site = removeSite(config.name);
1733
+ console.log(`✓ Site removed: ${site.name}`);
1734
+ logAudit(AUDIT_ACTIONS.SITE_REMOVE, { name: site.name });
1735
+ } else {
1736
+ // List sites
1737
+ const data = getSites();
1738
+ if (data.sites.length === 0) {
1739
+ console.log('No sites registered yet.');
1740
+ } else {
1741
+ console.log(`\nTotal sites: ${data.sites.length}\n`);
1742
+ const projects = listProjects();
1743
+ for (const proj of projects) {
1744
+ console.log(`📁 ${proj.name} (${proj.siteCount} site(s))`);
1745
+ const sites = getSitesByProject(proj.name);
1746
+ for (const site of sites) {
1747
+ console.log(` - ${site.name}: ${site.url}`);
1748
+ if (site.lastScannedAt) {
1749
+ console.log(` Last scan: ${site.lastScannedAt} (${site.scanCount} total)`);
1750
+ }
1751
+ }
1752
+ }
1753
+ }
1754
+ }
1755
+ process.exit(0);
1756
+ } catch (err) {
1757
+ console.error(`\n❌ Sites error: ${err.message}\n`);
1758
+ process.exit(1);
1759
+ }
1760
+ } else if (parsed.subcommand === 'users') {
1761
+ // Phase 11: User/role management
1762
+ try {
1763
+ requirePermission('user:view', 'manage users');
1764
+
1765
+ if (config.action === 'add') {
1766
+ requirePermission('user:add', 'add users');
1767
+ if (!config.username) {
1768
+ console.error('Usage: guardian users add <username> [role]');
1769
+ process.exit(2);
1770
+ }
1771
+ const user = addUser(config.username, config.role);
1772
+ console.log(`✓ User added: ${user.username} (${user.role})`);
1773
+ logAudit(AUDIT_ACTIONS.USER_ADD, { username: user.username, role: user.role });
1774
+ } else if (config.action === 'remove') {
1775
+ requirePermission('user:remove', 'remove users');
1776
+ if (!config.username) {
1777
+ console.error('Usage: guardian users remove <username>');
1778
+ process.exit(2);
1779
+ }
1780
+ const user = removeUser(config.username);
1781
+ console.log(`✓ User removed: ${user.username}`);
1782
+ logAudit(AUDIT_ACTIONS.USER_REMOVE, { username: user.username });
1783
+ } else if (config.action === 'roles') {
1784
+ // List roles
1785
+ const roles = listRoles();
1786
+ console.log('\nAvailable roles:\n');
1787
+ for (const role of roles) {
1788
+ console.log(`${role.name}:`);
1789
+ console.log(` Permissions: ${role.permissions.join(', ')}`);
1790
+ }
1791
+ } else {
1792
+ // List users
1793
+ const data = getUsers();
1794
+ console.log(`\nTotal users: ${data.users.length}\n`);
1795
+ for (const user of data.users) {
1796
+ const current = user.username === getCurrentUser().username ? ' (current)' : '';
1797
+ console.log(`- ${user.username}: ${user.role}${current}`);
1798
+ }
1799
+ }
1800
+ process.exit(0);
1801
+ } catch (err) {
1802
+ console.error(`\n❌ Users error: ${err.message}\n`);
1803
+ process.exit(1);
1804
+ }
1805
+ } else if (parsed.subcommand === 'audit') {
1806
+ // Phase 11: Audit log viewing
1807
+ try {
1808
+ requirePermission('audit:view', 'view audit logs');
1809
+
1810
+ if (config.action === 'summary') {
1811
+ const summary = getAuditSummary();
1812
+ console.log('\nAudit Summary:\n');
1813
+ console.log(`Total logs: ${summary.totalLogs}`);
1814
+ console.log(`First log: ${summary.firstLog || 'N/A'}`);
1815
+ console.log(`Last log: ${summary.lastLog || 'N/A'}`);
1816
+ console.log('\nActions:');
1817
+ for (const [action, count] of Object.entries(summary.actionCounts)) {
1818
+ console.log(` ${action}: ${count}`);
1819
+ }
1820
+ console.log('\nUsers:');
1821
+ for (const [user, count] of Object.entries(summary.userCounts)) {
1822
+ console.log(` ${user}: ${count}`);
1823
+ }
1824
+ } else {
1825
+ // List logs
1826
+ const logs = readAuditLogs({
1827
+ limit: config.limit,
1828
+ action: config.actionFilter,
1829
+ user: config.user
1830
+ });
1831
+
1832
+ if (logs.length === 0) {
1833
+ console.log('No audit logs found.');
1834
+ } else {
1835
+ console.log(`\nShowing ${logs.length} log(s):\n`);
1836
+ for (const log of logs) {
1837
+ console.log(`[${log.timestamp}] ${log.user} → ${log.action}`);
1838
+ if (Object.keys(log.details).length > 0) {
1839
+ console.log(` Details: ${JSON.stringify(log.details)}`);
1840
+ }
1841
+ }
1842
+ }
1843
+ }
1844
+ process.exit(0);
1845
+ } catch (err) {
1846
+ console.error(`\n❌ Audit error: ${err.message}\n`);
1847
+ process.exit(1);
1848
+ }
1849
+ } else if (parsed.subcommand === 'export') {
1850
+ // Phase 11: PDF export
1851
+ try {
1852
+ requirePermission('export:pdf', 'export reports');
1853
+
1854
+ if (!config.reportId) {
1855
+ // List available reports
1856
+ const reports = listAvailableReports();
1857
+ if (reports.length === 0) {
1858
+ console.log('No reports available for export.');
1859
+ } else {
1860
+ console.log(`\nAvailable reports:\n`);
1861
+ for (const report of reports.slice(0, 10)) {
1862
+ console.log(`- ${report.id}`);
1863
+ console.log(` Modified: ${report.modifiedAt}`);
1864
+ }
1865
+ console.log('\nUsage: guardian export <report-id> [--output <path>]');
1866
+ }
1867
+ } else {
1868
+ const result = exportReportToPDF(config.reportId, config.output);
1869
+ console.log(`✓ Report exported to: ${result.outputPath}`);
1870
+ console.log(` Size: ${result.size} bytes`);
1871
+ logAudit(AUDIT_ACTIONS.EXPORT_PDF, { reportId: config.reportId, output: result.outputPath });
1872
+ }
1873
+ process.exit(0);
1874
+ } catch (err) {
1875
+ console.error(`\n❌ Export error: ${err.message}\n`);
1876
+ process.exit(1);
1877
+ }
1878
+ } else if (parsed.subcommand === 'recipe') {
1879
+ // Phase 12.1: Recipes
1880
+ try {
1881
+ const action = config.action;
1882
+ // Ensure registry includes built-ins before any trust checks
1883
+ getAllRecipes();
1884
+
1885
+ if (action === 'list') {
1886
+ const recipes = getAllRecipes();
1887
+ console.log(`\n📚 Available Recipes (${recipes.length} total)\n`);
1888
+
1889
+ // Group by platform
1890
+ const byPlatform = {};
1891
+ for (const recipe of recipes) {
1892
+ if (!byPlatform[recipe.platform]) {
1893
+ byPlatform[recipe.platform] = [];
1894
+ }
1895
+ byPlatform[recipe.platform].push(recipe);
1896
+ }
1897
+
1898
+ for (const platform of Object.keys(byPlatform).sort()) {
1899
+ console.log(`🏪 ${platform.toUpperCase()}`);
1900
+ for (const recipe of byPlatform[platform]) {
1901
+ const reg = getRegistryEntry(recipe.id);
1902
+ const checksum = computeRecipeChecksum(recipe);
1903
+ const mismatch = reg && reg.checksum && reg.checksum !== checksum;
1904
+ const sourceLabel = reg ? reg.source : 'unknown';
1905
+ const trustNote = mismatch ? ' [checksum mismatch]' : '';
1906
+ console.log(` • ${recipe.id} - ${recipe.name} [${sourceLabel}]${trustNote}`);
1907
+ }
1908
+ console.log();
1909
+ }
1910
+ } else if (action === 'show') {
1911
+ if (!config.id) {
1912
+ console.error('Usage: guardian recipe show <id>');
1913
+ process.exit(2);
1914
+ }
1915
+
1916
+ const recipe = getRecipe(config.id);
1917
+ if (!recipe) {
1918
+ console.error(`Recipe not found: ${config.id}`);
1919
+ process.exit(1);
1920
+ }
1921
+
1922
+ console.log();
1923
+ console.log(formatRecipe(recipe));
1924
+ const reg = getRegistryEntry(recipe.id);
1925
+ const checksum = computeRecipeChecksum(recipe);
1926
+ const mismatch = reg && reg.checksum && reg.checksum !== checksum;
1927
+ if (reg) {
1928
+ console.log(`Source: ${reg.source}`);
1929
+ console.log(`Version: ${reg.version}`);
1930
+ console.log(`Checksum: ${reg.checksum}`);
1931
+ if (mismatch) {
1932
+ console.log('⚠️ Warning: checksum mismatch — recipe may have been modified');
1933
+ }
1934
+ }
1935
+ } else if (action === 'run') {
1936
+ if (!config.id || !config.url) {
1937
+ console.error('Usage: guardian recipe run <id> --url <url>');
1938
+ process.exit(2);
1939
+ }
1940
+
1941
+ const recipe = getRecipe(config.id);
1942
+ if (!recipe) {
1943
+ console.error(`Recipe not found: ${config.id}`);
1944
+ process.exit(1);
1945
+ }
1946
+
1947
+ // Enforce plan limits without counting recipe execution as a scan
1948
+ checkPlanBeforeScan({ url: config.url }, { recordUsage: false });
1949
+
1950
+ // Log recipe execution
1951
+ logAudit('recipe:run', { recipeId: recipe.id, url: config.url });
1952
+
1953
+ // Phase B: Execute recipe as enforced runtime
1954
+ (async () => {
1955
+ const { executeRecipeRuntime } = require('../src/recipes/recipe-runtime');
1956
+ const { recipeFailureToAttempt, assessRecipeImpact } = require('../src/recipes/recipe-failure-analysis');
1957
+
1958
+ console.log(`\n▶️ Executing recipe: ${recipe.name}`);
1959
+ console.log(` URL: ${config.url}`);
1960
+ console.log(` Steps: ${recipe.steps.length}\n`);
1961
+
1962
+ try {
1963
+ const result = await executeRecipeRuntime(config.id, config.url, { timeout: 20000 });
1964
+
1965
+ if (result.success) {
1966
+ console.log(`✅ RECIPE PASSED: ${recipe.name}`);
1967
+ console.log(` Goal reached: ${recipe.expectedGoal}`);
1968
+ console.log(` Duration: ${result.duration}s`);
1969
+ process.exit(0);
1970
+ } else {
1971
+ console.log(`❌ RECIPE FAILED: ${recipe.name}`);
1972
+ console.log(` Reason: ${result.failureReason}`);
1973
+ if (result.failedStep) {
1974
+ const stepNum = parseInt(result.failedStep.split('-').pop(), 10) + 1;
1975
+ console.log(` Failed at step ${stepNum} of ${result.steps.length}`);
1976
+ }
1977
+ console.log(` Duration: ${result.duration}s\n`);
1978
+
1979
+ // Integrate failure into decision engine
1980
+ const attemptForm = recipeFailureToAttempt(result);
1981
+ const impact = assessRecipeImpact(result);
1982
+
1983
+ console.log(` Risk Assessment: ${impact.severity} (score: ${impact.riskScore}/100)`);
1984
+ console.log(` Impact: ${impact.message}\n`);
1985
+
1986
+ process.exit(1);
1987
+ }
1988
+ } catch (err) {
1989
+ console.error(`\n❌ Recipe execution error: ${err.message}\n`);
1990
+ process.exit(2);
1991
+ }
1992
+ })();
1993
+ } else if (action === 'export') {
1994
+ if (!config.id || !config.out) {
1995
+ console.error('Usage: guardian recipe export <id> --out <file>');
1996
+ process.exit(2);
1997
+ }
1998
+ requirePermission('recipe:manage', 'export recipes');
1999
+ const result = exportRecipeWithMetadata(config.id, config.out);
2000
+ logAudit(AUDIT_ACTIONS.RECIPE_EXPORT, { recipeId: config.id, output: config.out, checksum: result.checksum });
2001
+ console.log(`\n✓ Recipe exported`);
2002
+ console.log(` File: ${config.out}`);
2003
+ console.log(` Checksum: ${result.checksum}`);
2004
+ } else if (action === 'import') {
2005
+ if (!config.file) {
2006
+ console.error('Usage: guardian recipe import <file>');
2007
+ process.exit(2);
2008
+ }
2009
+ requirePermission('recipe:manage', 'import recipes');
2010
+ const result = importRecipeWithMetadata(config.file, { force: config.force });
2011
+ logAudit(AUDIT_ACTIONS.RECIPE_IMPORT, { recipeId: result.recipe.id, file: config.file, checksum: result.checksum, force: config.force });
2012
+ console.log(`\n✓ Import complete`);
2013
+ console.log(` Recipe: ${result.recipe.id}`);
2014
+ console.log(` Checksum: ${result.checksum}`);
2015
+ } else {
2016
+ console.error('Unknown recipe action. Use: list, show <id>, run <id> --url <url>, export <id> --out <file>, import <file> [--force]');
2017
+ process.exit(2);
2018
+ }
2019
+
2020
+ process.exit(0);
2021
+ } catch (err) {
2022
+ console.error(`\n❌ Recipe error: ${err.message}\n`);
2023
+ process.exit(1);
2024
+ }
2025
+ } else if (parsed.subcommand === 'list') {
2026
+ const exitCode = listRuns(config.artifactsDir, config.filters);
2027
+ process.exit(exitCode);
2028
+ } else if (parsed.subcommand === 'cleanup') {
2029
+ const result = await cleanup(
2030
+ config.artifactsDir,
2031
+ {
2032
+ olderThan: config.olderThan,
2033
+ keepLatest: config.keepLatest,
2034
+ failedOnly: config.failedOnly
2035
+ }
2036
+ );
2037
+
2038
+ console.log(`\n✓ Cleanup completed`);
2039
+ console.log(` Deleted: ${result.deleted} run(s)`);
2040
+ console.log(` Kept: ${result.kept} run(s)`);
2041
+
2042
+ if (result.errors && result.errors.length > 0) {
2043
+ console.log(` Errors: ${result.errors.length}`);
2044
+ result.errors.forEach(err => {
2045
+ console.log(` - ${err}`);
2046
+ });
2047
+ process.exit(1);
2048
+ }
2049
+ process.exit(0);
648
2050
  } else if (parsed.subcommand === 'protect') {
2051
+ // Phase 8: Check plan limits before scan
2052
+ checkPlanBeforeScan(config);
649
2053
  await runRealityCLI(config);
2054
+ } else if (parsed.subcommand === 'smoke') {
2055
+ // Phase 8: Check plan limits before scan
2056
+ checkPlanBeforeScan(config);
2057
+ await runSmokeCLI(config);
650
2058
  } else if (parsed.subcommand === 'attempt') {
2059
+ // Phase 8: Check plan limits before scan
2060
+ checkPlanBeforeScan(config);
651
2061
  await runAttemptCLI(config);
2062
+ } else if (parsed.subcommand === 'journey-scan') {
2063
+ // Phase 8: Check plan limits before scan
2064
+ checkPlanBeforeScan(config);
2065
+ await runJourneyScanCLI(config);
2066
+ } else if (parsed.subcommand === 'live') {
2067
+ // Phase 8: Check feature allowed for live guardian
2068
+ const liveCheck = checkFeatureAllowed('liveGuardian');
2069
+ if (!liveCheck.allowed) {
2070
+ console.error(`\n❌ ${liveCheck.message}`);
2071
+ console.log(getUpgradeMessage());
2072
+ process.exit(1);
2073
+ }
2074
+
2075
+ // Phase 10: Track first live session
2076
+ recordFirstLive();
2077
+
2078
+ await runLiveCLI(config);
2079
+ } else if (parsed.subcommand === 'live-start') {
2080
+ // Feature check
2081
+ const liveCheck = checkFeatureAllowed('liveGuardian');
2082
+ if (!liveCheck.allowed) {
2083
+ console.error(`\n❌ ${liveCheck.message}`);
2084
+ console.log(getUpgradeMessage());
2085
+ process.exit(1);
2086
+ }
2087
+ // RBAC permission
2088
+ try { requirePermission('live:run', 'start live schedule'); } catch (e) { console.error(`\n❌ ${e.message}`); process.exit(1); }
2089
+ const { createSchedule, startBackgroundRunner } = require('../src/guardian/live-scheduler');
2090
+ const entry = createSchedule({ url: config.baseUrl, preset: config.preset, intervalMinutes: config.intervalMinutes });
2091
+ const runner = startBackgroundRunner();
2092
+ console.log('\n🟢 Live schedule started');
2093
+ console.log(` id: ${entry.id}`);
2094
+ console.log(` url: ${entry.url}`);
2095
+ console.log(` preset: ${entry.preset}`);
2096
+ console.log(` every: ${entry.intervalMinutes} min`);
2097
+ console.log(` nextRunAt: ${entry.nextRunAt}`);
2098
+ console.log(` runnerPid: ${runner.pid}`);
2099
+ process.exit(0);
2100
+ } else if (parsed.subcommand === 'live-stop') {
2101
+ // RBAC permission
2102
+ try { requirePermission('live:run', 'stop live schedule'); } catch (e) { console.error(`\n❌ ${e.message}`); process.exit(1); }
2103
+ const { stopSchedule } = require('../src/guardian/live-scheduler');
2104
+ try {
2105
+ const s = stopSchedule(config.id);
2106
+ console.log(`\n🛑 Schedule stopped: ${s.id}`);
2107
+ process.exit(0);
2108
+ } catch (err) {
2109
+ console.error(`\n❌ ${err.message}`);
2110
+ process.exit(1);
2111
+ }
2112
+ } else if (parsed.subcommand === 'live-status') {
2113
+ const { listSchedules, loadState } = require('../src/guardian/live-scheduler');
2114
+ const state = loadState();
2115
+ const schedules = listSchedules();
2116
+ console.log('\n📋 Live schedules:');
2117
+ for (const s of schedules) {
2118
+ console.log(` - ${s.id} | ${s.status} | every ${s.intervalMinutes} min`);
2119
+ console.log(` url: ${s.url} | preset: ${s.preset}`);
2120
+ console.log(` lastRunAt: ${s.lastRunAt || 'n/a'} | nextRunAt: ${s.nextRunAt || 'n/a'}`);
2121
+ }
2122
+ const pid = state.runner?.pid;
2123
+ console.log(`\nRunner: ${pid ? `pid ${pid}` : 'not running'}`);
2124
+ process.exit(0);
2125
+ } else if (parsed.subcommand === 'ci') {
2126
+ // Phase 8: Check feature allowed for CI mode
2127
+ const ciCheck = checkFeatureAllowed('ciMode');
2128
+ if (!ciCheck.allowed) {
2129
+ console.error(`\n❌ ${ciCheck.message}`);
2130
+ console.log(getUpgradeMessage());
2131
+ process.exit(1);
2132
+ }
2133
+ // Phase 4: CI gate mode
2134
+ const { runCIGate } = require('../src/guardian/ci-cli');
2135
+ const exitCode = await runCIGate(config);
2136
+ process.exit(exitCode);
652
2137
  } else if (parsed.subcommand === 'reality') {
2138
+ // Phase 8: Check plan limits before scan
2139
+ checkPlanBeforeScan(config);
653
2140
  await runRealityCLI(config);
654
2141
  } else if (parsed.subcommand === 'scan') {
2142
+ // Phase 8: Check plan limits before scan
2143
+ checkPlanBeforeScan(config);
655
2144
  // Phase 6: First-run concise guidance
656
2145
  try {
657
2146
  const { baselineExists } = require('../src/guardian/baseline-storage');