@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
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Phase C: RBAC Gate Enforcement
3
+ * Single shared gate for all sensitive commands
4
+ * Ensures no bypass paths exist
5
+ */
6
+
7
+ const { requirePermission } = require('./rbac');
8
+ const { logAudit, AUDIT_ACTIONS } = require('./audit-logger');
9
+
10
+ /**
11
+ * Definitive list of all sensitive commands and their required permissions
12
+ */
13
+ const SENSITIVE_COMMANDS = {
14
+ // Scan operations
15
+ 'scan:run': {
16
+ action: AUDIT_ACTIONS.SCAN_RUN,
17
+ description: 'Execute scan',
18
+ },
19
+ 'scan:view': {
20
+ action: AUDIT_ACTIONS.SCAN_VIEW,
21
+ description: 'View scan results',
22
+ },
23
+
24
+ // Live scheduling
25
+ 'live:start': {
26
+ action: AUDIT_ACTIONS.LIVE_START,
27
+ description: 'Start live scheduler',
28
+ },
29
+ 'live:stop': {
30
+ action: AUDIT_ACTIONS.LIVE_STOP,
31
+ description: 'Stop live scheduler',
32
+ },
33
+
34
+ // Site management
35
+ 'site:add': {
36
+ action: AUDIT_ACTIONS.SITE_ADD,
37
+ description: 'Add site',
38
+ },
39
+ 'site:remove': {
40
+ action: AUDIT_ACTIONS.SITE_REMOVE,
41
+ description: 'Remove site',
42
+ },
43
+
44
+ // User management
45
+ 'user:add': {
46
+ action: AUDIT_ACTIONS.USER_ADD,
47
+ description: 'Add user',
48
+ },
49
+ 'user:remove': {
50
+ action: AUDIT_ACTIONS.USER_REMOVE,
51
+ description: 'Remove user',
52
+ },
53
+
54
+ // Recipe management
55
+ 'recipe:import': {
56
+ action: AUDIT_ACTIONS.RECIPE_IMPORT,
57
+ description: 'Import recipe',
58
+ },
59
+ 'recipe:export': {
60
+ action: AUDIT_ACTIONS.RECIPE_EXPORT,
61
+ description: 'Export recipe',
62
+ },
63
+ 'recipe:remove': {
64
+ action: 'recipe:remove',
65
+ description: 'Remove recipe',
66
+ },
67
+
68
+ // Export operations
69
+ 'export:pdf': {
70
+ action: AUDIT_ACTIONS.EXPORT_PDF,
71
+ description: 'Export PDF report',
72
+ },
73
+
74
+ // Plan operations
75
+ 'plan:upgrade': {
76
+ action: AUDIT_ACTIONS.PLAN_UPGRADE,
77
+ description: 'Upgrade plan',
78
+ },
79
+
80
+ // Audit operations
81
+ 'audit:view': {
82
+ action: 'audit:view',
83
+ description: 'View audit logs',
84
+ },
85
+ };
86
+
87
+ /**
88
+ * Gate function: Check permission and log action
89
+ * This is the SINGLE entry point for all sensitive commands
90
+ *
91
+ * @param {string} permission - Permission string (e.g., 'scan:run', 'user:add')
92
+ * @param {Object} context - Optional context info for audit log
93
+ * @throws {Error} If permission denied
94
+ */
95
+ function enforceGate(permission, context = {}) {
96
+ // Validate permission exists in sensitive commands
97
+ if (!SENSITIVE_COMMANDS[permission]) {
98
+ throw new Error(`Unknown sensitive command: ${permission}`);
99
+ }
100
+
101
+ // Enforce RBAC
102
+ requirePermission(permission, SENSITIVE_COMMANDS[permission].description);
103
+
104
+ // Log to audit trail (immediately after successful permission check)
105
+ const cmdInfo = SENSITIVE_COMMANDS[permission];
106
+ logAudit(cmdInfo.action, {
107
+ permission,
108
+ description: cmdInfo.description,
109
+ ...context,
110
+ });
111
+
112
+ // Return context for caller to use
113
+ return {
114
+ permission,
115
+ action: cmdInfo.action,
116
+ description: cmdInfo.description,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Get all sensitive commands (for testing/documentation)
122
+ */
123
+ function listSensitiveCommands() {
124
+ return Object.entries(SENSITIVE_COMMANDS).map(([permission, info]) => ({
125
+ permission,
126
+ ...info,
127
+ }));
128
+ }
129
+
130
+ /**
131
+ * Check if a permission is sensitive (gated)
132
+ */
133
+ function isSensitiveCommand(permission) {
134
+ return permission in SENSITIVE_COMMANDS;
135
+ }
136
+
137
+ module.exports = {
138
+ enforceGate,
139
+ listSensitiveCommands,
140
+ isSensitiveCommand,
141
+ SENSITIVE_COMMANDS,
142
+ };
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Phase 11: Role-Based Access Control (RBAC)
3
+ * Enforce permission checks for enterprise features
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const RBAC_DIR = path.join(os.homedir(), '.odavl-guardian', 'rbac');
11
+ const USERS_FILE = path.join(RBAC_DIR, 'users.json');
12
+
13
+ // Role definitions
14
+ const ROLES = {
15
+ ADMIN: {
16
+ name: 'ADMIN',
17
+ permissions: [
18
+ 'scan:run',
19
+ 'scan:view',
20
+ 'live:run',
21
+ 'site:add',
22
+ 'site:remove',
23
+ 'site:view',
24
+ 'plan:view',
25
+ 'plan:upgrade',
26
+ 'user:add',
27
+ 'user:remove',
28
+ 'user:view',
29
+ 'audit:view',
30
+ 'export:pdf',
31
+ 'recipe:manage',
32
+ ],
33
+ },
34
+ OPERATOR: {
35
+ name: 'OPERATOR',
36
+ permissions: [
37
+ 'scan:run',
38
+ 'scan:view',
39
+ 'live:run',
40
+ 'site:view',
41
+ 'plan:view',
42
+ 'export:pdf',
43
+ ],
44
+ },
45
+ VIEWER: {
46
+ name: 'VIEWER',
47
+ permissions: [
48
+ 'scan:view',
49
+ 'site:view',
50
+ 'plan:view',
51
+ 'audit:view',
52
+ ],
53
+ },
54
+ };
55
+
56
+ /**
57
+ * Ensure RBAC directory exists
58
+ */
59
+ function ensureRbacDir() {
60
+ if (!fs.existsSync(RBAC_DIR)) {
61
+ fs.mkdirSync(RBAC_DIR, { recursive: true });
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Get all users
67
+ */
68
+ function getUsers() {
69
+ ensureRbacDir();
70
+
71
+ if (!fs.existsSync(USERS_FILE)) {
72
+ // Default: current user is ADMIN
73
+ const defaultUser = {
74
+ username: os.userInfo().username || 'admin',
75
+ role: 'ADMIN',
76
+ addedAt: new Date().toISOString(),
77
+ };
78
+ return { users: [defaultUser] };
79
+ }
80
+
81
+ try {
82
+ return JSON.parse(fs.readFileSync(USERS_FILE, 'utf-8'));
83
+ } catch (error) {
84
+ const defaultUser = {
85
+ username: os.userInfo().username || 'admin',
86
+ role: 'ADMIN',
87
+ addedAt: new Date().toISOString(),
88
+ };
89
+ return { users: [defaultUser] };
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Save users
95
+ */
96
+ function saveUsers(data) {
97
+ ensureRbacDir();
98
+ fs.writeFileSync(USERS_FILE, JSON.stringify(data, null, 2), 'utf-8');
99
+ }
100
+
101
+ /**
102
+ * Add a user
103
+ */
104
+ function addUser(username, role = 'VIEWER') {
105
+ if (!ROLES[role]) {
106
+ throw new Error(`Invalid role: ${role}. Must be ADMIN, OPERATOR, or VIEWER`);
107
+ }
108
+
109
+ const data = getUsers();
110
+
111
+ // Check if user already exists
112
+ const existing = data.users.find(u => u.username === username);
113
+ if (existing) {
114
+ throw new Error(`User '${username}' already exists`);
115
+ }
116
+
117
+ const user = {
118
+ username,
119
+ role,
120
+ addedAt: new Date().toISOString(),
121
+ };
122
+
123
+ data.users.push(user);
124
+ saveUsers(data);
125
+
126
+ return user;
127
+ }
128
+
129
+ /**
130
+ * Remove a user
131
+ */
132
+ function removeUser(username) {
133
+ const data = getUsers();
134
+
135
+ const index = data.users.findIndex(u => u.username === username);
136
+ if (index === -1) {
137
+ throw new Error(`User '${username}' not found`);
138
+ }
139
+
140
+ // Prevent removing last ADMIN
141
+ const user = data.users[index];
142
+ if (user.role === 'ADMIN') {
143
+ const adminCount = data.users.filter(u => u.role === 'ADMIN').length;
144
+ if (adminCount === 1) {
145
+ throw new Error('Cannot remove last ADMIN user');
146
+ }
147
+ }
148
+
149
+ data.users.splice(index, 1);
150
+ saveUsers(data);
151
+
152
+ return user;
153
+ }
154
+
155
+ /**
156
+ * Get current user
157
+ */
158
+ function getCurrentUser() {
159
+ const currentUsername = os.userInfo().username || 'admin';
160
+ const data = getUsers();
161
+
162
+ let user = data.users.find(u => u.username === currentUsername);
163
+
164
+ // If user doesn't exist, create as ADMIN
165
+ if (!user) {
166
+ user = {
167
+ username: currentUsername,
168
+ role: 'ADMIN',
169
+ addedAt: new Date().toISOString(),
170
+ };
171
+ data.users.push(user);
172
+ saveUsers(data);
173
+ }
174
+
175
+ return user;
176
+ }
177
+
178
+ /**
179
+ * Check if user has permission
180
+ */
181
+ function hasPermission(permission) {
182
+ const user = getCurrentUser();
183
+ const role = ROLES[user.role];
184
+
185
+ if (!role) {
186
+ return false;
187
+ }
188
+
189
+ return role.permissions.includes(permission);
190
+ }
191
+
192
+ /**
193
+ * Require permission (throws if denied)
194
+ */
195
+ function requirePermission(permission, action = null) {
196
+ if (!hasPermission(permission)) {
197
+ const user = getCurrentUser();
198
+ const actionMsg = action ? ` to ${action}` : '';
199
+ throw new Error(
200
+ `Permission denied${actionMsg}. Required: ${permission}. Your role: ${user.role}`
201
+ );
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Get role details
207
+ */
208
+ function getRole(roleName) {
209
+ return ROLES[roleName];
210
+ }
211
+
212
+ /**
213
+ * List all roles
214
+ */
215
+ function listRoles() {
216
+ return Object.values(ROLES);
217
+ }
218
+
219
+ /**
220
+ * Reset users (for testing)
221
+ */
222
+ function resetUsers() {
223
+ if (fs.existsSync(USERS_FILE)) {
224
+ fs.unlinkSync(USERS_FILE);
225
+ }
226
+ }
227
+
228
+ module.exports = {
229
+ ROLES,
230
+ addUser,
231
+ removeUser,
232
+ getUsers,
233
+ getCurrentUser,
234
+ hasPermission,
235
+ requirePermission,
236
+ getRole,
237
+ listRoles,
238
+ resetUsers,
239
+ };
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Phase 11: Multi-Site Management
3
+ * Support multiple sites per install with project organization
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const SITES_DIR = path.join(os.homedir(), '.odavl-guardian', 'sites');
11
+ const SITES_FILE = path.join(SITES_DIR, 'sites.json');
12
+
13
+ /**
14
+ * Ensure sites directory exists
15
+ */
16
+ function ensureSitesDir() {
17
+ if (!fs.existsSync(SITES_DIR)) {
18
+ fs.mkdirSync(SITES_DIR, { recursive: true });
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Get all sites
24
+ */
25
+ function getSites() {
26
+ ensureSitesDir();
27
+
28
+ if (!fs.existsSync(SITES_FILE)) {
29
+ return { sites: [], projects: {} };
30
+ }
31
+
32
+ try {
33
+ return JSON.parse(fs.readFileSync(SITES_FILE, 'utf-8'));
34
+ } catch (error) {
35
+ return { sites: [], projects: {} };
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Save sites
41
+ */
42
+ function saveSites(data) {
43
+ ensureSitesDir();
44
+ fs.writeFileSync(SITES_FILE, JSON.stringify(data, null, 2), 'utf-8');
45
+ }
46
+
47
+ /**
48
+ * Add a site
49
+ */
50
+ function addSite(name, url, project = 'default') {
51
+ const data = getSites();
52
+
53
+ // Check if site name already exists
54
+ const existing = data.sites.find(s => s.name === name);
55
+ if (existing) {
56
+ throw new Error(`Site '${name}' already exists`);
57
+ }
58
+
59
+ // Validate URL
60
+ try {
61
+ new URL(url);
62
+ } catch (error) {
63
+ throw new Error(`Invalid URL: ${url}`);
64
+ }
65
+
66
+ const site = {
67
+ name,
68
+ url,
69
+ project,
70
+ addedAt: new Date().toISOString(),
71
+ lastScannedAt: null,
72
+ scanCount: 0,
73
+ };
74
+
75
+ data.sites.push(site);
76
+
77
+ // Add to project index
78
+ if (!data.projects[project]) {
79
+ data.projects[project] = [];
80
+ }
81
+ data.projects[project].push(name);
82
+
83
+ saveSites(data);
84
+ return site;
85
+ }
86
+
87
+ /**
88
+ * Remove a site
89
+ */
90
+ function removeSite(name) {
91
+ const data = getSites();
92
+
93
+ const index = data.sites.findIndex(s => s.name === name);
94
+ if (index === -1) {
95
+ throw new Error(`Site '${name}' not found`);
96
+ }
97
+
98
+ const site = data.sites[index];
99
+
100
+ // Remove from sites array
101
+ data.sites.splice(index, 1);
102
+
103
+ // Remove from project index
104
+ if (data.projects[site.project]) {
105
+ data.projects[site.project] = data.projects[site.project].filter(n => n !== name);
106
+
107
+ // Clean up empty projects
108
+ if (data.projects[site.project].length === 0) {
109
+ delete data.projects[site.project];
110
+ }
111
+ }
112
+
113
+ saveSites(data);
114
+ return site;
115
+ }
116
+
117
+ /**
118
+ * Get a site by name
119
+ */
120
+ function getSite(name) {
121
+ const data = getSites();
122
+ return data.sites.find(s => s.name === name);
123
+ }
124
+
125
+ /**
126
+ * Update site scan stats
127
+ */
128
+ function recordSiteScan(name) {
129
+ const data = getSites();
130
+
131
+ const site = data.sites.find(s => s.name === name);
132
+ if (!site) {
133
+ return null;
134
+ }
135
+
136
+ site.lastScannedAt = new Date().toISOString();
137
+ site.scanCount += 1;
138
+
139
+ saveSites(data);
140
+ return site;
141
+ }
142
+
143
+ /**
144
+ * Get sites by project
145
+ */
146
+ function getSitesByProject(project) {
147
+ const data = getSites();
148
+ return data.sites.filter(s => s.project === project);
149
+ }
150
+
151
+ /**
152
+ * List all projects
153
+ */
154
+ function listProjects() {
155
+ const data = getSites();
156
+ return Object.keys(data.projects).map(name => ({
157
+ name,
158
+ siteCount: data.projects[name].length,
159
+ }));
160
+ }
161
+
162
+ /**
163
+ * Reset sites (for testing)
164
+ */
165
+ function resetSites() {
166
+ if (fs.existsSync(SITES_FILE)) {
167
+ fs.unlinkSync(SITES_FILE);
168
+ }
169
+ }
170
+
171
+ module.exports = {
172
+ addSite,
173
+ removeSite,
174
+ getSite,
175
+ getSites,
176
+ recordSiteScan,
177
+ getSitesByProject,
178
+ listProjects,
179
+ resetSites,
180
+ };
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Phase 10: Feedback System
3
+ * Lightweight feedback capture from users
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+ const readline = require('readline');
10
+
11
+ const FEEDBACK_DIR = path.join(os.homedir(), '.odavl-guardian', 'feedback');
12
+
13
+ /**
14
+ * Ensure feedback directory exists
15
+ */
16
+ function ensureFeedbackDir() {
17
+ if (!fs.existsSync(FEEDBACK_DIR)) {
18
+ fs.mkdirSync(FEEDBACK_DIR, { recursive: true });
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Create readline interface for interactive prompts
24
+ */
25
+ function createPrompt() {
26
+ return readline.createInterface({
27
+ input: process.stdin,
28
+ output: process.stdout,
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Ask a question and get response
34
+ */
35
+ function ask(rl, question) {
36
+ return new Promise((resolve) => {
37
+ rl.question(question, (answer) => {
38
+ resolve(answer.trim());
39
+ });
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Run interactive feedback session
45
+ */
46
+ async function runFeedbackSession() {
47
+ console.log('\n🙏 Thank you for taking time to share feedback!\n');
48
+ console.log('This helps us improve Guardian for everyone.\n');
49
+
50
+ const rl = createPrompt();
51
+ const feedback = {
52
+ timestamp: new Date().toISOString(),
53
+ responses: {},
54
+ };
55
+
56
+ try {
57
+ // Question 1: What worked?
58
+ console.log('1️⃣ What worked well for you?');
59
+ feedback.responses.whatWorked = await ask(rl, ' → ');
60
+ console.log();
61
+
62
+ // Question 2: What blocked you?
63
+ console.log('2️⃣ What blocked you or was frustrating?');
64
+ feedback.responses.whatBlocked = await ask(rl, ' → ');
65
+ console.log();
66
+
67
+ // Question 3: Would you recommend?
68
+ console.log('3️⃣ Would you recommend Guardian to others? (yes/no)');
69
+ const recommend = await ask(rl, ' → ');
70
+ feedback.responses.wouldRecommend = recommend.toLowerCase().startsWith('y') ? 'yes' : 'no';
71
+ console.log();
72
+
73
+ // Optional: Email
74
+ console.log('📧 (Optional) Share your email if you\'d like updates:');
75
+ const email = await ask(rl, ' → ');
76
+ if (email && email.includes('@')) {
77
+ feedback.email = email;
78
+ }
79
+
80
+ rl.close();
81
+
82
+ // Save feedback
83
+ saveFeedback(feedback);
84
+
85
+ console.log('\n✅ Feedback saved! Thank you for helping improve Guardian.\n');
86
+
87
+ return feedback;
88
+ } catch (error) {
89
+ rl.close();
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Save feedback to local file
96
+ */
97
+ function saveFeedback(feedback) {
98
+ ensureFeedbackDir();
99
+
100
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
101
+ const filename = `feedback-${timestamp}.json`;
102
+ const filepath = path.join(FEEDBACK_DIR, filename);
103
+
104
+ fs.writeFileSync(filepath, JSON.stringify(feedback, null, 2), 'utf-8');
105
+
106
+ return filepath;
107
+ }
108
+
109
+ /**
110
+ * Get all feedback submissions
111
+ */
112
+ function getAllFeedback() {
113
+ ensureFeedbackDir();
114
+
115
+ const files = fs.readdirSync(FEEDBACK_DIR)
116
+ .filter(f => f.startsWith('feedback-') && f.endsWith('.json'))
117
+ .sort()
118
+ .reverse();
119
+
120
+ return files.map(filename => {
121
+ const filepath = path.join(FEEDBACK_DIR, filename);
122
+ try {
123
+ return JSON.parse(fs.readFileSync(filepath, 'utf-8'));
124
+ } catch (error) {
125
+ return null;
126
+ }
127
+ }).filter(Boolean);
128
+ }
129
+
130
+ /**
131
+ * Get feedback count
132
+ */
133
+ function getFeedbackCount() {
134
+ const feedback = getAllFeedback();
135
+ return feedback.length;
136
+ }
137
+
138
+ /**
139
+ * Clear all feedback (for testing)
140
+ */
141
+ function clearFeedback() {
142
+ if (fs.existsSync(FEEDBACK_DIR)) {
143
+ const files = fs.readdirSync(FEEDBACK_DIR);
144
+ files.forEach(file => {
145
+ fs.unlinkSync(path.join(FEEDBACK_DIR, file));
146
+ });
147
+ }
148
+ }
149
+
150
+ module.exports = {
151
+ runFeedbackSession,
152
+ saveFeedback,
153
+ getAllFeedback,
154
+ getFeedbackCount,
155
+ clearFeedback,
156
+ };