@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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 12.3: Human Failure Intelligence
|
|
3
|
+
* Deterministic heuristics to explain failures like a human.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
const STORE_DIR = path.join(os.homedir(), '.odavl-guardian', 'failures');
|
|
11
|
+
const SIGNATURE_FILE = path.join(STORE_DIR, 'signatures.json');
|
|
12
|
+
|
|
13
|
+
function ensureStore() {
|
|
14
|
+
if (!fs.existsSync(STORE_DIR)) {
|
|
15
|
+
fs.mkdirSync(STORE_DIR, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function loadSignatures() {
|
|
20
|
+
ensureStore();
|
|
21
|
+
if (!fs.existsSync(SIGNATURE_FILE)) {
|
|
22
|
+
return { sites: {} };
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(SIGNATURE_FILE, 'utf-8'));
|
|
26
|
+
} catch {
|
|
27
|
+
return { sites: {} };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function saveSignatures(data) {
|
|
32
|
+
ensureStore();
|
|
33
|
+
fs.writeFileSync(SIGNATURE_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getDomain(url) {
|
|
38
|
+
try {
|
|
39
|
+
const u = new URL(url);
|
|
40
|
+
return u.hostname;
|
|
41
|
+
} catch {
|
|
42
|
+
return url;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function classifyFailureStage(stepIndex, totalSteps, goalIndex, success) {
|
|
47
|
+
if (success) return 'AFTER_GOAL';
|
|
48
|
+
if (typeof goalIndex !== 'number') goalIndex = totalSteps - 1;
|
|
49
|
+
if (stepIndex < goalIndex) return 'BEFORE_GOAL';
|
|
50
|
+
if (stepIndex === goalIndex) return 'AT_GOAL';
|
|
51
|
+
return 'AFTER_GOAL';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function determineCause(step) {
|
|
55
|
+
// Deterministic priority
|
|
56
|
+
// 1) CTA_NOT_FOUND
|
|
57
|
+
// 2) ELEMENT_NOT_FOUND on submit
|
|
58
|
+
// 3) TIMEOUT near navigation
|
|
59
|
+
// 4) INTENT_DRIFT
|
|
60
|
+
const code = step?.errorCode || step?.result?.errorCode || step?.status;
|
|
61
|
+
const tags = step?.tags || step?.result?.tags || [];
|
|
62
|
+
const action = (step?.action || step?.name || '').toLowerCase();
|
|
63
|
+
|
|
64
|
+
if (code === 'CTA_NOT_FOUND' || tags.includes('cta')) {
|
|
65
|
+
return { cause: 'Primary action not visible', hint: 'Make the main signup button visible without scrolling.' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if ((code === 'ELEMENT_NOT_FOUND' || code === 'MISSING_ELEMENT') && (tags.includes('submit') || action.includes('submit'))) {
|
|
69
|
+
return { cause: 'Form submission blocked', hint: 'Ensure the submit button exists and is enabled.' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (code === 'TIMEOUT' || (tags.includes('nav') && (code === 'SLOW' || code === 'BLOCKED'))) {
|
|
73
|
+
return { cause: 'Slow or blocked navigation', hint: 'Speed up routing or ensure target page loads reliably.' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (code === 'INTENT_DRIFT' || tags.includes('drift')) {
|
|
77
|
+
return { cause: 'Page no longer matches visitor intent', hint: 'Restore intent-aligned content and CTA on the target page.' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { cause: 'Unknown failure', hint: 'Investigate logs and UI for missing elements or errors.' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function analyzeFailure(journeyResult) {
|
|
84
|
+
const totalSteps = (journeyResult.executedSteps?.length || 0) + (journeyResult.failedSteps?.length || 0);
|
|
85
|
+
const steps = journeyResult.executedSteps || [];
|
|
86
|
+
const failed = journeyResult.failedSteps || [];
|
|
87
|
+
|
|
88
|
+
let failureStepId = null;
|
|
89
|
+
let failureStepIdx = -1;
|
|
90
|
+
let failureStep = null;
|
|
91
|
+
|
|
92
|
+
if (failed.length > 0) {
|
|
93
|
+
// failedSteps may be an array of IDs; locate the first failing step
|
|
94
|
+
const firstFailId = typeof failed[0] === 'string' ? failed[0] : failed[0]?.id || failed[0];
|
|
95
|
+
failureStepId = firstFailId;
|
|
96
|
+
failureStepIdx = steps.findIndex(s => s.id === firstFailId);
|
|
97
|
+
if (failureStepIdx === -1 && typeof firstFailId === 'number') {
|
|
98
|
+
failureStepIdx = firstFailId;
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// Otherwise, look for error status in executed steps
|
|
102
|
+
failureStepIdx = steps.findIndex(s => s.status === 'error' || s.status === 'timeout' || s.result?.status === 'error');
|
|
103
|
+
if (failureStepIdx !== -1) failureStepId = steps[failureStepIdx].id;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (failureStepIdx === -1) {
|
|
107
|
+
// No explicit failure found; classify after goal if success, else before goal as default
|
|
108
|
+
const stage = classifyFailureStage(totalSteps - 1, totalSteps, totalSteps - 1, journeyResult.goal?.goalReached === true || journeyResult.success === true);
|
|
109
|
+
const causeInfo = { cause: 'Unknown failure', hint: 'Investigate logs and UI for missing elements or errors.' };
|
|
110
|
+
return {
|
|
111
|
+
failureStepId: failureStepId,
|
|
112
|
+
failureStage: stage,
|
|
113
|
+
cause: causeInfo.cause,
|
|
114
|
+
hint: causeInfo.hint,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
failureStep = steps[failureStepIdx] || null;
|
|
119
|
+
const goalReached = journeyResult.goal?.goalReached === true || journeyResult.success === true;
|
|
120
|
+
let stage = 'AFTER_GOAL';
|
|
121
|
+
if (!goalReached) {
|
|
122
|
+
// Default to BEFORE_GOAL when goal not reached unless explicitly marked as goal step
|
|
123
|
+
const goalStepId = journeyResult.goal?.goalStepId;
|
|
124
|
+
if (goalStepId && (failureStep?.id === goalStepId || failureStepIdx === goalStepId)) {
|
|
125
|
+
stage = 'AT_GOAL';
|
|
126
|
+
} else {
|
|
127
|
+
stage = 'BEFORE_GOAL';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const causeInfo = determineCause(failureStep);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
failureStepId: failureStep?.id || failureStepIdx,
|
|
134
|
+
failureStage: stage,
|
|
135
|
+
cause: causeInfo.cause,
|
|
136
|
+
hint: causeInfo.hint,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildSignature(info) {
|
|
141
|
+
return `${info.failureStage}|${info.cause}|${info.failureStepId ?? 'unknown'}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function recordSignature(siteUrl, info) {
|
|
145
|
+
const data = loadSignatures();
|
|
146
|
+
const domain = getDomain(siteUrl);
|
|
147
|
+
if (!data.sites[domain]) data.sites[domain] = { signatures: {} };
|
|
148
|
+
const sig = buildSignature(info);
|
|
149
|
+
const entry = data.sites[domain].signatures[sig] || { count: 0, lastSeen: null };
|
|
150
|
+
entry.count += 1;
|
|
151
|
+
entry.lastSeen = new Date().toISOString();
|
|
152
|
+
data.sites[domain].signatures[sig] = entry;
|
|
153
|
+
saveSignatures(data);
|
|
154
|
+
return { signature: sig, count: entry.count };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getSignatureCount(siteUrl, info) {
|
|
158
|
+
const data = loadSignatures();
|
|
159
|
+
const domain = getDomain(siteUrl);
|
|
160
|
+
const sig = buildSignature(info);
|
|
161
|
+
return data.sites[domain]?.signatures?.[sig]?.count || 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
analyzeFailure,
|
|
166
|
+
determineCause,
|
|
167
|
+
classifyFailureStage,
|
|
168
|
+
recordSignature,
|
|
169
|
+
getSignatureCount,
|
|
170
|
+
buildSignature,
|
|
171
|
+
loadSignatures,
|
|
172
|
+
saveSignatures,
|
|
173
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-Run State Tracking
|
|
3
|
+
*
|
|
4
|
+
* Detects if this is the user's first invocation of Guardian
|
|
5
|
+
* Applies conservative "golden path" profile on first run
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
const FIRST_RUN_STATE_DIR = path.join(os.homedir(), '.odavl-guardian');
|
|
13
|
+
const FIRST_RUN_MARKER = path.join(FIRST_RUN_STATE_DIR, 'first-run-complete.json');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if this is the user's first run
|
|
17
|
+
*/
|
|
18
|
+
function isFirstRun() {
|
|
19
|
+
return !fs.existsSync(FIRST_RUN_MARKER);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mark first run as complete
|
|
24
|
+
*/
|
|
25
|
+
function markFirstRunComplete() {
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.existsSync(FIRST_RUN_STATE_DIR)) {
|
|
28
|
+
fs.mkdirSync(FIRST_RUN_STATE_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
fs.writeFileSync(FIRST_RUN_MARKER, JSON.stringify({
|
|
31
|
+
completedAt: new Date().toISOString(),
|
|
32
|
+
version: require('../../package.json').version
|
|
33
|
+
}), 'utf8');
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// Silent fail if we can't write state
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get first-run execution profile
|
|
41
|
+
* Conservative settings for initial scan
|
|
42
|
+
*/
|
|
43
|
+
function getFirstRunProfile() {
|
|
44
|
+
return {
|
|
45
|
+
// Timeouts: more generous for first run
|
|
46
|
+
timeout: 25000, // 25s (vs default 20s)
|
|
47
|
+
// Disable resource-intensive options
|
|
48
|
+
parallel: 1, // Single-threaded
|
|
49
|
+
failFast: false,
|
|
50
|
+
fast: false, // Don't skip for speed
|
|
51
|
+
// Minimal discovery
|
|
52
|
+
enableDiscovery: false,
|
|
53
|
+
enableCrawl: true, // Light crawl only
|
|
54
|
+
maxPages: 10, // Fewer pages
|
|
55
|
+
maxDepth: 2, // Shallower
|
|
56
|
+
// Evidence capture
|
|
57
|
+
enableScreenshots: true,
|
|
58
|
+
enableTrace: false, // Traces can slow things down
|
|
59
|
+
headful: false, // Headless is faster
|
|
60
|
+
// Safety
|
|
61
|
+
includeUniversal: false,
|
|
62
|
+
// CI mode off for first run (more readable output)
|
|
63
|
+
ciMode: false
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Apply first-run profile to config
|
|
69
|
+
* Merges conservative defaults without overwriting user intent on --url
|
|
70
|
+
*/
|
|
71
|
+
function applyFirstRunProfile(userConfig) {
|
|
72
|
+
if (!isFirstRun()) {
|
|
73
|
+
return userConfig; // Not first run; use as-is
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const profile = getFirstRunProfile();
|
|
77
|
+
return {
|
|
78
|
+
...profile,
|
|
79
|
+
...userConfig, // User overrides profile
|
|
80
|
+
baseUrl: userConfig.baseUrl // Preserve required --url
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
isFirstRun,
|
|
86
|
+
markFirstRunComplete,
|
|
87
|
+
getFirstRunProfile,
|
|
88
|
+
applyFirstRunProfile
|
|
89
|
+
};
|
|
@@ -41,9 +41,14 @@ function printWelcome(label = 'ODAVL Guardian') {
|
|
|
41
41
|
console.log(lines.join('\n'));
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function printFirstRunHint() {
|
|
45
|
+
console.log("\nTip: Try 'guardian smoke <url>' for a fast CI-ready check.\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
module.exports = {
|
|
45
49
|
isFirstRun,
|
|
46
50
|
hasRunBefore,
|
|
47
51
|
markAsRun,
|
|
48
|
-
printWelcome
|
|
52
|
+
printWelcome,
|
|
53
|
+
printFirstRunHint
|
|
49
54
|
};
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
const VALID_SUBCOMMANDS = [
|
|
7
7
|
'init', 'protect', 'reality', 'attempt', 'baseline',
|
|
8
|
-
'presets', 'evaluate', 'version', 'flow', 'scan', 'smoke'
|
|
8
|
+
'presets', 'template', 'list', 'cleanup', 'evaluate', 'version', 'flow', 'scan', 'smoke', 'check',
|
|
9
|
+
'journey-scan', 'journey', 'live', 'plan', 'upgrade', 'ci', 'feedback',
|
|
10
|
+
'sites', 'users', 'audit', 'export', 'recipe'
|
|
9
11
|
];
|
|
10
12
|
|
|
11
13
|
const VALID_GLOBAL_FLAGS = [
|
|
@@ -14,13 +16,25 @@ const VALID_GLOBAL_FLAGS = [
|
|
|
14
16
|
|
|
15
17
|
const VALID_SUBCOMMAND_FLAGS = {
|
|
16
18
|
'scan': ['--url', '--preset', '--artifacts', '--policy', '--headful', '--no-trace', '--no-screenshots', '--watch', '-w', '--fast', '--fail-fast', '--timeout-profile', '--attempts', '--parallel', '--help', '-h'],
|
|
19
|
+
'journey-scan': ['--url', '--preset', '--out', '--artifacts', '--timeout', '--headful', '--help', '-h'],
|
|
20
|
+
'journey': ['--url', '--preset', '--out', '--artifacts', '--timeout', '--headful', '--help', '-h'],
|
|
21
|
+
'live': ['--url', '--preset', '--out', '--artifacts', '--timeout', '--interval', '--cooldown', '--headful', '--help', '-h'],
|
|
17
22
|
'protect': ['--url', '--policy', '--webhook', '--watch', '-w', '--fast', '--fail-fast', '--timeout-profile', '--attempts', '--parallel', '--help', '-h'],
|
|
18
|
-
'reality': ['--url', '--attempts', '--artifacts', '--policy', '--discover', '--universal', '--webhook', '--headful', '--watch', '-w', '--no-trace', '--no-screenshots', '--fast', '--fail-fast', '--timeout-profile', '--parallel', '--help', '-h'],
|
|
23
|
+
'reality': ['--url', '--attempts', '--artifacts', '--policy', '--preset', '--discover', '--universal', '--webhook', '--headful', '--watch', '-w', '--no-trace', '--no-screenshots', '--fast', '--fail-fast', '--timeout-profile', '--parallel', '--help', '-h', '--max-pages', '--max-depth', '--timeout'],
|
|
19
24
|
'attempt': ['--url', '--attempt', '--artifacts', '--headful', '--no-trace', '--no-screenshots', '--help', '-h'],
|
|
20
25
|
'smoke': ['--url', '--headful', '--budget-ms', '--help', '-h'],
|
|
26
|
+
'check': ['--url', '--headful', '--budget-ms', '--help', '-h'],
|
|
21
27
|
'baseline': [],
|
|
22
28
|
'init': ['--preset', '--help', '-h'],
|
|
23
|
-
'
|
|
29
|
+
'list': ['--artifacts', '--failed', '--site', '--limit', '--help', '-h'],
|
|
30
|
+
'cleanup': ['--artifacts', '--older-than', '--keep-latest', '--failed-only', '--help', '-h'],
|
|
31
|
+
'presets': ['--help', '-h'],
|
|
32
|
+
'template': ['--output', '--help', '-h'],
|
|
33
|
+
'sites': ['--project', '--help', '-h'],
|
|
34
|
+
'users': ['--help', '-h'],
|
|
35
|
+
'audit': ['--limit', '--action', '--user', '--help', '-h'],
|
|
36
|
+
'export': ['--format', '--output', '--help', '-h'],
|
|
37
|
+
'recipe': ['--url', '--file', '--out', '--force', '--help', '-h']
|
|
24
38
|
};
|
|
25
39
|
|
|
26
40
|
function validateFlags(argv) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const { getFounderBadgeHTML } = require('../founder/founder-tracker');
|
|
8
9
|
|
|
9
10
|
class GuardianHTMLReporter {
|
|
10
11
|
/**
|
|
@@ -207,6 +208,7 @@ class GuardianHTMLReporter {
|
|
|
207
208
|
<div class="subtitle">Market Reality Testing Report</div>
|
|
208
209
|
</div>
|
|
209
210
|
|
|
211
|
+
${getFounderBadgeHTML()}
|
|
210
212
|
${this.generateVerdictSection(jsonReport)}
|
|
211
213
|
${this.generateMetricsSection(jsonReport)}
|
|
212
214
|
${this.generateReasonsSection(jsonReport)}
|