@odavl/guardian 2.0.0 ā 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +210 -210
- package/LICENSE +21 -21
- package/README.md +297 -184
- package/bin/guardian.js +2242 -2221
- package/config/README.md +59 -59
- package/config/guardian.config.json +54 -54
- package/config/guardian.policy.json +12 -12
- package/config/profiles/docs.yaml +18 -18
- package/config/profiles/ecommerce.yaml +17 -17
- package/config/profiles/landing-demo.yaml +16 -16
- package/config/profiles/marketing.yaml +18 -18
- package/config/profiles/saas.yaml +21 -21
- package/flows/example-login-flow.json +36 -36
- package/flows/example-signup-flow.json +44 -44
- package/package.json +124 -116
- package/policies/enterprise.json +12 -12
- package/policies/landing-demo.json +22 -22
- package/policies/saas.json +12 -12
- package/policies/startup.json +12 -12
- package/src/enterprise/audit-logger.js +166 -166
- package/src/enterprise/pdf-exporter.js +267 -267
- package/src/enterprise/rbac-gate.js +142 -142
- package/src/enterprise/rbac.js +239 -239
- package/src/enterprise/site-manager.js +180 -180
- package/src/founder/feedback-system.js +156 -156
- package/src/founder/founder-tracker.js +213 -213
- package/src/founder/usage-signals.js +141 -141
- package/src/guardian/action-hints.js +439 -439
- package/src/guardian/alert-ledger.js +121 -121
- package/src/guardian/artifact-sanitizer.js +56 -56
- package/src/guardian/attempt-engine.js +1069 -1029
- package/src/guardian/attempt-registry.js +267 -267
- package/src/guardian/attempt-relevance.js +106 -106
- package/src/guardian/attempt-reporter.js +513 -507
- package/src/guardian/attempt.js +274 -273
- package/src/guardian/attempts-filter.js +63 -63
- package/src/guardian/auto-attempt-builder.js +283 -283
- package/src/guardian/baseline-registry.js +177 -177
- package/src/guardian/baseline-reporter.js +143 -143
- package/src/guardian/baseline-storage.js +285 -285
- package/src/guardian/baseline.js +535 -534
- package/src/guardian/behavioral-signals.js +261 -261
- package/src/guardian/breakage-intelligence.js +224 -224
- package/src/guardian/browser-pool.js +131 -131
- package/src/guardian/browser.js +119 -119
- package/src/guardian/canonical-truth.js +308 -308
- package/src/guardian/ci-cli.js +121 -121
- package/src/guardian/ci-gate.js +96 -96
- package/src/guardian/ci-mode.js +15 -15
- package/src/guardian/ci-output.js +55 -38
- package/src/guardian/cli-summary.js +102 -102
- package/src/guardian/confidence-signals.js +251 -251
- package/src/guardian/config-loader.js +161 -161
- package/src/guardian/config-validator.js +285 -283
- package/src/guardian/coverage-model.js +239 -239
- package/src/guardian/coverage-packs.js +58 -58
- package/src/guardian/crawler.js +142 -142
- package/src/guardian/data-guardian-detector.js +189 -189
- package/src/guardian/decision-authority.js +746 -725
- package/src/guardian/detection-layers.js +271 -271
- package/src/guardian/determinism.js +146 -146
- package/src/guardian/discovery-engine.js +661 -661
- package/src/guardian/drift-detector.js +100 -100
- package/src/guardian/enhanced-html-reporter.js +522 -522
- package/src/guardian/env-guard.js +128 -127
- package/src/guardian/error-clarity.js +399 -399
- package/src/guardian/export-contract.js +196 -196
- package/src/guardian/fail-safe.js +212 -212
- package/src/guardian/failure-intelligence.js +173 -173
- package/src/guardian/failure-taxonomy.js +169 -169
- package/src/guardian/final-outcome.js +206 -206
- package/src/guardian/first-run-profile.js +89 -89
- package/src/guardian/first-run.js +65 -67
- package/src/guardian/flag-validator.js +111 -111
- package/src/guardian/flow-executor.js +641 -639
- package/src/guardian/flow-registry.js +67 -67
- package/src/guardian/honesty.js +394 -394
- package/src/guardian/html-reporter.js +416 -416
- package/src/guardian/human-intent-resolver.js +296 -296
- package/src/guardian/human-interaction-model.js +351 -351
- package/src/guardian/human-journey-context.js +184 -184
- package/src/guardian/human-navigator.js +544 -544
- package/src/guardian/human-reporter.js +435 -431
- package/src/guardian/index.js +226 -221
- package/src/guardian/init-command.js +143 -143
- package/src/guardian/intent-detector.js +148 -146
- package/src/guardian/journey-definitions.js +132 -132
- package/src/guardian/journey-scan-cli.js +142 -145
- package/src/guardian/journey-scanner.js +583 -583
- package/src/guardian/junit-reporter.js +281 -281
- package/src/guardian/language-detection.js +99 -99
- package/src/guardian/live-alert.js +56 -56
- package/src/guardian/live-baseline-compare.js +146 -146
- package/src/guardian/live-cli.js +95 -95
- package/src/guardian/live-guardian.js +210 -210
- package/src/guardian/live-scheduler-runner.js +137 -137
- package/src/guardian/live-scheduler-state.js +167 -168
- package/src/guardian/live-scheduler.js +146 -146
- package/src/guardian/live-state.js +110 -110
- package/src/guardian/market-criticality.js +335 -335
- package/src/guardian/market-reporter.js +577 -577
- package/src/guardian/network-trace.js +178 -178
- package/src/guardian/obs-logger.js +110 -110
- package/src/guardian/observed-capabilities.js +427 -427
- package/src/guardian/output-contract.js +154 -0
- package/src/guardian/output-readability.js +264 -264
- package/src/guardian/parallel-executor.js +116 -116
- package/src/guardian/path-safety.js +56 -56
- package/src/guardian/pattern-analyzer.js +348 -348
- package/src/guardian/policy.js +432 -434
- package/src/guardian/prelaunch-gate.js +193 -193
- package/src/guardian/prerequisite-checker.js +101 -101
- package/src/guardian/preset-loader.js +152 -157
- package/src/guardian/profile-loader.js +96 -96
- package/src/guardian/reality.js +3025 -2826
- package/src/guardian/realworld-scenarios.js +94 -94
- package/src/guardian/reporter.js +167 -167
- package/src/guardian/retry-policy.js +123 -123
- package/src/guardian/root-cause-analysis.js +171 -171
- package/src/guardian/rules-engine.js +558 -558
- package/src/guardian/run-artifacts.js +212 -212
- package/src/guardian/run-cleanup.js +207 -207
- package/src/guardian/run-export.js +522 -522
- package/src/guardian/run-latest.js +90 -90
- package/src/guardian/run-list.js +211 -211
- package/src/guardian/run-summary.js +20 -20
- package/src/guardian/runtime-root.js +246 -246
- package/src/guardian/safety.js +248 -248
- package/src/guardian/scan-presets.js +133 -149
- package/src/guardian/screenshot.js +152 -152
- package/src/guardian/secret-hygiene.js +44 -44
- package/src/guardian/selector-fallbacks.js +394 -394
- package/src/guardian/semantic-contact-detection.js +255 -255
- package/src/guardian/semantic-contact-finder.js +201 -201
- package/src/guardian/semantic-targets.js +234 -234
- package/src/guardian/site-intelligence.js +588 -588
- package/src/guardian/site-introspection.js +257 -257
- package/src/guardian/sitemap.js +225 -225
- package/src/guardian/smoke.js +283 -258
- package/src/guardian/snapshot-schema.js +177 -290
- package/src/guardian/snapshot.js +430 -397
- package/src/guardian/stability-scorer.js +169 -169
- package/src/guardian/success-evaluator.js +214 -214
- package/src/guardian/template-command.js +184 -184
- package/src/guardian/text-formatters.js +426 -426
- package/src/guardian/timeout-profiles.js +57 -57
- package/src/guardian/truth/attempt.contract.js +158 -0
- package/src/guardian/truth/decision.contract.js +275 -0
- package/src/guardian/truth/snapshot.contract.js +363 -0
- package/src/guardian/validators.js +323 -323
- package/src/guardian/verdict-card.js +474 -474
- package/src/guardian/verdict-clarity.js +298 -298
- package/src/guardian/verdict-policy.js +363 -363
- package/src/guardian/verdict.js +333 -333
- package/src/guardian/verdicts.js +79 -74
- package/src/guardian/visual-diff.js +247 -247
- package/src/guardian/wait-for-outcome.js +119 -119
- package/src/guardian/watch-runner.js +181 -181
- package/src/guardian/watchdog-diff.js +167 -167
- package/src/guardian/webhook.js +206 -206
- package/src/payments/stripe-checkout.js +169 -169
- package/src/plans/plan-definitions.js +148 -148
- package/src/plans/plan-manager.js +211 -211
- package/src/plans/usage-tracker.js +210 -210
- package/src/recipes/recipe-engine.js +188 -188
- package/src/recipes/recipe-failure-analysis.js +159 -159
- package/src/recipes/recipe-registry.js +134 -134
- package/src/recipes/recipe-runtime.js +507 -507
- package/src/recipes/recipe-store.js +410 -410
- package/SECURITY.md +0 -77
- package/VERSIONING.md +0 -100
- package/guardian-contract-v1.md +0 -502
|
@@ -1,180 +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 (
|
|
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 (
|
|
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
|
-
};
|
|
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
|
+
};
|
|
@@ -1,156 +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 (
|
|
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
|
-
};
|
|
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
|
+
};
|