@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.
- package/CHANGELOG.md +86 -2
- package/README.md +155 -97
- package/bin/guardian.js +1345 -60
- package/config/README.md +59 -0
- package/config/profiles/landing-demo.yaml +16 -0
- package/package.json +21 -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 +568 -7
- package/src/guardian/attempt-registry.js +42 -1
- package/src/guardian/attempt-relevance.js +106 -0
- package/src/guardian/attempt.js +24 -0
- package/src/guardian/baseline.js +12 -4
- package/src/guardian/breakage-intelligence.js +1 -0
- package/src/guardian/ci-cli.js +121 -0
- package/src/guardian/ci-output.js +4 -3
- package/src/guardian/cli-summary.js +79 -92
- package/src/guardian/config-loader.js +162 -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 +6 -1
- package/src/guardian/flag-validator.js +17 -3
- 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/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 +341 -81
- package/src/guardian/pattern-analyzer.js +348 -0
- package/src/guardian/policy.js +80 -3
- package/src/guardian/preset-loader.js +9 -6
- package/src/guardian/reality.js +1278 -117
- 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/scan-presets.js +100 -11
- package/src/guardian/selector-fallbacks.js +394 -0
- package/src/guardian/semantic-contact-finder.js +2 -1
- package/src/guardian/site-introspection.js +257 -0
- package/src/guardian/smoke.js +2 -2
- package/src/guardian/snapshot-schema.js +25 -1
- package/src/guardian/snapshot.js +46 -2
- package/src/guardian/stability-scorer.js +169 -0
- package/src/guardian/template-command.js +184 -0
- package/src/guardian/text-formatters.js +426 -0
- package/src/guardian/verdict.js +320 -0
- package/src/guardian/verdicts.js +74 -0
- package/src/guardian/watch-runner.js +3 -7
- 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
|
@@ -45,7 +45,7 @@ const SNAPSHOT_SCHEMA_VERSION = 'v1';
|
|
|
45
45
|
* @property {string} attemptId - unique attempt identifier
|
|
46
46
|
* @property {string} attemptName - human-readable name
|
|
47
47
|
* @property {string} goal - what the user tried to achieve
|
|
48
|
-
* @property {string} outcome - 'SUCCESS', 'FAILURE',
|
|
48
|
+
* @property {string} outcome - 'SUCCESS', 'FAILURE', 'FRICTION', 'NOT_APPLICABLE', 'DISCOVERY_FAILED', 'SKIPPED'
|
|
49
49
|
* @property {number} totalDurationMs - elapsed time
|
|
50
50
|
* @property {number} stepCount - how many steps executed
|
|
51
51
|
* @property {number} failedStepIndex - index of first failed step, or -1 if all succeeded
|
|
@@ -53,6 +53,10 @@ const SNAPSHOT_SCHEMA_VERSION = 'v1';
|
|
|
53
53
|
* @property {ValidatorResult[]} [validators] - soft failure detectors (Phase 2)
|
|
54
54
|
* @property {number} [softFailureCount] - count of failed validators
|
|
55
55
|
* @property {string} [riskCategory] - 'LEAD', 'REVENUE', 'TRUST/UX' (Phase 2)
|
|
56
|
+
* @property {string} [skipReason] - reason if SKIPPED, NOT_APPLICABLE, or DISCOVERY_FAILED
|
|
57
|
+
* @property {string[]} [selectorChainTried] - selectors attempted during discovery
|
|
58
|
+
* @property {Object} [discoverySignals] - element discovery signals and heuristics
|
|
59
|
+
* @property {string} [finalSelection] - which selector/strategy successfully matched element
|
|
56
60
|
*/
|
|
57
61
|
|
|
58
62
|
/**
|
|
@@ -133,6 +137,13 @@ const SNAPSHOT_SCHEMA_VERSION = 'v1';
|
|
|
133
137
|
* @typedef {Object} MarketRealitySnapshot
|
|
134
138
|
* @property {string} schemaVersion - always 'v1'
|
|
135
139
|
* @property {SnapshotMeta} meta
|
|
140
|
+
* @property {Object} [verdict] - unified run-level verdict
|
|
141
|
+
* @property {('READY'|'DO_NOT_LAUNCH'|'FRICTION')} verdict.verdict
|
|
142
|
+
* @property {{ level: ('low'|'medium'|'high'), score: number, reasons: string[] }} verdict.confidence
|
|
143
|
+
* @property {string} verdict.why
|
|
144
|
+
* @property {string[]} verdict.keyFindings
|
|
145
|
+
* @property {{ screenshots?: string[], traces?: string[], reportPaths?: string[], affectedPages?: string[] }} verdict.evidence
|
|
146
|
+
* @property {string[]} verdict.limits
|
|
136
147
|
* @property {CrawlResult} [crawl]
|
|
137
148
|
* @property {AttemptResult[]} attempts
|
|
138
149
|
* @property {Array} flows
|
|
@@ -166,6 +177,7 @@ function createEmptySnapshot(baseUrl, runId, toolVersion) {
|
|
|
166
177
|
attempts: [],
|
|
167
178
|
flows: [],
|
|
168
179
|
signals: [],
|
|
180
|
+
verdict: null,
|
|
169
181
|
riskSummary: {
|
|
170
182
|
totalSoftFailures: 0,
|
|
171
183
|
totalFriction: 0,
|
|
@@ -253,6 +265,18 @@ function validateSnapshot(snapshot) {
|
|
|
253
265
|
errors.push('Missing baseline section');
|
|
254
266
|
}
|
|
255
267
|
|
|
268
|
+
// Basic verdict validation (if present)
|
|
269
|
+
if (snapshot.verdict) {
|
|
270
|
+
const v = snapshot.verdict;
|
|
271
|
+
const allowed = ['READY', 'DO_NOT_LAUNCH', 'FRICTION'];
|
|
272
|
+
if (!v.verdict || !allowed.includes(v.verdict)) {
|
|
273
|
+
errors.push('Invalid verdict.verdict');
|
|
274
|
+
}
|
|
275
|
+
if (!v.confidence || typeof v.confidence.score !== 'number' || v.confidence.score < 0 || v.confidence.score > 1) {
|
|
276
|
+
errors.push('Invalid verdict.confidence.score');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
256
280
|
return {
|
|
257
281
|
valid: errors.length === 0,
|
|
258
282
|
errors
|
package/src/guardian/snapshot.js
CHANGED
|
@@ -34,10 +34,44 @@ class SnapshotBuilder {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Set unified verdict object
|
|
39
|
+
*/
|
|
40
|
+
setVerdict(verdict) {
|
|
41
|
+
if (!verdict) return;
|
|
42
|
+
this.snapshot.verdict = {
|
|
43
|
+
verdict: verdict.verdict,
|
|
44
|
+
confidence: verdict.confidence,
|
|
45
|
+
why: verdict.why || '',
|
|
46
|
+
keyFindings: Array.isArray(verdict.keyFindings) ? verdict.keyFindings.slice(0, 7) : [],
|
|
47
|
+
evidence: verdict.evidence || {},
|
|
48
|
+
limits: Array.isArray(verdict.limits) ? verdict.limits.slice(0, 6) : []
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
/**
|
|
38
53
|
* Add attempt result to snapshot
|
|
39
54
|
*/
|
|
40
55
|
addAttempt(attemptResult, artifactDir) {
|
|
56
|
+
// Handle NOT_APPLICABLE and DISCOVERY_FAILED attempts
|
|
57
|
+
if (attemptResult.outcome === 'NOT_APPLICABLE' || attemptResult.outcome === 'DISCOVERY_FAILED') {
|
|
58
|
+
this.snapshot.attempts.push({
|
|
59
|
+
attemptId: attemptResult.attemptId,
|
|
60
|
+
attemptName: attemptResult.attemptName,
|
|
61
|
+
goal: attemptResult.goal,
|
|
62
|
+
outcome: attemptResult.outcome,
|
|
63
|
+
executed: false,
|
|
64
|
+
skipReason: attemptResult.skipReason || (attemptResult.outcome === 'NOT_APPLICABLE' ? 'Feature not present' : 'Element discovery failed'),
|
|
65
|
+
skipReasonCode: attemptResult.skipReasonCode,
|
|
66
|
+
discoverySignals: attemptResult.discoverySignals || {},
|
|
67
|
+
totalDurationMs: attemptResult.totalDurationMs || 0,
|
|
68
|
+
stepCount: attemptResult.stepCount || 0,
|
|
69
|
+
failedStepIndex: -1,
|
|
70
|
+
friction: null
|
|
71
|
+
});
|
|
72
|
+
return; // Don't create signals for non-applicable attempts
|
|
73
|
+
}
|
|
74
|
+
|
|
41
75
|
// Phase 7.4: Handle SKIPPED attempts (don't add as signal)
|
|
42
76
|
if (attemptResult.outcome === 'SKIPPED') {
|
|
43
77
|
this.snapshot.attempts.push({
|
|
@@ -45,7 +79,9 @@ class SnapshotBuilder {
|
|
|
45
79
|
attemptName: attemptResult.attemptName,
|
|
46
80
|
goal: attemptResult.goal,
|
|
47
81
|
outcome: 'SKIPPED',
|
|
82
|
+
executed: false,
|
|
48
83
|
skipReason: attemptResult.skipReason || 'Prerequisites not met',
|
|
84
|
+
skipReasonCode: attemptResult.skipReasonCode,
|
|
49
85
|
totalDurationMs: 0,
|
|
50
86
|
stepCount: 0,
|
|
51
87
|
failedStepIndex: -1,
|
|
@@ -71,10 +107,17 @@ class SnapshotBuilder {
|
|
|
71
107
|
attemptName: attemptResult.attemptName,
|
|
72
108
|
goal: attemptResult.goal,
|
|
73
109
|
outcome: attemptResult.outcome,
|
|
110
|
+
executed: true,
|
|
111
|
+
discoverySignals: attemptResult.discoverySignals || {},
|
|
74
112
|
totalDurationMs: attemptResult.attemptResult?.totalDurationMs || 0,
|
|
75
113
|
stepCount: (attemptResult.steps || []).length,
|
|
76
114
|
failedStepIndex: (attemptResult.steps || []).findIndex(s => s.status === 'failed'),
|
|
77
|
-
friction: attemptResult.friction || null
|
|
115
|
+
friction: attemptResult.friction || null,
|
|
116
|
+
evidenceSummary: {
|
|
117
|
+
screenshots: (attemptResult.steps || []).reduce((sum, s) => sum + (Array.isArray(s.screenshots) ? s.screenshots.length : 0), 0),
|
|
118
|
+
validators: Array.isArray(attemptResult.validators) ? attemptResult.validators.length : 0,
|
|
119
|
+
tracesCaptured: attemptResult.tracePath ? 1 : 0
|
|
120
|
+
}
|
|
78
121
|
});
|
|
79
122
|
|
|
80
123
|
// Track artifacts
|
|
@@ -82,7 +125,8 @@ class SnapshotBuilder {
|
|
|
82
125
|
this.snapshot.evidence.attemptArtifacts[attemptResult.attemptId] = {
|
|
83
126
|
reportJson: path.join(attemptResult.attemptId, 'attempt-report.json'),
|
|
84
127
|
reportHtml: path.join(attemptResult.attemptId, 'attempt-report.html'),
|
|
85
|
-
screenshotDir: path.join(attemptResult.attemptId, 'attempt-screenshots')
|
|
128
|
+
screenshotDir: path.join(attemptResult.attemptId, 'attempt-screenshots'),
|
|
129
|
+
attemptJson: attemptResult.attemptJsonPath ? path.relative(artifactDir, attemptResult.attemptJsonPath) : undefined
|
|
86
130
|
};
|
|
87
131
|
}
|
|
88
132
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stability Scorer - Real-world reliability metrics
|
|
3
|
+
*
|
|
4
|
+
* Measures how stable a journey run was:
|
|
5
|
+
* - Per-step stability (transient vs deterministic failures)
|
|
6
|
+
* - Overall run stability score (0-100)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Classify error type for determining if it's transient or deterministic
|
|
11
|
+
* @param {string} errorMessage - Error message from step
|
|
12
|
+
* @returns {object} - { isTransient: boolean, classification: string }
|
|
13
|
+
*/
|
|
14
|
+
function classifyErrorType(errorMessage) {
|
|
15
|
+
if (!errorMessage) return { isTransient: false, classification: 'UNKNOWN' };
|
|
16
|
+
|
|
17
|
+
const msg = errorMessage.toLowerCase();
|
|
18
|
+
|
|
19
|
+
// Transient errors (safe to retry)
|
|
20
|
+
if (msg.includes('timeout') || msg.includes('timed out')) {
|
|
21
|
+
return { isTransient: true, classification: 'TIMEOUT' };
|
|
22
|
+
}
|
|
23
|
+
if (msg.includes('navigation') && (msg.includes('timeout') || msg.includes('closed'))) {
|
|
24
|
+
return { isTransient: true, classification: 'NAVIGATION_TIMEOUT' };
|
|
25
|
+
}
|
|
26
|
+
if (msg.includes('detached') || msg.includes('frame')) {
|
|
27
|
+
return { isTransient: true, classification: 'DETACHED_FRAME' };
|
|
28
|
+
}
|
|
29
|
+
if (msg.includes('econnrefused') || msg.includes('network') || msg.includes('socket')) {
|
|
30
|
+
return { isTransient: true, classification: 'NETWORK_ERROR' };
|
|
31
|
+
}
|
|
32
|
+
if (msg.includes('connection') && (msg.includes('reset') || msg.includes('closed'))) {
|
|
33
|
+
return { isTransient: true, classification: 'CONNECTION_ERROR' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Deterministic errors (don't retry)
|
|
37
|
+
if (msg.includes('not found') && (msg.includes('cta') || msg.includes('element'))) {
|
|
38
|
+
return { isTransient: false, classification: 'ELEMENT_NOT_FOUND' };
|
|
39
|
+
}
|
|
40
|
+
if (msg.includes('not visible')) {
|
|
41
|
+
return { isTransient: false, classification: 'ELEMENT_NOT_VISIBLE' };
|
|
42
|
+
}
|
|
43
|
+
if (msg.includes('cta') && msg.includes('found')) {
|
|
44
|
+
return { isTransient: false, classification: 'CTA_NOT_FOUND' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Default: assume transient to be safe
|
|
48
|
+
return { isTransient: true, classification: 'UNKNOWN' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compute stability score for a single step
|
|
53
|
+
* @param {object} step - Executed step result { id, name, success, attemptNumber, error }
|
|
54
|
+
* @returns {object} - { attempts, finalStatus, stable, confidence, errorType }
|
|
55
|
+
*/
|
|
56
|
+
function scoreStepStability(step) {
|
|
57
|
+
const attempts = step.attemptNumber || 1;
|
|
58
|
+
const finalStatus = step.success ? 'SUCCESS' : 'FAILED';
|
|
59
|
+
const errorType = classifyErrorType(step.error);
|
|
60
|
+
|
|
61
|
+
// Determine stability
|
|
62
|
+
let stable = true;
|
|
63
|
+
let confidence = 100;
|
|
64
|
+
|
|
65
|
+
if (finalStatus === 'SUCCESS') {
|
|
66
|
+
if (attempts > 1) {
|
|
67
|
+
// Success after retries = transient failure
|
|
68
|
+
stable = true; // The step ultimately worked
|
|
69
|
+
confidence = Math.max(30, 100 - (attempts - 1) * 20);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
// Step failed all retries
|
|
73
|
+
stable = false;
|
|
74
|
+
confidence = 10; // Very low confidence in a consistently failing step
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
stepId: step.id,
|
|
79
|
+
attempts,
|
|
80
|
+
finalStatus,
|
|
81
|
+
stable,
|
|
82
|
+
confidence,
|
|
83
|
+
errorType: errorType.classification,
|
|
84
|
+
isTransient: errorType.isTransient
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Compute overall run stability score (0-100)
|
|
90
|
+
* @param {object} result - Journey result with executedSteps array
|
|
91
|
+
* @returns {number} - Stability score 0-100
|
|
92
|
+
*/
|
|
93
|
+
function computeRunStabilityScore(result) {
|
|
94
|
+
const steps = result.executedSteps || [];
|
|
95
|
+
|
|
96
|
+
if (steps.length === 0) return 0;
|
|
97
|
+
|
|
98
|
+
// Calculate step-level stability
|
|
99
|
+
const stepScores = steps.map(scoreStepStability);
|
|
100
|
+
|
|
101
|
+
// Count how many steps needed retries
|
|
102
|
+
const stepsWithRetries = stepScores.filter(s => s.attempts > 1).length;
|
|
103
|
+
const failedSteps = stepScores.filter(s => s.finalStatus === 'FAILED').length;
|
|
104
|
+
|
|
105
|
+
// Scoring algorithm:
|
|
106
|
+
// - Start at 100
|
|
107
|
+
// - Deduct 10 points per step that needed retries
|
|
108
|
+
// - Deduct 30 points per failed step
|
|
109
|
+
// - Floor at 0
|
|
110
|
+
let score = 100;
|
|
111
|
+
score -= stepsWithRetries * 10;
|
|
112
|
+
score -= failedSteps * 30;
|
|
113
|
+
score = Math.max(0, score);
|
|
114
|
+
|
|
115
|
+
// Consistency check: if goalReached varies, reduce score
|
|
116
|
+
// (This is a simple heuristic; more complex consistency checks could be added)
|
|
117
|
+
const hasInconsistency = false; // Would need multiple runs to detect
|
|
118
|
+
if (hasInconsistency) {
|
|
119
|
+
score = Math.max(0, score - 20);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return Math.round(score);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build stability report from journey result
|
|
127
|
+
* @param {object} result - Journey scan result
|
|
128
|
+
* @returns {object} - Stability report with scores and metrics
|
|
129
|
+
*/
|
|
130
|
+
function buildStabilityReport(result) {
|
|
131
|
+
const steps = result.executedSteps || [];
|
|
132
|
+
const stepStability = steps.map(scoreStepStability);
|
|
133
|
+
const runScore = computeRunStabilityScore(result);
|
|
134
|
+
|
|
135
|
+
const metrics = {
|
|
136
|
+
totalSteps: steps.length,
|
|
137
|
+
succeededSteps: stepStability.filter(s => s.finalStatus === 'SUCCESS').length,
|
|
138
|
+
failedSteps: stepStability.filter(s => s.finalStatus === 'FAILED').length,
|
|
139
|
+
stepsWithRetries: stepStability.filter(s => s.attempts > 1).length,
|
|
140
|
+
totalAttempts: stepStability.reduce((sum, s) => sum + s.attempts, 0)
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
runStabilityScore: runScore,
|
|
145
|
+
metrics,
|
|
146
|
+
stepStability,
|
|
147
|
+
assessment: assessStability(runScore)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Assess stability level based on score
|
|
153
|
+
* @param {number} score - Stability score 0-100
|
|
154
|
+
* @returns {string} - Assessment: 'excellent' | 'good' | 'fair' | 'poor'
|
|
155
|
+
*/
|
|
156
|
+
function assessStability(score) {
|
|
157
|
+
if (score >= 80) return 'excellent';
|
|
158
|
+
if (score >= 60) return 'good';
|
|
159
|
+
if (score >= 40) return 'fair';
|
|
160
|
+
return 'poor';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
classifyErrorType,
|
|
165
|
+
scoreStepStability,
|
|
166
|
+
computeRunStabilityScore,
|
|
167
|
+
buildStabilityReport,
|
|
168
|
+
assessStability
|
|
169
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Command
|
|
3
|
+
*
|
|
4
|
+
* Generate minimal config templates:
|
|
5
|
+
* guardian template saas
|
|
6
|
+
* guardian template shop
|
|
7
|
+
* guardian template landing
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const TEMPLATES = {
|
|
14
|
+
saas: {
|
|
15
|
+
name: 'SaaS Startup Check',
|
|
16
|
+
description: 'Verify core SaaS flows: signup, login, dashboard',
|
|
17
|
+
intent: {
|
|
18
|
+
category: 'product_type',
|
|
19
|
+
value: 'saas',
|
|
20
|
+
confidence: 1.0
|
|
21
|
+
},
|
|
22
|
+
journeys: [
|
|
23
|
+
{
|
|
24
|
+
id: 'signup_flow',
|
|
25
|
+
name: 'User Signup',
|
|
26
|
+
description: 'Verify signup journey completes',
|
|
27
|
+
steps: [
|
|
28
|
+
{ action: 'navigate', target: '/', name: 'Home' },
|
|
29
|
+
{ action: 'click', target: 'a[href*="/signup"], button:has-text("Sign up")', name: 'Click signup' },
|
|
30
|
+
{ action: 'wait', ms: 2000 },
|
|
31
|
+
{ action: 'screenshot', name: 'Signup form loaded' }
|
|
32
|
+
],
|
|
33
|
+
criticality: 'CRITICAL',
|
|
34
|
+
onFailure: 'FAIL'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'landing_page',
|
|
38
|
+
name: 'Landing Page Load',
|
|
39
|
+
description: 'Verify homepage loads without errors',
|
|
40
|
+
steps: [
|
|
41
|
+
{ action: 'navigate', target: '/', name: 'Home' },
|
|
42
|
+
{ action: 'screenshot', name: 'Homepage loaded' }
|
|
43
|
+
],
|
|
44
|
+
criticality: 'CRITICAL',
|
|
45
|
+
onFailure: 'WARN'
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
policy: {
|
|
49
|
+
failOnSeverity: 'CRITICAL',
|
|
50
|
+
maxWarnings: 10,
|
|
51
|
+
requireBaseline: false
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
shop: {
|
|
56
|
+
name: 'E-Commerce Shop Check',
|
|
57
|
+
description: 'Verify core shop flows: browse, add to cart, checkout',
|
|
58
|
+
intent: {
|
|
59
|
+
category: 'product_type',
|
|
60
|
+
value: 'ecommerce',
|
|
61
|
+
confidence: 1.0
|
|
62
|
+
},
|
|
63
|
+
journeys: [
|
|
64
|
+
{
|
|
65
|
+
id: 'browse_products',
|
|
66
|
+
name: 'Browse Products',
|
|
67
|
+
description: 'Verify product catalog loads',
|
|
68
|
+
steps: [
|
|
69
|
+
{ action: 'navigate', target: '/', name: 'Home' },
|
|
70
|
+
{ action: 'click', target: 'a[href*="/shop"], a[href*="/products"], button:has-text("Shop")', name: 'Go to shop' },
|
|
71
|
+
{ action: 'wait', ms: 2000 },
|
|
72
|
+
{ action: 'screenshot', name: 'Products page' }
|
|
73
|
+
],
|
|
74
|
+
criticality: 'CRITICAL',
|
|
75
|
+
onFailure: 'FAIL'
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'add_to_cart',
|
|
79
|
+
name: 'Add to Cart',
|
|
80
|
+
description: 'Verify add-to-cart flow',
|
|
81
|
+
steps: [
|
|
82
|
+
{ action: 'navigate', target: '/', name: 'Home' },
|
|
83
|
+
{ action: 'click', target: 'a[href*="/shop"], a[href*="/products"]', name: 'Go to shop' },
|
|
84
|
+
{ action: 'wait', ms: 2000 },
|
|
85
|
+
{ action: 'click', target: 'button:has-text("Add"), button:has-text("Cart")', name: 'Add item' },
|
|
86
|
+
{ action: 'screenshot', name: 'Cart updated' }
|
|
87
|
+
],
|
|
88
|
+
criticality: 'CRITICAL',
|
|
89
|
+
onFailure: 'WARN'
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
policy: {
|
|
93
|
+
failOnSeverity: 'CRITICAL',
|
|
94
|
+
maxWarnings: 10,
|
|
95
|
+
requireBaseline: false
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
landing: {
|
|
100
|
+
name: 'Landing Page Check',
|
|
101
|
+
description: 'Verify landing page loads and core CTAs work',
|
|
102
|
+
intent: {
|
|
103
|
+
category: 'product_type',
|
|
104
|
+
value: 'landing',
|
|
105
|
+
confidence: 1.0
|
|
106
|
+
},
|
|
107
|
+
journeys: [
|
|
108
|
+
{
|
|
109
|
+
id: 'page_load',
|
|
110
|
+
name: 'Page Load',
|
|
111
|
+
description: 'Verify landing page loads',
|
|
112
|
+
steps: [
|
|
113
|
+
{ action: 'navigate', target: '/', name: 'Home' },
|
|
114
|
+
{ action: 'screenshot', name: 'Landing page' }
|
|
115
|
+
],
|
|
116
|
+
criticality: 'CRITICAL',
|
|
117
|
+
onFailure: 'FAIL'
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'cta_click',
|
|
121
|
+
name: 'Call-to-Action',
|
|
122
|
+
description: 'Verify main CTA is clickable',
|
|
123
|
+
steps: [
|
|
124
|
+
{ action: 'navigate', target: '/', name: 'Home' },
|
|
125
|
+
{ action: 'click', target: 'button:has-text("Get started"), button:has-text("Start"), button:has-text("Sign up"), a[href*="/signup"]', name: 'Click CTA' },
|
|
126
|
+
{ action: 'screenshot', name: 'CTA works' }
|
|
127
|
+
],
|
|
128
|
+
criticality: 'CRITICAL',
|
|
129
|
+
onFailure: 'WARN'
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
policy: {
|
|
133
|
+
failOnSeverity: 'CRITICAL',
|
|
134
|
+
maxWarnings: 5,
|
|
135
|
+
requireBaseline: false
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Generate config file from template
|
|
142
|
+
* @param {string} templateName - saas, shop, or landing
|
|
143
|
+
* @param {object} options - Generation options
|
|
144
|
+
* @returns {object} Result with generated file path
|
|
145
|
+
*/
|
|
146
|
+
function generateTemplate(templateName, options = {}) {
|
|
147
|
+
const cwd = options.cwd || process.cwd();
|
|
148
|
+
const outputFile = options.output || `guardian-${templateName}.json`;
|
|
149
|
+
const outputPath = path.join(cwd, outputFile);
|
|
150
|
+
|
|
151
|
+
if (!TEMPLATES[templateName]) {
|
|
152
|
+
throw new Error(`Unknown template: ${templateName}. Available: ${Object.keys(TEMPLATES).join(', ')}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const template = TEMPLATES[templateName];
|
|
156
|
+
const content = JSON.stringify(template, null, 2);
|
|
157
|
+
|
|
158
|
+
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
success: true,
|
|
162
|
+
template: templateName,
|
|
163
|
+
outputPath,
|
|
164
|
+
message: `Generated ${templateName} template: ${outputFile}`
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* List available templates
|
|
170
|
+
* @returns {array} Array of template names with descriptions
|
|
171
|
+
*/
|
|
172
|
+
function listTemplates() {
|
|
173
|
+
return Object.entries(TEMPLATES).map(([name, config]) => ({
|
|
174
|
+
name,
|
|
175
|
+
description: config.description,
|
|
176
|
+
journeys: config.journeys.length
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
generateTemplate,
|
|
182
|
+
listTemplates,
|
|
183
|
+
TEMPLATES
|
|
184
|
+
};
|