@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.
- package/CHANGELOG.md +146 -0
- package/README.md +155 -97
- package/bin/guardian.js +1544 -55
- package/config/README.md +59 -0
- package/config/profiles/landing-demo.yaml +16 -0
- package/package.json +26 -11
- package/policies/landing-demo.json +22 -0
- package/src/enterprise/audit-logger.js +166 -0
- package/src/enterprise/pdf-exporter.js +267 -0
- package/src/enterprise/rbac-gate.js +142 -0
- package/src/enterprise/rbac.js +239 -0
- package/src/enterprise/site-manager.js +180 -0
- package/src/founder/feedback-system.js +156 -0
- package/src/founder/founder-tracker.js +213 -0
- package/src/founder/usage-signals.js +141 -0
- package/src/guardian/alert-ledger.js +121 -0
- package/src/guardian/attempt-engine.js +587 -12
- package/src/guardian/attempt-registry.js +42 -1
- package/src/guardian/attempt-relevance.js +106 -0
- package/src/guardian/attempt.js +85 -39
- package/src/guardian/attempts-filter.js +63 -0
- package/src/guardian/baseline.js +50 -8
- package/src/guardian/breakage-intelligence.js +1 -0
- package/src/guardian/browser-pool.js +131 -0
- package/src/guardian/browser.js +28 -1
- package/src/guardian/ci-cli.js +121 -0
- package/src/guardian/ci-mode.js +15 -0
- package/src/guardian/ci-output.js +38 -0
- package/src/guardian/cli-summary.js +167 -67
- package/src/guardian/config-loader.js +162 -0
- package/src/guardian/data-guardian-detector.js +189 -0
- package/src/guardian/detection-layers.js +271 -0
- package/src/guardian/drift-detector.js +100 -0
- package/src/guardian/enhanced-html-reporter.js +221 -4
- package/src/guardian/env-guard.js +127 -0
- package/src/guardian/failure-intelligence.js +173 -0
- package/src/guardian/first-run-profile.js +89 -0
- package/src/guardian/first-run.js +54 -0
- package/src/guardian/flag-validator.js +111 -0
- package/src/guardian/flow-executor.js +309 -44
- package/src/guardian/html-reporter.js +2 -0
- package/src/guardian/human-reporter.js +431 -0
- package/src/guardian/index.js +22 -19
- package/src/guardian/init-command.js +9 -5
- package/src/guardian/intent-detector.js +146 -0
- package/src/guardian/journey-definitions.js +132 -0
- package/src/guardian/journey-scan-cli.js +145 -0
- package/src/guardian/journey-scanner.js +583 -0
- package/src/guardian/junit-reporter.js +18 -1
- package/src/guardian/language-detection.js +99 -0
- package/src/guardian/live-cli.js +95 -0
- package/src/guardian/live-scheduler-runner.js +137 -0
- package/src/guardian/live-scheduler.js +146 -0
- package/src/guardian/market-reporter.js +357 -82
- package/src/guardian/parallel-executor.js +116 -0
- package/src/guardian/pattern-analyzer.js +348 -0
- package/src/guardian/policy.js +80 -3
- package/src/guardian/prerequisite-checker.js +101 -0
- package/src/guardian/preset-loader.js +27 -18
- package/src/guardian/profile-loader.js +96 -0
- package/src/guardian/reality.js +1612 -115
- package/src/guardian/reporter.js +27 -41
- package/src/guardian/run-artifacts.js +212 -0
- package/src/guardian/run-cleanup.js +207 -0
- package/src/guardian/run-latest.js +90 -0
- package/src/guardian/run-list.js +211 -0
- package/src/guardian/run-summary.js +20 -0
- package/src/guardian/scan-presets.js +100 -11
- package/src/guardian/selector-fallbacks.js +394 -0
- package/src/guardian/semantic-contact-detection.js +255 -0
- package/src/guardian/semantic-contact-finder.js +201 -0
- package/src/guardian/semantic-targets.js +234 -0
- package/src/guardian/site-introspection.js +257 -0
- package/src/guardian/smoke.js +258 -0
- package/src/guardian/snapshot-schema.js +25 -1
- package/src/guardian/snapshot.js +69 -3
- package/src/guardian/stability-scorer.js +169 -0
- package/src/guardian/success-evaluator.js +214 -0
- package/src/guardian/template-command.js +184 -0
- package/src/guardian/text-formatters.js +426 -0
- package/src/guardian/timeout-profiles.js +57 -0
- package/src/guardian/verdict.js +320 -0
- package/src/guardian/verdicts.js +74 -0
- package/src/guardian/wait-for-outcome.js +120 -0
- package/src/guardian/watch-runner.js +181 -0
- package/src/payments/stripe-checkout.js +169 -0
- package/src/plans/plan-definitions.js +148 -0
- package/src/plans/plan-manager.js +211 -0
- package/src/plans/usage-tracker.js +210 -0
- package/src/recipes/recipe-engine.js +188 -0
- package/src/recipes/recipe-failure-analysis.js +159 -0
- package/src/recipes/recipe-registry.js +134 -0
- package/src/recipes/recipe-runtime.js +507 -0
- package/src/recipes/recipe-store.js +410 -0
- package/guardian-contract-v1.md +0 -149
- /package/{guardian.config.json → config/guardian.config.json} +0 -0
- /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
- /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
- /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
- /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
- /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human Report Generator
|
|
3
|
+
* Transforms journey results into human-readable summaries
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { analyzeFailure, recordSignature, getSignatureCount } = require('./failure-intelligence');
|
|
9
|
+
|
|
10
|
+
class HumanReporter {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate a complete human summary and save to file
|
|
17
|
+
*/
|
|
18
|
+
generateSummary(journeyResult, outputDir) {
|
|
19
|
+
const summary = this._buildSummary(journeyResult);
|
|
20
|
+
|
|
21
|
+
// Save as both .txt and .md
|
|
22
|
+
const txtPath = path.join(outputDir, 'summary.txt');
|
|
23
|
+
const mdPath = path.join(outputDir, 'summary.md');
|
|
24
|
+
|
|
25
|
+
fs.writeFileSync(txtPath, summary.text, 'utf8');
|
|
26
|
+
fs.writeFileSync(mdPath, summary.markdown, 'utf8');
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
text: txtPath,
|
|
30
|
+
markdown: mdPath,
|
|
31
|
+
content: summary.text
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate JSON report for programmatic access
|
|
37
|
+
*/
|
|
38
|
+
generateJSON(journeyResult, outputDir) {
|
|
39
|
+
const { toCanonicalJourneyVerdict } = require('./verdicts');
|
|
40
|
+
const decisionCanonical = toCanonicalJourneyVerdict(journeyResult.finalDecision);
|
|
41
|
+
const report = {
|
|
42
|
+
metadata: {
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
version: '1.0',
|
|
45
|
+
journey: journeyResult.journey
|
|
46
|
+
},
|
|
47
|
+
target: {
|
|
48
|
+
url: journeyResult.url,
|
|
49
|
+
reachable: journeyResult.executedSteps.length > 0
|
|
50
|
+
},
|
|
51
|
+
intentDetection: journeyResult.intentDetection || { intent: 'unknown', confidence: 0, signals: [] },
|
|
52
|
+
goal: journeyResult.goal || { goalReached: false, goalDescription: '' },
|
|
53
|
+
baseline: journeyResult.baseline || null,
|
|
54
|
+
drift: journeyResult.drift || { driftDetected: false, driftReasons: [] },
|
|
55
|
+
execution: {
|
|
56
|
+
totalSteps: journeyResult.executedSteps.length + journeyResult.failedSteps.length,
|
|
57
|
+
succeededSteps: journeyResult.executedSteps.length,
|
|
58
|
+
failedSteps: journeyResult.failedSteps.length,
|
|
59
|
+
successRate: journeyResult.executedSteps.length > 0
|
|
60
|
+
? Math.round((journeyResult.executedSteps.length /
|
|
61
|
+
(journeyResult.executedSteps.length + journeyResult.failedSteps.length)) * 100)
|
|
62
|
+
: 0
|
|
63
|
+
},
|
|
64
|
+
classification: journeyResult.errorClassification || { type: 'UNKNOWN' },
|
|
65
|
+
decision: journeyResult.finalDecision,
|
|
66
|
+
decisionCanonical,
|
|
67
|
+
reasoning: this._buildReasoning(journeyResult),
|
|
68
|
+
impact: this._assessUserImpact(journeyResult),
|
|
69
|
+
timing: {
|
|
70
|
+
started: journeyResult.startedAt,
|
|
71
|
+
ended: journeyResult.endedAt
|
|
72
|
+
},
|
|
73
|
+
details: {
|
|
74
|
+
steps: journeyResult.executedSteps,
|
|
75
|
+
failures: journeyResult.failedSteps,
|
|
76
|
+
evidence: journeyResult.evidence
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Failure intelligence section
|
|
81
|
+
const info = analyzeFailure(journeyResult);
|
|
82
|
+
const rec = recordSignature(journeyResult.url, info);
|
|
83
|
+
report.failureInsights = {
|
|
84
|
+
failureStage: info.failureStage,
|
|
85
|
+
failureStepId: info.failureStepId,
|
|
86
|
+
cause: info.cause,
|
|
87
|
+
hint: info.hint,
|
|
88
|
+
signature: rec.signature,
|
|
89
|
+
occurrences: rec.count,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const reportPath = path.join(outputDir, 'report.json');
|
|
93
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8');
|
|
94
|
+
|
|
95
|
+
return report;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build human text summary
|
|
100
|
+
*/
|
|
101
|
+
_buildSummary(journeyResult) {
|
|
102
|
+
const decision = journeyResult.finalDecision || 'UNKNOWN';
|
|
103
|
+
const classification = journeyResult.errorClassification || {};
|
|
104
|
+
|
|
105
|
+
const text = this._formatText(journeyResult);
|
|
106
|
+
const markdown = this._formatMarkdown(journeyResult);
|
|
107
|
+
|
|
108
|
+
return { text, markdown };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_formatText(journeyResult) {
|
|
112
|
+
const { toCanonicalJourneyVerdict } = require('./verdicts');
|
|
113
|
+
const canonical = toCanonicalJourneyVerdict(journeyResult.finalDecision);
|
|
114
|
+
// Baseline compare
|
|
115
|
+
if (journeyResult.baseline) {
|
|
116
|
+
lines.push('BASELINE');
|
|
117
|
+
lines.push('─────────────────────────────────────────────────────────────────');
|
|
118
|
+
const b = journeyResult.baseline;
|
|
119
|
+
lines.push(`Saved decision: ${b.decision}`);
|
|
120
|
+
lines.push(`Saved intent: ${String(b.intent || 'unknown').toUpperCase()}`);
|
|
121
|
+
lines.push(`Saved goal: ${b.goalReached ? 'Reached' : 'Not reached'}\n`);
|
|
122
|
+
|
|
123
|
+
lines.push('CURRENT VS BASELINE');
|
|
124
|
+
lines.push('─────────────────────────────────────────────────────────────────');
|
|
125
|
+
const d = journeyResult.drift || { driftDetected: false, driftReasons: [] };
|
|
126
|
+
if (d.driftDetected) {
|
|
127
|
+
lines.push('Regression detected:');
|
|
128
|
+
for (const r of d.driftReasons) lines.push(`– ${r}`);
|
|
129
|
+
} else {
|
|
130
|
+
lines.push('No regression detected');
|
|
131
|
+
}
|
|
132
|
+
lines.push();
|
|
133
|
+
}
|
|
134
|
+
const lines = [];
|
|
135
|
+
const decision = canonical || 'DO_NOT_LAUNCH';
|
|
136
|
+
|
|
137
|
+
lines.push('═══════════════════════════════════════════════════════════════');
|
|
138
|
+
lines.push(' ODAVL GUARDIAN — JOURNEY REPORT');
|
|
139
|
+
lines.push('═══════════════════════════════════════════════════════════════\n');
|
|
140
|
+
|
|
141
|
+
lines.push(`DECISION: ${this._decisionEmoji(decision)} ${decision}`);
|
|
142
|
+
lines.push(`Journey: ${journeyResult.journey || 'Unknown'}`);
|
|
143
|
+
lines.push(`Target: ${journeyResult.url}\n`);
|
|
144
|
+
|
|
145
|
+
// Intent detection
|
|
146
|
+
if (journeyResult.intentDetection) {
|
|
147
|
+
const id = journeyResult.intentDetection;
|
|
148
|
+
lines.push('SITE TYPE');
|
|
149
|
+
lines.push('─────────────────────────────────────────────────────────────────');
|
|
150
|
+
lines.push(`Detected: ${String(id.intent || 'unknown').toUpperCase()} (confidence ${id.confidence || 0}%)`);
|
|
151
|
+
lines.push(`Visitor Goal: ${this._intentToHumanGoal(id.intent)}`);
|
|
152
|
+
const goal = journeyResult.goal || { goalReached: false, goalDescription: '' };
|
|
153
|
+
lines.push(`Goal Reached: ${goal.goalReached ? 'Yes' : 'No'} — ${goal.goalDescription}\n`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
lines.push('EXECUTION SUMMARY');
|
|
157
|
+
lines.push('─────────────────────────────────────────────────────────────────');
|
|
158
|
+
lines.push(`Steps Executed: ${journeyResult.executedSteps.length}`);
|
|
159
|
+
lines.push(`Steps Failed: ${journeyResult.failedSteps.length}`);
|
|
160
|
+
lines.push(`Total Steps: ${journeyResult.executedSteps.length + journeyResult.failedSteps.length}`);
|
|
161
|
+
|
|
162
|
+
if (journeyResult.executedSteps.length > 0) {
|
|
163
|
+
const rate = Math.round((journeyResult.executedSteps.length /
|
|
164
|
+
(journeyResult.executedSteps.length + journeyResult.failedSteps.length)) * 100);
|
|
165
|
+
lines.push(`Success Rate: ${rate}%\n`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
lines.push('WHAT GUARDIAN TESTED');
|
|
169
|
+
lines.push('─────────────────────────────────────────────────────────────────');
|
|
170
|
+
const steps = journeyResult.executedSteps || [];
|
|
171
|
+
for (let i = 0; i < steps.length; i++) {
|
|
172
|
+
lines.push(`${i + 1}. ${steps[i].name || `Step ${i + 1}`}`);
|
|
173
|
+
}
|
|
174
|
+
lines.push();
|
|
175
|
+
|
|
176
|
+
if (journeyResult.failedSteps && journeyResult.failedSteps.length > 0) {
|
|
177
|
+
lines.push('WHAT FAILED');
|
|
178
|
+
lines.push('─────────────────────────────────────────────────────────────────');
|
|
179
|
+
for (const failure of journeyResult.failedSteps) {
|
|
180
|
+
const step = journeyResult.executedSteps.find(s => s.id === failure);
|
|
181
|
+
if (step) {
|
|
182
|
+
lines.push(`✗ ${step.name || failure}: ${step.error || 'Unknown failure'}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
lines.push();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (journeyResult.errorClassification) {
|
|
189
|
+
lines.push('ERROR CLASSIFICATION');
|
|
190
|
+
lines.push('─────────────────────────────────────────────────────────────────');
|
|
191
|
+
lines.push(`Type: ${journeyResult.errorClassification.type || 'UNKNOWN'}`);
|
|
192
|
+
lines.push(`Reason: ${journeyResult.errorClassification.reason || 'N/A'}\n`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
lines.push('DECISION REASONING');
|
|
196
|
+
lines.push('─────────────────────────────────────────────────────────────────');
|
|
197
|
+
lines.push(this._buildReasoning(journeyResult));
|
|
198
|
+
lines.push();
|
|
199
|
+
|
|
200
|
+
// Failure intelligence
|
|
201
|
+
const fi = analyzeFailure(journeyResult);
|
|
202
|
+
const occurrences = getSignatureCount(journeyResult.url, fi);
|
|
203
|
+
lines.push('WHERE USERS STOP');
|
|
204
|
+
lines.push('─────────────────────────────────────────────────────────────────');
|
|
205
|
+
lines.push(`Stage: ${fi.failureStage}`);
|
|
206
|
+
lines.push(`Step: ${fi.failureStepId ?? 'unknown'}`);
|
|
207
|
+
lines.push();
|
|
208
|
+
|
|
209
|
+
lines.push('WHY IT LIKELY HAPPENS');
|
|
210
|
+
lines.push('─────────────────────────────────────────────────────────────────');
|
|
211
|
+
lines.push(fi.cause);
|
|
212
|
+
lines.push();
|
|
213
|
+
|
|
214
|
+
lines.push('FIRST FIX TO TRY');
|
|
215
|
+
lines.push('─────────────────────────────────────────────────────────────────');
|
|
216
|
+
lines.push(fi.hint);
|
|
217
|
+
lines.push(`\nThis failure pattern occurred ${occurrences} time(s).`);
|
|
218
|
+
lines.push();
|
|
219
|
+
|
|
220
|
+
lines.push('USER IMPACT');
|
|
221
|
+
lines.push('─────────────────────────────────────────────────────────────────');
|
|
222
|
+
lines.push(this._assessUserImpact(journeyResult));
|
|
223
|
+
lines.push();
|
|
224
|
+
|
|
225
|
+
lines.push('═══════════════════════════════════════════════════════════════');
|
|
226
|
+
lines.push('RECOMMENDATION');
|
|
227
|
+
lines.push('═══════════════════════════════════════════════════════════════');
|
|
228
|
+
lines.push(this._getRecommendation(journeyResult));
|
|
229
|
+
lines.push();
|
|
230
|
+
|
|
231
|
+
return lines.join('\n');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
_formatMarkdown(journeyResult) {
|
|
235
|
+
const { toCanonicalJourneyVerdict } = require('./verdicts');
|
|
236
|
+
const canonical = toCanonicalJourneyVerdict(journeyResult.finalDecision);
|
|
237
|
+
if (journeyResult.baseline) {
|
|
238
|
+
const b = journeyResult.baseline;
|
|
239
|
+
const d = journeyResult.drift || { driftDetected: false, driftReasons: [] };
|
|
240
|
+
lines.push(`## Baseline`);
|
|
241
|
+
lines.push(`- Decision: ${b.decision}`);
|
|
242
|
+
lines.push(`- Intent: ${String(b.intent || 'unknown').toUpperCase()}`);
|
|
243
|
+
lines.push(`- Goal: ${b.goalReached ? 'Reached' : 'Not reached'}`);
|
|
244
|
+
lines.push();
|
|
245
|
+
lines.push(`## Current vs Baseline`);
|
|
246
|
+
if (d.driftDetected) {
|
|
247
|
+
lines.push('Regression detected:');
|
|
248
|
+
for (const r of d.driftReasons) lines.push(`- ${r}`);
|
|
249
|
+
} else {
|
|
250
|
+
lines.push('No regression detected');
|
|
251
|
+
}
|
|
252
|
+
lines.push();
|
|
253
|
+
}
|
|
254
|
+
const lines = [];
|
|
255
|
+
const decision = canonical || 'DO_NOT_LAUNCH';
|
|
256
|
+
|
|
257
|
+
lines.push(`# ODAVL Guardian — Journey Report\n`);
|
|
258
|
+
|
|
259
|
+
lines.push(`## Decision: ${this._decisionEmoji(decision)} **${decision}**\n`);
|
|
260
|
+
lines.push(`- **Journey:** ${journeyResult.journey || 'Unknown'}`);
|
|
261
|
+
lines.push(`- **Target:** ${journeyResult.url}`);
|
|
262
|
+
lines.push(`- **Time:** ${journeyResult.startedAt}\n`);
|
|
263
|
+
|
|
264
|
+
if (journeyResult.intentDetection) {
|
|
265
|
+
const id = journeyResult.intentDetection;
|
|
266
|
+
lines.push(`## Site Type`);
|
|
267
|
+
lines.push(`- **Detected:** ${String(id.intent || 'unknown').toUpperCase()} (confidence ${id.confidence || 0}%)`);
|
|
268
|
+
lines.push(`- **Visitor Goal:** ${this._intentToHumanGoal(id.intent)}`);
|
|
269
|
+
const goal = journeyResult.goal || { goalReached: false, goalDescription: '' };
|
|
270
|
+
lines.push(`- **Goal Reached:** ${goal.goalReached ? 'Yes' : 'No'} — ${goal.goalDescription}\n`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
lines.push(`## Execution Summary\n`);
|
|
274
|
+
lines.push(`| Metric | Value |`);
|
|
275
|
+
lines.push(`|--------|-------|`);
|
|
276
|
+
lines.push(`| Steps Executed | ${journeyResult.executedSteps.length} |`);
|
|
277
|
+
lines.push(`| Steps Failed | ${journeyResult.failedSteps.length} |`);
|
|
278
|
+
const rate = journeyResult.executedSteps.length > 0
|
|
279
|
+
? Math.round((journeyResult.executedSteps.length /
|
|
280
|
+
(journeyResult.executedSteps.length + journeyResult.failedSteps.length)) * 100)
|
|
281
|
+
: 0;
|
|
282
|
+
lines.push(`| Success Rate | ${rate}% |\n`);
|
|
283
|
+
|
|
284
|
+
lines.push(`## What Guardian Tested\n`);
|
|
285
|
+
const steps = journeyResult.executedSteps || [];
|
|
286
|
+
for (let i = 0; i < steps.length; i++) {
|
|
287
|
+
lines.push(`${i + 1}. ${steps[i].name || `Step ${i + 1}`}`);
|
|
288
|
+
}
|
|
289
|
+
lines.push();
|
|
290
|
+
|
|
291
|
+
if (journeyResult.failedSteps?.length > 0) {
|
|
292
|
+
lines.push(`## Failures\n`);
|
|
293
|
+
for (const failure of journeyResult.failedSteps) {
|
|
294
|
+
const step = journeyResult.executedSteps.find(s => s.id === failure);
|
|
295
|
+
if (step) {
|
|
296
|
+
lines.push(`- ✗ **${step.name || failure}**: ${step.error || 'Unknown failure'}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
lines.push();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (journeyResult.errorClassification) {
|
|
303
|
+
lines.push(`## Error Classification\n`);
|
|
304
|
+
lines.push(`- **Type:** ${journeyResult.errorClassification.type || 'UNKNOWN'}`);
|
|
305
|
+
lines.push(`- **Reason:** ${journeyResult.errorClassification.reason || 'N/A'}\n`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
lines.push(`## Reasoning\n`);
|
|
309
|
+
lines.push(this._buildReasoning(journeyResult));
|
|
310
|
+
lines.push();
|
|
311
|
+
|
|
312
|
+
lines.push(`## User Impact\n`);
|
|
313
|
+
lines.push(this._assessUserImpact(journeyResult));
|
|
314
|
+
lines.push();
|
|
315
|
+
|
|
316
|
+
const fi = analyzeFailure(journeyResult);
|
|
317
|
+
const occurrences = getSignatureCount(journeyResult.url, fi);
|
|
318
|
+
lines.push(`## Where Users Stop`);
|
|
319
|
+
lines.push(`- **Stage:** ${fi.failureStage}`);
|
|
320
|
+
lines.push(`- **Step:** ${fi.failureStepId ?? 'unknown'}`);
|
|
321
|
+
lines.push();
|
|
322
|
+
lines.push(`## Why It Likely Happens`);
|
|
323
|
+
lines.push(`- ${fi.cause}`);
|
|
324
|
+
lines.push();
|
|
325
|
+
lines.push(`## First Fix To Try`);
|
|
326
|
+
lines.push(`- ${fi.hint}`);
|
|
327
|
+
lines.push();
|
|
328
|
+
lines.push(`> This failure pattern occurred ${occurrences} time(s).`);
|
|
329
|
+
lines.push();
|
|
330
|
+
|
|
331
|
+
lines.push(`## Recommendation\n`);
|
|
332
|
+
lines.push(this._getRecommendation(journeyResult));
|
|
333
|
+
lines.push();
|
|
334
|
+
|
|
335
|
+
return lines.join('\n');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
_decisionEmoji(decision) {
|
|
339
|
+
const map = {
|
|
340
|
+
'READY': '✅',
|
|
341
|
+
'FRICTION': '⚠️ ',
|
|
342
|
+
'DO_NOT_LAUNCH': '🚫'
|
|
343
|
+
};
|
|
344
|
+
return map[decision] || '❓';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
_buildReasoning(journeyResult) {
|
|
348
|
+
const decision = journeyResult.finalDecision;
|
|
349
|
+
const executed = journeyResult.executedSteps?.length || 0;
|
|
350
|
+
const failed = journeyResult.failedSteps?.length || 0;
|
|
351
|
+
const goalReached = journeyResult.goal?.goalReached === true;
|
|
352
|
+
|
|
353
|
+
if (decision === 'SAFE') {
|
|
354
|
+
return `All ${executed} steps completed, and the visitor goal was reached. Journey is fully functional.`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (decision === 'RISK') {
|
|
358
|
+
if (failed === 0 && !goalReached) {
|
|
359
|
+
return `Journey steps succeeded, but the visitor goal was not reached. Conversion risk exists.`;
|
|
360
|
+
}
|
|
361
|
+
return `${executed} of ${executed + failed} steps succeeded (${Math.round((executed / (executed + failed)) * 100)}%). Some parts of the critical journey work, but risks exist.`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (decision === 'DO_NOT_LAUNCH') {
|
|
365
|
+
if (journeyResult.fatalError) {
|
|
366
|
+
return `Site is unreachable or blocked. Cannot complete user journey at all. Error: ${journeyResult.fatalError}`;
|
|
367
|
+
}
|
|
368
|
+
return `Journey failed completely (0/${executed + failed} steps succeeded). Site or critical elements are broken. Do not launch.`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return 'Unable to determine outcome from results.';
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
_assessUserImpact(journeyResult) {
|
|
375
|
+
const classification = journeyResult.errorClassification?.type;
|
|
376
|
+
|
|
377
|
+
if (journeyResult.finalDecision === 'SAFE') {
|
|
378
|
+
return 'Visitors will successfully complete the critical user journey. No blockers detected.';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (classification === 'CTA_NOT_FOUND') {
|
|
382
|
+
return 'Visitors cannot find the key conversion element. Sign-up/checkout CTA is missing or inaccessible.';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (classification === 'NAVIGATION_BLOCKED') {
|
|
386
|
+
return 'Visitors cannot navigate to critical pages. Internal navigation is broken.';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (classification === 'SITE_UNREACHABLE') {
|
|
390
|
+
return 'Site is entirely unreachable. Visitors cannot access the website at all.';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (journeyResult.finalDecision === 'RISK') {
|
|
394
|
+
return 'Some steps work but the journey is incomplete. Visitors may struggle to convert.';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return 'Site has critical failures that impact user conversion.';
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
_getRecommendation(journeyResult) {
|
|
401
|
+
const decision = journeyResult.finalDecision;
|
|
402
|
+
|
|
403
|
+
if (decision === 'SAFE') {
|
|
404
|
+
return '✅ **READY TO LAUNCH** — Critical journey is fully functional. Monitor for regressions.';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (decision === 'RISK') {
|
|
408
|
+
if (journeyResult.goal?.goalReached === false && journeyResult.failedSteps?.length === 0) {
|
|
409
|
+
return '⚠️ **LAUNCH WITH CAUTION** — Help visitors reach the goal (signup, checkout, contact) before launch.';
|
|
410
|
+
}
|
|
411
|
+
return '⚠️ **LAUNCH WITH CAUTION** — Fix identified failures before launch. Test thoroughly.';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (decision === 'DO_NOT_LAUNCH') {
|
|
415
|
+
return '🚫 **DO NOT LAUNCH** — Critical failures detected. Fix issues before attempting deployment.';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return 'Unable to make recommendation.';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
_intentToHumanGoal(intent) {
|
|
422
|
+
switch (intent) {
|
|
423
|
+
case 'saas': return 'Sign up or view pricing';
|
|
424
|
+
case 'shop': return 'Add to cart or begin checkout';
|
|
425
|
+
case 'landing': return 'Send a message or reach contact section';
|
|
426
|
+
default: return 'Find a clear action and proceed';
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
module.exports = { HumanReporter };
|
package/src/guardian/index.js
CHANGED
|
@@ -176,35 +176,38 @@ async function runGuardian(config) {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
// Display verdict
|
|
179
|
+
// Display verdict with evidence-first messaging
|
|
180
180
|
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
181
|
-
|
|
182
|
-
const { decision } = report.finalJudgment;
|
|
181
|
+
|
|
182
|
+
const { decision, reasons } = report.finalJudgment;
|
|
183
183
|
const coverageStr = `${report.summary.coverage}%`;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
184
|
+
|
|
185
|
+
// Map internal to canonical verdict labels for CLI output
|
|
186
|
+
const { toCanonicalVerdict } = require('./verdicts');
|
|
187
|
+
const canonical = toCanonicalVerdict(decision);
|
|
188
|
+
const verdictLabel = canonical === 'READY'
|
|
189
|
+
? 'READY — flows seen end-to-end'
|
|
190
|
+
: canonical === 'FRICTION'
|
|
191
|
+
? 'FRICTION — some flows failed or could not be confirmed'
|
|
192
|
+
: 'DO_NOT_LAUNCH — only limited observations or critical failures'
|
|
193
|
+
|
|
194
|
+
console.log(`\n🔎 Verdict: ${verdictLabel}`);
|
|
195
|
+
console.log(`\n📈 Coverage (link discovery only): ${coverageStr}`);
|
|
194
196
|
console.log(`📄 Pages visited: ${report.summary.visitedPages}`);
|
|
195
|
-
console.log(`❌ Failed pages: ${report.summary.failedPages}`);
|
|
197
|
+
console.log(`❌ Failed pages (server/nav errors): ${report.summary.failedPages}`);
|
|
196
198
|
console.log(`💬 Confidence: ${report.confidence.level}`);
|
|
197
|
-
|
|
198
|
-
console.log(`\
|
|
199
|
-
|
|
199
|
+
|
|
200
|
+
console.log(`\nEvidence and limitations:`);
|
|
201
|
+
reasons.forEach(reason => {
|
|
200
202
|
console.log(` • ${reason}`);
|
|
201
203
|
});
|
|
202
204
|
|
|
203
205
|
console.log(`\n💾 Full report: ${savedReport.reportPath}`);
|
|
204
206
|
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
|
205
207
|
|
|
206
|
-
// Exit with
|
|
207
|
-
const
|
|
208
|
+
// Exit with deterministic codes: OBSERVED=0, PARTIAL=1, INSUFFICIENT_DATA=2
|
|
209
|
+
const { mapExitCodeFromCanonical } = require('./verdicts');
|
|
210
|
+
const exitCode = mapExitCodeFromCanonical(canonical);
|
|
208
211
|
process.exit(exitCode);
|
|
209
212
|
|
|
210
213
|
} catch (err) {
|
|
@@ -27,10 +27,14 @@ function initGuardian(options = {}) {
|
|
|
27
27
|
|
|
28
28
|
// 1. Create policy file
|
|
29
29
|
try {
|
|
30
|
-
const
|
|
30
|
+
const configDir = path.join(cwd, 'config');
|
|
31
|
+
if (!fs.existsSync(configDir)) {
|
|
32
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
const policyPath = path.join(configDir, 'guardian.policy.json');
|
|
31
35
|
|
|
32
36
|
if (fs.existsSync(policyPath)) {
|
|
33
|
-
console.log('⚠️ guardian.policy.json already exists. Skipping.');
|
|
37
|
+
console.log('⚠️ config/guardian.policy.json already exists. Skipping.');
|
|
34
38
|
} else {
|
|
35
39
|
// Load preset
|
|
36
40
|
const presetsDir = path.join(__dirname, '../../policies');
|
|
@@ -56,8 +60,8 @@ function initGuardian(options = {}) {
|
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
fs.writeFileSync(policyPath, policyContent, 'utf-8');
|
|
59
|
-
result.created.push('guardian.policy.json');
|
|
60
|
-
console.log('✅ Created guardian.policy.json');
|
|
63
|
+
result.created.push('config/guardian.policy.json');
|
|
64
|
+
console.log('✅ Created config/guardian.policy.json');
|
|
61
65
|
}
|
|
62
66
|
} catch (error) {
|
|
63
67
|
result.errors.push(`Failed to create policy: ${error.message}`);
|
|
@@ -105,7 +109,7 @@ function initGuardian(options = {}) {
|
|
|
105
109
|
console.log(' 3. Review the generated report:');
|
|
106
110
|
console.log(' Check artifacts/ directory\n');
|
|
107
111
|
console.log(' 4. Customize policy:');
|
|
108
|
-
|
|
112
|
+
console.log(' Edit config/guardian.policy.json\n');
|
|
109
113
|
console.log(' 5. Integrate with CI/CD:');
|
|
110
114
|
console.log(' Use .github/workflows/guardian.yml as template\n');
|
|
111
115
|
console.log('━'.repeat(60) + '\n');
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent Detection Engine
|
|
3
|
+
* Inspects the homepage and classifies site intent.
|
|
4
|
+
* Returns { intent: 'saas'|'shop'|'landing'|'unknown', confidence: 0-100, signals: string[] }
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { GuardianBrowser } = require('./browser');
|
|
8
|
+
|
|
9
|
+
async function detectIntent(url, options = {}) {
|
|
10
|
+
const browser = new GuardianBrowser();
|
|
11
|
+
const timeout = options.timeout || 20000;
|
|
12
|
+
const headless = options.headless !== false;
|
|
13
|
+
const result = { intent: 'unknown', confidence: 0, signals: [] };
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
await browser.launch(timeout, { headless });
|
|
17
|
+
await browser.page.goto(url, { waitUntil: 'load', timeout });
|
|
18
|
+
// Ensure content is rendered before signal extraction
|
|
19
|
+
await browser.page.waitForLoadState('domcontentloaded');
|
|
20
|
+
await browser.page.waitForTimeout(100);
|
|
21
|
+
const signals = await _extractSignals(browser.page);
|
|
22
|
+
const { intent, confidence } = _classifyFromSignals(signals);
|
|
23
|
+
result.intent = intent;
|
|
24
|
+
result.confidence = confidence;
|
|
25
|
+
result.signals = signals;
|
|
26
|
+
return result;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return { intent: 'unknown', confidence: 0, signals: [`error:${err.message}`] };
|
|
29
|
+
} finally {
|
|
30
|
+
try {
|
|
31
|
+
if (browser?.context) await browser.context.close();
|
|
32
|
+
if (browser?.browser) await browser.browser.close();
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function _extractSignals(page) {
|
|
38
|
+
try {
|
|
39
|
+
return await page.evaluate(() => {
|
|
40
|
+
const signals = [];
|
|
41
|
+
|
|
42
|
+
function textIncludes(el, kws) {
|
|
43
|
+
const t = (el.innerText || '').toLowerCase();
|
|
44
|
+
return kws.some(k => t.includes(k));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const anchors = Array.from(document.querySelectorAll('a'));
|
|
48
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
49
|
+
const forms = Array.from(document.querySelectorAll('form'));
|
|
50
|
+
|
|
51
|
+
// URL patterns in anchors
|
|
52
|
+
const urlKWs = ['/pricing', '/signup', '/account/signup', '/shop', '/cart', '/checkout', '/contact'];
|
|
53
|
+
for (const a of anchors) {
|
|
54
|
+
const href = (a.getAttribute('href') || '').toLowerCase();
|
|
55
|
+
urlKWs.forEach(k => { if (href.includes(k)) signals.push(`url:${k}`); });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Keywords: signup/subscribe/pricing
|
|
59
|
+
const saasKWs = ['sign up', 'signup', 'subscribe', 'get started', 'pricing', 'plan'];
|
|
60
|
+
[...anchors, ...buttons].forEach(el => {
|
|
61
|
+
if (textIncludes(el, saasKWs)) signals.push('kw:saas');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Shop keywords: buy/cart/checkout/add to cart/order/purchase
|
|
65
|
+
const shopKWs = ['buy', 'cart', 'checkout', 'add to cart', 'order', 'purchase'];
|
|
66
|
+
[...anchors, ...buttons].forEach(el => {
|
|
67
|
+
if (textIncludes(el, shopKWs)) signals.push('kw:shop');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Landing/contact keywords
|
|
71
|
+
const landingKWs = ['contact', 'get in touch'];
|
|
72
|
+
[...anchors, ...buttons].forEach(el => {
|
|
73
|
+
if (textIncludes(el, landingKWs)) signals.push('kw:landing');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Forms vs payment buttons
|
|
77
|
+
const hasEmailInput = !!document.querySelector('input[type="email"], input[name*="email" i]');
|
|
78
|
+
const hasContactForm = forms.some(f => {
|
|
79
|
+
const txts = f.querySelectorAll('textarea');
|
|
80
|
+
const names = f.querySelectorAll('input[name*="name" i]');
|
|
81
|
+
const emails = f.querySelectorAll('input[type="email"], input[name*="email" i]');
|
|
82
|
+
return emails.length > 0 || (names.length > 0 && txts.length > 0);
|
|
83
|
+
});
|
|
84
|
+
if (hasEmailInput) signals.push('form:email');
|
|
85
|
+
if (hasContactForm) signals.push('form:contact');
|
|
86
|
+
|
|
87
|
+
const hasPaymentBtn = [...buttons, ...anchors].some(el => textIncludes(el, ['buy', 'checkout', 'add to cart', 'purchase']));
|
|
88
|
+
if (hasPaymentBtn) signals.push('btn:payment');
|
|
89
|
+
|
|
90
|
+
// Price indications (numbers with per month/year)
|
|
91
|
+
const bodyText = (document.body.innerText || '').toLowerCase();
|
|
92
|
+
if (/\$\s*\d+/.test(bodyText) || /€\s*\d+/.test(bodyText)) {
|
|
93
|
+
if (bodyText.includes('per month') || bodyText.includes('/month') || bodyText.includes('monthly')) {
|
|
94
|
+
signals.push('price:monthly');
|
|
95
|
+
}
|
|
96
|
+
if (bodyText.includes('per year') || bodyText.includes('/year') || bodyText.includes('annually')) {
|
|
97
|
+
signals.push('price:annual');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Fallback: scan body text for keywords
|
|
102
|
+
if (saasKWs.some(k => bodyText.includes(k))) signals.push('kw:saas');
|
|
103
|
+
if (shopKWs.some(k => bodyText.includes(k))) signals.push('kw:shop');
|
|
104
|
+
if (landingKWs.some(k => bodyText.includes(k))) signals.push('kw:landing');
|
|
105
|
+
|
|
106
|
+
return signals;
|
|
107
|
+
});
|
|
108
|
+
} catch (e) {
|
|
109
|
+
return [`error:${e.message}`];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _classifyFromSignals(signals) {
|
|
114
|
+
let saas = 0, shop = 0, landing = 0;
|
|
115
|
+
|
|
116
|
+
for (const s of signals) {
|
|
117
|
+
if (s.startsWith('kw:saas')) saas += 2;
|
|
118
|
+
if (s.startsWith('url:/pricing')) saas += 3;
|
|
119
|
+
if (s.startsWith('url:/signup')) saas += 3;
|
|
120
|
+
if (s.startsWith('form:email')) saas += 2;
|
|
121
|
+
if (s.startsWith('price:')) saas += 1;
|
|
122
|
+
|
|
123
|
+
if (s.startsWith('kw:shop')) shop += 2;
|
|
124
|
+
if (s.startsWith('btn:payment')) shop += 3;
|
|
125
|
+
if (s.startsWith('url:/shop')) shop += 2;
|
|
126
|
+
if (s.startsWith('url:/cart')) shop += 3;
|
|
127
|
+
if (s.startsWith('url:/checkout')) shop += 3;
|
|
128
|
+
|
|
129
|
+
if (s.startsWith('kw:landing')) landing += 2;
|
|
130
|
+
if (s.startsWith('form:contact')) landing += 3;
|
|
131
|
+
if (s.startsWith('url:/contact')) landing += 2;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const scores = { saas, shop, landing };
|
|
135
|
+
const top = Object.entries(scores).sort((a, b) => b[1] - a[1])[0];
|
|
136
|
+
const maxScore = top ? top[1] : 0;
|
|
137
|
+
const intent = maxScore >= 2 ? top[0] : 'unknown';
|
|
138
|
+
const confidence = Math.min(100, Math.round((maxScore / 10) * 100));
|
|
139
|
+
return { intent, confidence };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
detectIntent,
|
|
144
|
+
_extractSignals,
|
|
145
|
+
_classifyFromSignals
|
|
146
|
+
};
|