@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,588 +1,588 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ODAVL Guardian Site Intelligence Engine
|
|
3
|
-
* Phase 10: Core intelligence layer that understands sites before attempting flows
|
|
4
|
-
*
|
|
5
|
-
* CRITICAL: This is NOT heuristics. This is CORE INTELLIGENCE.
|
|
6
|
-
* Guardian must UNDERSTAND before ACTING.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Site types Guardian can classify
|
|
11
|
-
*/
|
|
12
|
-
const SITE_TYPES = {
|
|
13
|
-
MARKETING: 'marketing',
|
|
14
|
-
SAAS: 'saas_application',
|
|
15
|
-
ECOMMERCE: 'ecommerce',
|
|
16
|
-
DOCUMENTATION: 'documentation',
|
|
17
|
-
BLOG: 'blog',
|
|
18
|
-
UNKNOWN: 'unknown'
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Capabilities a site might support
|
|
23
|
-
*/
|
|
24
|
-
const CAPABILITIES = {
|
|
25
|
-
SUPPORTS_LOGIN: 'supports_login',
|
|
26
|
-
SUPPORTS_SIGNUP: 'supports_signup',
|
|
27
|
-
SUPPORTS_CHECKOUT: 'supports_checkout',
|
|
28
|
-
SUPPORTS_FORMS: 'supports_forms',
|
|
29
|
-
SUPPORTS_PRIMARY_CTA: 'supports_primary_cta',
|
|
30
|
-
SUPPORTS_LANGUAGE_SWITCH: 'supports_language_switch',
|
|
31
|
-
SUPPORTS_PRICING: 'supports_pricing',
|
|
32
|
-
SUPPORTS_CONTACT: 'supports_contact',
|
|
33
|
-
SUPPORTS_NEWSLETTER: 'supports_newsletter'
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Analyze a page to classify site type and detect capabilities
|
|
38
|
-
* @param {Page} page - Playwright page object
|
|
39
|
-
* @param {string} baseUrl - Base URL of the site
|
|
40
|
-
* @returns {Promise<Object>} Intelligence report
|
|
41
|
-
*/
|
|
42
|
-
async function analyzeSite(page, baseUrl) {
|
|
43
|
-
const intelligence = {
|
|
44
|
-
siteType: SITE_TYPES.UNKNOWN,
|
|
45
|
-
confidence: 0,
|
|
46
|
-
detectedSignals: [],
|
|
47
|
-
capabilities: {},
|
|
48
|
-
flowApplicability: {},
|
|
49
|
-
timestamp: new Date().toISOString()
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
// Navigate to base URL if not already there
|
|
54
|
-
const currentUrl = page.url();
|
|
55
|
-
if (!currentUrl.startsWith(baseUrl)) {
|
|
56
|
-
await page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Collect signals from the page
|
|
60
|
-
const signals = await collectPageSignals(page, baseUrl);
|
|
61
|
-
intelligence.detectedSignals = signals;
|
|
62
|
-
|
|
63
|
-
// Classify site type
|
|
64
|
-
const classification = classifySiteType(signals);
|
|
65
|
-
intelligence.siteType = classification.siteType;
|
|
66
|
-
intelligence.confidence = classification.confidence;
|
|
67
|
-
|
|
68
|
-
// Detect capabilities
|
|
69
|
-
intelligence.capabilities = detectCapabilities(signals);
|
|
70
|
-
|
|
71
|
-
// Determine flow applicability
|
|
72
|
-
intelligence.flowApplicability = determineFlowApplicability(intelligence.capabilities, intelligence.siteType);
|
|
73
|
-
|
|
74
|
-
return intelligence;
|
|
75
|
-
} catch (error) {
|
|
76
|
-
console.warn(`⚠️ Site intelligence analysis failed: ${error.message}`);
|
|
77
|
-
// Return conservative defaults on failure
|
|
78
|
-
return {
|
|
79
|
-
...intelligence,
|
|
80
|
-
siteType: SITE_TYPES.UNKNOWN,
|
|
81
|
-
confidence: 0,
|
|
82
|
-
detectedSignals: [{ type: 'error', message: error.message }],
|
|
83
|
-
capabilities: getConservativeCapabilities(),
|
|
84
|
-
flowApplicability: getConservativeFlowApplicability()
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Collect signals from the page for classification
|
|
91
|
-
* @param {Page} page - Playwright page
|
|
92
|
-
* @param {string} baseUrl - Base URL
|
|
93
|
-
* @returns {Promise<Array>} Array of detected signals
|
|
94
|
-
*/
|
|
95
|
-
async function collectPageSignals(page, baseUrl) {
|
|
96
|
-
const signals = [];
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
// URL pattern analysis
|
|
100
|
-
const url = page.url();
|
|
101
|
-
const urlSignals = analyzeUrlPatterns(url, baseUrl);
|
|
102
|
-
signals.push(...urlSignals);
|
|
103
|
-
|
|
104
|
-
// DOM-based detection
|
|
105
|
-
const domSignals = await page.evaluate(() => {
|
|
106
|
-
const signals = [];
|
|
107
|
-
|
|
108
|
-
// Login indicators (CSS + text matches)
|
|
109
|
-
const loginSelectors = [
|
|
110
|
-
'a[href*="login"]',
|
|
111
|
-
'a[href*="signin"]',
|
|
112
|
-
'input[type="password"]',
|
|
113
|
-
'[data-testid*="login"]',
|
|
114
|
-
'[data-guardian*="login"]'
|
|
115
|
-
];
|
|
116
|
-
for (const selector of loginSelectors) {
|
|
117
|
-
const elements = document.querySelectorAll(selector);
|
|
118
|
-
if (elements.length > 0) {
|
|
119
|
-
signals.push({
|
|
120
|
-
type: 'login_indicator',
|
|
121
|
-
selector,
|
|
122
|
-
count: elements.length,
|
|
123
|
-
text: Array.from(elements).slice(0, 3).map(el => el.textContent?.trim()).filter(Boolean)
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const loginTextCandidates = ['Login', 'Sign in'];
|
|
128
|
-
for (const text of loginTextCandidates) {
|
|
129
|
-
const elements = Array.from(document.querySelectorAll('button, a')).filter(el => (el.textContent || '').toLowerCase().includes(text.toLowerCase()));
|
|
130
|
-
if (elements.length > 0) {
|
|
131
|
-
signals.push({
|
|
132
|
-
type: 'login_indicator',
|
|
133
|
-
selector: `text:${text}`,
|
|
134
|
-
count: elements.length,
|
|
135
|
-
text: elements.slice(0, 3).map(el => el.textContent?.trim()).filter(Boolean)
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Signup indicators (CSS + text matches)
|
|
141
|
-
const signupSelectors = [
|
|
142
|
-
'a[href*="signup"]',
|
|
143
|
-
'a[href*="register"]',
|
|
144
|
-
'a[href*="join"]',
|
|
145
|
-
'[data-testid*="signup"]',
|
|
146
|
-
'[data-guardian*="signup"]'
|
|
147
|
-
];
|
|
148
|
-
|
|
149
|
-
for (const selector of signupSelectors) {
|
|
150
|
-
const elements = document.querySelectorAll(selector);
|
|
151
|
-
if (elements.length > 0) {
|
|
152
|
-
signals.push({
|
|
153
|
-
type: 'signup_indicator',
|
|
154
|
-
selector,
|
|
155
|
-
count: elements.length,
|
|
156
|
-
text: Array.from(elements).slice(0, 3).map(el => el.textContent?.trim()).filter(Boolean)
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const signupTextCandidates = ['Sign up', 'Register', 'Get started'];
|
|
161
|
-
for (const text of signupTextCandidates) {
|
|
162
|
-
const elements = Array.from(document.querySelectorAll('button, a')).filter(el => (el.textContent || '').toLowerCase().includes(text.toLowerCase()));
|
|
163
|
-
if (elements.length > 0) {
|
|
164
|
-
signals.push({
|
|
165
|
-
type: 'signup_indicator',
|
|
166
|
-
selector: `text:${text}`,
|
|
167
|
-
count: elements.length,
|
|
168
|
-
text: elements.slice(0, 3).map(el => el.textContent?.trim()).filter(Boolean)
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// E-commerce indicators (CSS + text matches)
|
|
174
|
-
const commerceSelectors = [
|
|
175
|
-
'a[href*="cart"]',
|
|
176
|
-
'a[href*="checkout"]',
|
|
177
|
-
'a[href*="shop"]',
|
|
178
|
-
'[data-testid*="cart"]',
|
|
179
|
-
'[data-testid*="checkout"]'
|
|
180
|
-
];
|
|
181
|
-
|
|
182
|
-
for (const selector of commerceSelectors) {
|
|
183
|
-
const elements = document.querySelectorAll(selector);
|
|
184
|
-
if (elements.length > 0) {
|
|
185
|
-
signals.push({
|
|
186
|
-
type: 'commerce_indicator',
|
|
187
|
-
selector,
|
|
188
|
-
count: elements.length,
|
|
189
|
-
text: Array.from(elements).slice(0, 3).map(el => el.textContent?.trim()).filter(Boolean)
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
const commerceTextCandidates = ['Add to cart', 'Buy', 'Purchase'];
|
|
194
|
-
for (const text of commerceTextCandidates) {
|
|
195
|
-
const elements = Array.from(document.querySelectorAll('button, a')).filter(el => (el.textContent || '').toLowerCase().includes(text.toLowerCase()));
|
|
196
|
-
if (elements.length > 0) {
|
|
197
|
-
signals.push({
|
|
198
|
-
type: 'commerce_indicator',
|
|
199
|
-
selector: `text:${text}`,
|
|
200
|
-
count: elements.length,
|
|
201
|
-
text: elements.slice(0, 3).map(el => el.textContent?.trim()).filter(Boolean)
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Pricing indicators
|
|
207
|
-
const pricingSelectors = [
|
|
208
|
-
'a[href*="pricing"]',
|
|
209
|
-
'a[href*="plans"]',
|
|
210
|
-
'[class*="price"]',
|
|
211
|
-
'[class*="pricing"]'
|
|
212
|
-
];
|
|
213
|
-
|
|
214
|
-
for (const selector of pricingSelectors) {
|
|
215
|
-
const elements = document.querySelectorAll(selector);
|
|
216
|
-
if (elements.length > 0) {
|
|
217
|
-
signals.push({
|
|
218
|
-
type: 'pricing_indicator',
|
|
219
|
-
selector,
|
|
220
|
-
count: elements.length
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Dashboard/App indicators
|
|
226
|
-
const dashboardSelectors = [
|
|
227
|
-
'a[href*="/app"]',
|
|
228
|
-
'a[href*="/dashboard"]',
|
|
229
|
-
'a[href*="/console"]'
|
|
230
|
-
];
|
|
231
|
-
|
|
232
|
-
for (const selector of dashboardSelectors) {
|
|
233
|
-
const elements = document.querySelectorAll(selector);
|
|
234
|
-
if (elements.length > 0) {
|
|
235
|
-
signals.push({
|
|
236
|
-
type: 'dashboard_indicator',
|
|
237
|
-
selector,
|
|
238
|
-
count: elements.length
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Documentation indicators
|
|
244
|
-
const docsSelectors = [
|
|
245
|
-
'a[href*="/docs"]',
|
|
246
|
-
'a[href*="/documentation"]',
|
|
247
|
-
'a[href*="/guide"]'
|
|
248
|
-
];
|
|
249
|
-
|
|
250
|
-
for (const selector of docsSelectors) {
|
|
251
|
-
const elements = document.querySelectorAll(selector);
|
|
252
|
-
if (elements.length > 0) {
|
|
253
|
-
signals.push({
|
|
254
|
-
type: 'docs_indicator',
|
|
255
|
-
selector,
|
|
256
|
-
count: elements.length
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Form indicators
|
|
262
|
-
const forms = document.querySelectorAll('form');
|
|
263
|
-
if (forms.length > 0) {
|
|
264
|
-
signals.push({
|
|
265
|
-
type: 'form_indicator',
|
|
266
|
-
count: forms.length
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Contact form indicators
|
|
271
|
-
const contactSelectors = [
|
|
272
|
-
'a[href*="contact"]',
|
|
273
|
-
'form[action*="contact"]',
|
|
274
|
-
'input[name*="email"]',
|
|
275
|
-
'[data-guardian*="contact"]'
|
|
276
|
-
];
|
|
277
|
-
|
|
278
|
-
for (const selector of contactSelectors) {
|
|
279
|
-
const elements = document.querySelectorAll(selector);
|
|
280
|
-
if (elements.length > 0) {
|
|
281
|
-
signals.push({
|
|
282
|
-
type: 'contact_indicator',
|
|
283
|
-
selector,
|
|
284
|
-
count: elements.length
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Primary CTA detection
|
|
290
|
-
const ctaSelectors = [
|
|
291
|
-
'button',
|
|
292
|
-
'a[class*="button"]',
|
|
293
|
-
'a[class*="cta"]',
|
|
294
|
-
'[role="button"]'
|
|
295
|
-
];
|
|
296
|
-
|
|
297
|
-
let ctaCount = 0;
|
|
298
|
-
for (const selector of ctaSelectors) {
|
|
299
|
-
ctaCount += document.querySelectorAll(selector).length;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (ctaCount > 0) {
|
|
303
|
-
signals.push({
|
|
304
|
-
type: 'cta_indicator',
|
|
305
|
-
count: ctaCount
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return signals;
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
signals.push(...domSignals);
|
|
313
|
-
|
|
314
|
-
return signals;
|
|
315
|
-
} catch (error) {
|
|
316
|
-
console.warn(`⚠️ Signal collection failed: ${error.message}`);
|
|
317
|
-
return signals;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Analyze URL patterns for classification hints
|
|
323
|
-
* @param {string} url - Current URL
|
|
324
|
-
* @param {string} baseUrl - Base URL
|
|
325
|
-
* @returns {Array} URL-based signals
|
|
326
|
-
*/
|
|
327
|
-
function analyzeUrlPatterns(url, baseUrl) {
|
|
328
|
-
const signals = [];
|
|
329
|
-
const urlLower = url.toLowerCase();
|
|
330
|
-
|
|
331
|
-
if (urlLower.includes('/app') || urlLower.includes('/dashboard')) {
|
|
332
|
-
signals.push({ type: 'url_pattern', pattern: 'app/dashboard', indicator: 'saas' });
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (urlLower.includes('/shop') || urlLower.includes('/store') || urlLower.includes('/products')) {
|
|
336
|
-
signals.push({ type: 'url_pattern', pattern: 'shop/store', indicator: 'ecommerce' });
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (urlLower.includes('/docs') || urlLower.includes('/documentation')) {
|
|
340
|
-
signals.push({ type: 'url_pattern', pattern: 'docs', indicator: 'documentation' });
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (urlLower.includes('/blog') || urlLower.includes('/articles')) {
|
|
344
|
-
signals.push({ type: 'url_pattern', pattern: 'blog', indicator: 'blog' });
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return signals;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Classify site type based on collected signals
|
|
352
|
-
* @param {Array} signals - Collected signals
|
|
353
|
-
* @returns {Object} Classification result with type and confidence
|
|
354
|
-
*/
|
|
355
|
-
function classifySiteType(signals) {
|
|
356
|
-
const scores = {
|
|
357
|
-
[SITE_TYPES.MARKETING]: 0,
|
|
358
|
-
[SITE_TYPES.SAAS]: 0,
|
|
359
|
-
[SITE_TYPES.ECOMMERCE]: 0,
|
|
360
|
-
[SITE_TYPES.DOCUMENTATION]: 0,
|
|
361
|
-
[SITE_TYPES.BLOG]: 0
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
// Score based on signals
|
|
365
|
-
for (const signal of signals) {
|
|
366
|
-
switch (signal.type) {
|
|
367
|
-
case 'login_indicator':
|
|
368
|
-
scores[SITE_TYPES.SAAS] += 3;
|
|
369
|
-
break;
|
|
370
|
-
case 'signup_indicator':
|
|
371
|
-
scores[SITE_TYPES.SAAS] += 2;
|
|
372
|
-
scores[SITE_TYPES.MARKETING] += 1;
|
|
373
|
-
break;
|
|
374
|
-
case 'commerce_indicator':
|
|
375
|
-
scores[SITE_TYPES.ECOMMERCE] += 5;
|
|
376
|
-
break;
|
|
377
|
-
case 'dashboard_indicator':
|
|
378
|
-
scores[SITE_TYPES.SAAS] += 4;
|
|
379
|
-
break;
|
|
380
|
-
case 'docs_indicator':
|
|
381
|
-
scores[SITE_TYPES.DOCUMENTATION] += 5;
|
|
382
|
-
break;
|
|
383
|
-
case 'pricing_indicator':
|
|
384
|
-
scores[SITE_TYPES.SAAS] += 2;
|
|
385
|
-
scores[SITE_TYPES.MARKETING] += 1;
|
|
386
|
-
break;
|
|
387
|
-
case 'contact_indicator':
|
|
388
|
-
scores[SITE_TYPES.MARKETING] += 2;
|
|
389
|
-
break;
|
|
390
|
-
case 'cta_indicator':
|
|
391
|
-
scores[SITE_TYPES.MARKETING] += 1;
|
|
392
|
-
break;
|
|
393
|
-
case 'url_pattern':
|
|
394
|
-
if (signal.indicator === 'saas') scores[SITE_TYPES.SAAS] += 3;
|
|
395
|
-
if (signal.indicator === 'ecommerce') scores[SITE_TYPES.ECOMMERCE] += 3;
|
|
396
|
-
if (signal.indicator === 'documentation') scores[SITE_TYPES.DOCUMENTATION] += 3;
|
|
397
|
-
if (signal.indicator === 'blog') scores[SITE_TYPES.BLOG] += 3;
|
|
398
|
-
break;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Find highest score
|
|
403
|
-
let maxScore = 0;
|
|
404
|
-
let siteType = SITE_TYPES.UNKNOWN;
|
|
405
|
-
|
|
406
|
-
for (const [type, score] of Object.entries(scores)) {
|
|
407
|
-
if (score > maxScore) {
|
|
408
|
-
maxScore = score;
|
|
409
|
-
siteType = type;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Calculate confidence (0-1)
|
|
414
|
-
const totalScore = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
415
|
-
const confidence = totalScore > 0 ? Math.min(maxScore / totalScore, 1) : 0;
|
|
416
|
-
|
|
417
|
-
// If no strong signals, default to marketing with low confidence
|
|
418
|
-
if (maxScore === 0) {
|
|
419
|
-
return {
|
|
420
|
-
siteType: SITE_TYPES.MARKETING,
|
|
421
|
-
confidence: 0.3
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return {
|
|
426
|
-
siteType,
|
|
427
|
-
confidence: Math.round(confidence * 100) / 100
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Detect what capabilities the site actually supports
|
|
433
|
-
* @param {Array} signals - Collected signals
|
|
434
|
-
* @returns {Object} Capability map with evidence
|
|
435
|
-
*/
|
|
436
|
-
function detectCapabilities(signals) {
|
|
437
|
-
const capabilities = {};
|
|
438
|
-
|
|
439
|
-
// Login detection
|
|
440
|
-
const hasLoginSignals = signals.some(s => s.type === 'login_indicator');
|
|
441
|
-
capabilities[CAPABILITIES.SUPPORTS_LOGIN] = {
|
|
442
|
-
supported: hasLoginSignals,
|
|
443
|
-
confidence: hasLoginSignals ? 0.9 : 0.1,
|
|
444
|
-
evidence: signals.filter(s => s.type === 'login_indicator').slice(0, 2)
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
// Signup detection
|
|
448
|
-
const hasSignupSignals = signals.some(s => s.type === 'signup_indicator');
|
|
449
|
-
capabilities[CAPABILITIES.SUPPORTS_SIGNUP] = {
|
|
450
|
-
supported: hasSignupSignals,
|
|
451
|
-
confidence: hasSignupSignals ? 0.9 : 0.1,
|
|
452
|
-
evidence: signals.filter(s => s.type === 'signup_indicator').slice(0, 2)
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
// Checkout detection
|
|
456
|
-
const hasCommerceSignals = signals.some(s => s.type === 'commerce_indicator');
|
|
457
|
-
capabilities[CAPABILITIES.SUPPORTS_CHECKOUT] = {
|
|
458
|
-
supported: hasCommerceSignals,
|
|
459
|
-
confidence: hasCommerceSignals ? 0.9 : 0.1,
|
|
460
|
-
evidence: signals.filter(s => s.type === 'commerce_indicator').slice(0, 2)
|
|
461
|
-
};
|
|
462
|
-
|
|
463
|
-
// Forms detection
|
|
464
|
-
const hasFormSignals = signals.some(s => s.type === 'form_indicator');
|
|
465
|
-
capabilities[CAPABILITIES.SUPPORTS_FORMS] = {
|
|
466
|
-
supported: hasFormSignals,
|
|
467
|
-
confidence: hasFormSignals ? 0.8 : 0.2,
|
|
468
|
-
evidence: signals.filter(s => s.type === 'form_indicator').slice(0, 1)
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
// Primary CTA detection
|
|
472
|
-
const hasCtaSignals = signals.some(s => s.type === 'cta_indicator');
|
|
473
|
-
capabilities[CAPABILITIES.SUPPORTS_PRIMARY_CTA] = {
|
|
474
|
-
supported: hasCtaSignals,
|
|
475
|
-
confidence: hasCtaSignals ? 0.8 : 0.2,
|
|
476
|
-
evidence: signals.filter(s => s.type === 'cta_indicator').slice(0, 1)
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
// Pricing detection
|
|
480
|
-
const hasPricingSignals = signals.some(s => s.type === 'pricing_indicator');
|
|
481
|
-
capabilities[CAPABILITIES.SUPPORTS_PRICING] = {
|
|
482
|
-
supported: hasPricingSignals,
|
|
483
|
-
confidence: hasPricingSignals ? 0.8 : 0.2,
|
|
484
|
-
evidence: signals.filter(s => s.type === 'pricing_indicator').slice(0, 2)
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
// Contact detection
|
|
488
|
-
const hasContactSignals = signals.some(s => s.type === 'contact_indicator');
|
|
489
|
-
capabilities[CAPABILITIES.SUPPORTS_CONTACT] = {
|
|
490
|
-
supported: hasContactSignals,
|
|
491
|
-
confidence: hasContactSignals ? 0.8 : 0.2,
|
|
492
|
-
evidence: signals.filter(s => s.type === 'contact_indicator').slice(0, 2)
|
|
493
|
-
};
|
|
494
|
-
|
|
495
|
-
return capabilities;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Determine which flows are applicable based on capabilities
|
|
500
|
-
* @param {Object} capabilities - Detected capabilities
|
|
501
|
-
* @param {string} siteType - Classified site type
|
|
502
|
-
* @returns {Object} Flow applicability map
|
|
503
|
-
*/
|
|
504
|
-
function determineFlowApplicability(capabilities, siteType) {
|
|
505
|
-
const applicability = {};
|
|
506
|
-
|
|
507
|
-
// Signup flow
|
|
508
|
-
const supportsSignup = capabilities[CAPABILITIES.SUPPORTS_SIGNUP]?.supported;
|
|
509
|
-
applicability.signup_flow = {
|
|
510
|
-
applicable: supportsSignup === true,
|
|
511
|
-
reason: supportsSignup ? 'Signup elements detected' : 'No signup elements found',
|
|
512
|
-
confidence: capabilities[CAPABILITIES.SUPPORTS_SIGNUP]?.confidence || 0
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
// Login flow
|
|
516
|
-
const supportsLogin = capabilities[CAPABILITIES.SUPPORTS_LOGIN]?.supported;
|
|
517
|
-
applicability.login_flow = {
|
|
518
|
-
applicable: supportsLogin === true,
|
|
519
|
-
reason: supportsLogin ? 'Login elements detected' : 'No login elements found',
|
|
520
|
-
confidence: capabilities[CAPABILITIES.SUPPORTS_LOGIN]?.confidence || 0
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
// Checkout flow
|
|
524
|
-
const supportsCheckout = capabilities[CAPABILITIES.SUPPORTS_CHECKOUT]?.supported;
|
|
525
|
-
applicability.checkout_flow = {
|
|
526
|
-
applicable: supportsCheckout === true,
|
|
527
|
-
reason: supportsCheckout ? 'Checkout/commerce elements detected' : 'No checkout elements found',
|
|
528
|
-
confidence: capabilities[CAPABILITIES.SUPPORTS_CHECKOUT]?.confidence || 0
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
return applicability;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Get conservative capabilities when analysis fails
|
|
536
|
-
* @returns {Object} Conservative capability map
|
|
537
|
-
*/
|
|
538
|
-
function getConservativeCapabilities() {
|
|
539
|
-
return {
|
|
540
|
-
[CAPABILITIES.SUPPORTS_LOGIN]: { supported: false, confidence: 0, evidence: [] },
|
|
541
|
-
[CAPABILITIES.SUPPORTS_SIGNUP]: { supported: false, confidence: 0, evidence: [] },
|
|
542
|
-
[CAPABILITIES.SUPPORTS_CHECKOUT]: { supported: false, confidence: 0, evidence: [] },
|
|
543
|
-
[CAPABILITIES.SUPPORTS_FORMS]: { supported: false, confidence: 0, evidence: [] },
|
|
544
|
-
[CAPABILITIES.SUPPORTS_PRIMARY_CTA]: { supported: true, confidence: 0.5, evidence: [] },
|
|
545
|
-
[CAPABILITIES.SUPPORTS_PRICING]: { supported: false, confidence: 0, evidence: [] },
|
|
546
|
-
[CAPABILITIES.SUPPORTS_CONTACT]: { supported: false, confidence: 0, evidence: [] }
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* Get conservative flow applicability when analysis fails
|
|
552
|
-
* @returns {Object} Conservative applicability map
|
|
553
|
-
*/
|
|
554
|
-
function getConservativeFlowApplicability() {
|
|
555
|
-
return {
|
|
556
|
-
signup_flow: { applicable: false, reason: 'Analysis failed; conservative default', confidence: 0 },
|
|
557
|
-
login_flow: { applicable: false, reason: 'Analysis failed; conservative default', confidence: 0 },
|
|
558
|
-
checkout_flow: { applicable: false, reason: 'Analysis failed; conservative default', confidence: 0 }
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Check if a specific flow is applicable based on intelligence
|
|
564
|
-
* @param {Object} intelligence - Site intelligence data
|
|
565
|
-
* @param {string} flowId - Flow identifier
|
|
566
|
-
* @returns {boolean} True if flow is applicable
|
|
567
|
-
*/
|
|
568
|
-
function isFlowApplicable(intelligence, flowId) {
|
|
569
|
-
if (!intelligence || !intelligence.flowApplicability) {
|
|
570
|
-
// Conservative: if no intelligence, don't run the flow
|
|
571
|
-
return false;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const applicability = intelligence.flowApplicability[flowId];
|
|
575
|
-
if (!applicability) {
|
|
576
|
-
// Unknown flow, conservative approach
|
|
577
|
-
return false;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
return applicability.applicable === true;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
module.exports = {
|
|
584
|
-
analyzeSite,
|
|
585
|
-
isFlowApplicable,
|
|
586
|
-
SITE_TYPES,
|
|
587
|
-
CAPABILITIES
|
|
588
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* ODAVL Guardian Site Intelligence Engine
|
|
3
|
+
* Phase 10: Core intelligence layer that understands sites before attempting flows
|
|
4
|
+
*
|
|
5
|
+
* CRITICAL: This is NOT heuristics. This is CORE INTELLIGENCE.
|
|
6
|
+
* Guardian must UNDERSTAND before ACTING.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Site types Guardian can classify
|
|
11
|
+
*/
|
|
12
|
+
const SITE_TYPES = {
|
|
13
|
+
MARKETING: 'marketing',
|
|
14
|
+
SAAS: 'saas_application',
|
|
15
|
+
ECOMMERCE: 'ecommerce',
|
|
16
|
+
DOCUMENTATION: 'documentation',
|
|
17
|
+
BLOG: 'blog',
|
|
18
|
+
UNKNOWN: 'unknown'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Capabilities a site might support
|
|
23
|
+
*/
|
|
24
|
+
const CAPABILITIES = {
|
|
25
|
+
SUPPORTS_LOGIN: 'supports_login',
|
|
26
|
+
SUPPORTS_SIGNUP: 'supports_signup',
|
|
27
|
+
SUPPORTS_CHECKOUT: 'supports_checkout',
|
|
28
|
+
SUPPORTS_FORMS: 'supports_forms',
|
|
29
|
+
SUPPORTS_PRIMARY_CTA: 'supports_primary_cta',
|
|
30
|
+
SUPPORTS_LANGUAGE_SWITCH: 'supports_language_switch',
|
|
31
|
+
SUPPORTS_PRICING: 'supports_pricing',
|
|
32
|
+
SUPPORTS_CONTACT: 'supports_contact',
|
|
33
|
+
SUPPORTS_NEWSLETTER: 'supports_newsletter'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Analyze a page to classify site type and detect capabilities
|
|
38
|
+
* @param {Page} page - Playwright page object
|
|
39
|
+
* @param {string} baseUrl - Base URL of the site
|
|
40
|
+
* @returns {Promise<Object>} Intelligence report
|
|
41
|
+
*/
|
|
42
|
+
async function analyzeSite(page, baseUrl) {
|
|
43
|
+
const intelligence = {
|
|
44
|
+
siteType: SITE_TYPES.UNKNOWN,
|
|
45
|
+
confidence: 0,
|
|
46
|
+
detectedSignals: [],
|
|
47
|
+
capabilities: {},
|
|
48
|
+
flowApplicability: {},
|
|
49
|
+
timestamp: new Date().toISOString()
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Navigate to base URL if not already there
|
|
54
|
+
const currentUrl = page.url();
|
|
55
|
+
if (!currentUrl.startsWith(baseUrl)) {
|
|
56
|
+
await page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Collect signals from the page
|
|
60
|
+
const signals = await collectPageSignals(page, baseUrl);
|
|
61
|
+
intelligence.detectedSignals = signals;
|
|
62
|
+
|
|
63
|
+
// Classify site type
|
|
64
|
+
const classification = classifySiteType(signals);
|
|
65
|
+
intelligence.siteType = classification.siteType;
|
|
66
|
+
intelligence.confidence = classification.confidence;
|
|
67
|
+
|
|
68
|
+
// Detect capabilities
|
|
69
|
+
intelligence.capabilities = detectCapabilities(signals);
|
|
70
|
+
|
|
71
|
+
// Determine flow applicability
|
|
72
|
+
intelligence.flowApplicability = determineFlowApplicability(intelligence.capabilities, intelligence.siteType);
|
|
73
|
+
|
|
74
|
+
return intelligence;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.warn(`⚠️ Site intelligence analysis failed: ${error.message}`);
|
|
77
|
+
// Return conservative defaults on failure
|
|
78
|
+
return {
|
|
79
|
+
...intelligence,
|
|
80
|
+
siteType: SITE_TYPES.UNKNOWN,
|
|
81
|
+
confidence: 0,
|
|
82
|
+
detectedSignals: [{ type: 'error', message: error.message }],
|
|
83
|
+
capabilities: getConservativeCapabilities(),
|
|
84
|
+
flowApplicability: getConservativeFlowApplicability()
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Collect signals from the page for classification
|
|
91
|
+
* @param {Page} page - Playwright page
|
|
92
|
+
* @param {string} baseUrl - Base URL
|
|
93
|
+
* @returns {Promise<Array>} Array of detected signals
|
|
94
|
+
*/
|
|
95
|
+
async function collectPageSignals(page, baseUrl) {
|
|
96
|
+
const signals = [];
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
// URL pattern analysis
|
|
100
|
+
const url = page.url();
|
|
101
|
+
const urlSignals = analyzeUrlPatterns(url, baseUrl);
|
|
102
|
+
signals.push(...urlSignals);
|
|
103
|
+
|
|
104
|
+
// DOM-based detection
|
|
105
|
+
const domSignals = await page.evaluate(() => {
|
|
106
|
+
const signals = [];
|
|
107
|
+
|
|
108
|
+
// Login indicators (CSS + text matches)
|
|
109
|
+
const loginSelectors = [
|
|
110
|
+
'a[href*="login"]',
|
|
111
|
+
'a[href*="signin"]',
|
|
112
|
+
'input[type="password"]',
|
|
113
|
+
'[data-testid*="login"]',
|
|
114
|
+
'[data-guardian*="login"]'
|
|
115
|
+
];
|
|
116
|
+
for (const selector of loginSelectors) {
|
|
117
|
+
const elements = document.querySelectorAll(selector);
|
|
118
|
+
if (elements.length > 0) {
|
|
119
|
+
signals.push({
|
|
120
|
+
type: 'login_indicator',
|
|
121
|
+
selector,
|
|
122
|
+
count: elements.length,
|
|
123
|
+
text: Array.from(elements).slice(0, 3).map(el => el.textContent?.trim()).filter(Boolean)
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const loginTextCandidates = ['Login', 'Sign in'];
|
|
128
|
+
for (const text of loginTextCandidates) {
|
|
129
|
+
const elements = Array.from(document.querySelectorAll('button, a')).filter(el => (el.textContent || '').toLowerCase().includes(text.toLowerCase()));
|
|
130
|
+
if (elements.length > 0) {
|
|
131
|
+
signals.push({
|
|
132
|
+
type: 'login_indicator',
|
|
133
|
+
selector: `text:${text}`,
|
|
134
|
+
count: elements.length,
|
|
135
|
+
text: elements.slice(0, 3).map(el => el.textContent?.trim()).filter(Boolean)
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Signup indicators (CSS + text matches)
|
|
141
|
+
const signupSelectors = [
|
|
142
|
+
'a[href*="signup"]',
|
|
143
|
+
'a[href*="register"]',
|
|
144
|
+
'a[href*="join"]',
|
|
145
|
+
'[data-testid*="signup"]',
|
|
146
|
+
'[data-guardian*="signup"]'
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
for (const selector of signupSelectors) {
|
|
150
|
+
const elements = document.querySelectorAll(selector);
|
|
151
|
+
if (elements.length > 0) {
|
|
152
|
+
signals.push({
|
|
153
|
+
type: 'signup_indicator',
|
|
154
|
+
selector,
|
|
155
|
+
count: elements.length,
|
|
156
|
+
text: Array.from(elements).slice(0, 3).map(el => el.textContent?.trim()).filter(Boolean)
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const signupTextCandidates = ['Sign up', 'Register', 'Get started'];
|
|
161
|
+
for (const text of signupTextCandidates) {
|
|
162
|
+
const elements = Array.from(document.querySelectorAll('button, a')).filter(el => (el.textContent || '').toLowerCase().includes(text.toLowerCase()));
|
|
163
|
+
if (elements.length > 0) {
|
|
164
|
+
signals.push({
|
|
165
|
+
type: 'signup_indicator',
|
|
166
|
+
selector: `text:${text}`,
|
|
167
|
+
count: elements.length,
|
|
168
|
+
text: elements.slice(0, 3).map(el => el.textContent?.trim()).filter(Boolean)
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// E-commerce indicators (CSS + text matches)
|
|
174
|
+
const commerceSelectors = [
|
|
175
|
+
'a[href*="cart"]',
|
|
176
|
+
'a[href*="checkout"]',
|
|
177
|
+
'a[href*="shop"]',
|
|
178
|
+
'[data-testid*="cart"]',
|
|
179
|
+
'[data-testid*="checkout"]'
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
for (const selector of commerceSelectors) {
|
|
183
|
+
const elements = document.querySelectorAll(selector);
|
|
184
|
+
if (elements.length > 0) {
|
|
185
|
+
signals.push({
|
|
186
|
+
type: 'commerce_indicator',
|
|
187
|
+
selector,
|
|
188
|
+
count: elements.length,
|
|
189
|
+
text: Array.from(elements).slice(0, 3).map(el => el.textContent?.trim()).filter(Boolean)
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const commerceTextCandidates = ['Add to cart', 'Buy', 'Purchase'];
|
|
194
|
+
for (const text of commerceTextCandidates) {
|
|
195
|
+
const elements = Array.from(document.querySelectorAll('button, a')).filter(el => (el.textContent || '').toLowerCase().includes(text.toLowerCase()));
|
|
196
|
+
if (elements.length > 0) {
|
|
197
|
+
signals.push({
|
|
198
|
+
type: 'commerce_indicator',
|
|
199
|
+
selector: `text:${text}`,
|
|
200
|
+
count: elements.length,
|
|
201
|
+
text: elements.slice(0, 3).map(el => el.textContent?.trim()).filter(Boolean)
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Pricing indicators
|
|
207
|
+
const pricingSelectors = [
|
|
208
|
+
'a[href*="pricing"]',
|
|
209
|
+
'a[href*="plans"]',
|
|
210
|
+
'[class*="price"]',
|
|
211
|
+
'[class*="pricing"]'
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
for (const selector of pricingSelectors) {
|
|
215
|
+
const elements = document.querySelectorAll(selector);
|
|
216
|
+
if (elements.length > 0) {
|
|
217
|
+
signals.push({
|
|
218
|
+
type: 'pricing_indicator',
|
|
219
|
+
selector,
|
|
220
|
+
count: elements.length
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Dashboard/App indicators
|
|
226
|
+
const dashboardSelectors = [
|
|
227
|
+
'a[href*="/app"]',
|
|
228
|
+
'a[href*="/dashboard"]',
|
|
229
|
+
'a[href*="/console"]'
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
for (const selector of dashboardSelectors) {
|
|
233
|
+
const elements = document.querySelectorAll(selector);
|
|
234
|
+
if (elements.length > 0) {
|
|
235
|
+
signals.push({
|
|
236
|
+
type: 'dashboard_indicator',
|
|
237
|
+
selector,
|
|
238
|
+
count: elements.length
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Documentation indicators
|
|
244
|
+
const docsSelectors = [
|
|
245
|
+
'a[href*="/docs"]',
|
|
246
|
+
'a[href*="/documentation"]',
|
|
247
|
+
'a[href*="/guide"]'
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
for (const selector of docsSelectors) {
|
|
251
|
+
const elements = document.querySelectorAll(selector);
|
|
252
|
+
if (elements.length > 0) {
|
|
253
|
+
signals.push({
|
|
254
|
+
type: 'docs_indicator',
|
|
255
|
+
selector,
|
|
256
|
+
count: elements.length
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Form indicators
|
|
262
|
+
const forms = document.querySelectorAll('form');
|
|
263
|
+
if (forms.length > 0) {
|
|
264
|
+
signals.push({
|
|
265
|
+
type: 'form_indicator',
|
|
266
|
+
count: forms.length
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Contact form indicators
|
|
271
|
+
const contactSelectors = [
|
|
272
|
+
'a[href*="contact"]',
|
|
273
|
+
'form[action*="contact"]',
|
|
274
|
+
'input[name*="email"]',
|
|
275
|
+
'[data-guardian*="contact"]'
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
for (const selector of contactSelectors) {
|
|
279
|
+
const elements = document.querySelectorAll(selector);
|
|
280
|
+
if (elements.length > 0) {
|
|
281
|
+
signals.push({
|
|
282
|
+
type: 'contact_indicator',
|
|
283
|
+
selector,
|
|
284
|
+
count: elements.length
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Primary CTA detection
|
|
290
|
+
const ctaSelectors = [
|
|
291
|
+
'button',
|
|
292
|
+
'a[class*="button"]',
|
|
293
|
+
'a[class*="cta"]',
|
|
294
|
+
'[role="button"]'
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
let ctaCount = 0;
|
|
298
|
+
for (const selector of ctaSelectors) {
|
|
299
|
+
ctaCount += document.querySelectorAll(selector).length;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (ctaCount > 0) {
|
|
303
|
+
signals.push({
|
|
304
|
+
type: 'cta_indicator',
|
|
305
|
+
count: ctaCount
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return signals;
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
signals.push(...domSignals);
|
|
313
|
+
|
|
314
|
+
return signals;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
console.warn(`⚠️ Signal collection failed: ${error.message}`);
|
|
317
|
+
return signals;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Analyze URL patterns for classification hints
|
|
323
|
+
* @param {string} url - Current URL
|
|
324
|
+
* @param {string} baseUrl - Base URL
|
|
325
|
+
* @returns {Array} URL-based signals
|
|
326
|
+
*/
|
|
327
|
+
function analyzeUrlPatterns(url, baseUrl) {
|
|
328
|
+
const signals = [];
|
|
329
|
+
const urlLower = url.toLowerCase();
|
|
330
|
+
|
|
331
|
+
if (urlLower.includes('/app') || urlLower.includes('/dashboard')) {
|
|
332
|
+
signals.push({ type: 'url_pattern', pattern: 'app/dashboard', indicator: 'saas' });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (urlLower.includes('/shop') || urlLower.includes('/store') || urlLower.includes('/products')) {
|
|
336
|
+
signals.push({ type: 'url_pattern', pattern: 'shop/store', indicator: 'ecommerce' });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (urlLower.includes('/docs') || urlLower.includes('/documentation')) {
|
|
340
|
+
signals.push({ type: 'url_pattern', pattern: 'docs', indicator: 'documentation' });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (urlLower.includes('/blog') || urlLower.includes('/articles')) {
|
|
344
|
+
signals.push({ type: 'url_pattern', pattern: 'blog', indicator: 'blog' });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return signals;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Classify site type based on collected signals
|
|
352
|
+
* @param {Array} signals - Collected signals
|
|
353
|
+
* @returns {Object} Classification result with type and confidence
|
|
354
|
+
*/
|
|
355
|
+
function classifySiteType(signals) {
|
|
356
|
+
const scores = {
|
|
357
|
+
[SITE_TYPES.MARKETING]: 0,
|
|
358
|
+
[SITE_TYPES.SAAS]: 0,
|
|
359
|
+
[SITE_TYPES.ECOMMERCE]: 0,
|
|
360
|
+
[SITE_TYPES.DOCUMENTATION]: 0,
|
|
361
|
+
[SITE_TYPES.BLOG]: 0
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Score based on signals
|
|
365
|
+
for (const signal of signals) {
|
|
366
|
+
switch (signal.type) {
|
|
367
|
+
case 'login_indicator':
|
|
368
|
+
scores[SITE_TYPES.SAAS] += 3;
|
|
369
|
+
break;
|
|
370
|
+
case 'signup_indicator':
|
|
371
|
+
scores[SITE_TYPES.SAAS] += 2;
|
|
372
|
+
scores[SITE_TYPES.MARKETING] += 1;
|
|
373
|
+
break;
|
|
374
|
+
case 'commerce_indicator':
|
|
375
|
+
scores[SITE_TYPES.ECOMMERCE] += 5;
|
|
376
|
+
break;
|
|
377
|
+
case 'dashboard_indicator':
|
|
378
|
+
scores[SITE_TYPES.SAAS] += 4;
|
|
379
|
+
break;
|
|
380
|
+
case 'docs_indicator':
|
|
381
|
+
scores[SITE_TYPES.DOCUMENTATION] += 5;
|
|
382
|
+
break;
|
|
383
|
+
case 'pricing_indicator':
|
|
384
|
+
scores[SITE_TYPES.SAAS] += 2;
|
|
385
|
+
scores[SITE_TYPES.MARKETING] += 1;
|
|
386
|
+
break;
|
|
387
|
+
case 'contact_indicator':
|
|
388
|
+
scores[SITE_TYPES.MARKETING] += 2;
|
|
389
|
+
break;
|
|
390
|
+
case 'cta_indicator':
|
|
391
|
+
scores[SITE_TYPES.MARKETING] += 1;
|
|
392
|
+
break;
|
|
393
|
+
case 'url_pattern':
|
|
394
|
+
if (signal.indicator === 'saas') scores[SITE_TYPES.SAAS] += 3;
|
|
395
|
+
if (signal.indicator === 'ecommerce') scores[SITE_TYPES.ECOMMERCE] += 3;
|
|
396
|
+
if (signal.indicator === 'documentation') scores[SITE_TYPES.DOCUMENTATION] += 3;
|
|
397
|
+
if (signal.indicator === 'blog') scores[SITE_TYPES.BLOG] += 3;
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Find highest score
|
|
403
|
+
let maxScore = 0;
|
|
404
|
+
let siteType = SITE_TYPES.UNKNOWN;
|
|
405
|
+
|
|
406
|
+
for (const [type, score] of Object.entries(scores)) {
|
|
407
|
+
if (score > maxScore) {
|
|
408
|
+
maxScore = score;
|
|
409
|
+
siteType = type;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Calculate confidence (0-1)
|
|
414
|
+
const totalScore = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
415
|
+
const confidence = totalScore > 0 ? Math.min(maxScore / totalScore, 1) : 0;
|
|
416
|
+
|
|
417
|
+
// If no strong signals, default to marketing with low confidence
|
|
418
|
+
if (maxScore === 0) {
|
|
419
|
+
return {
|
|
420
|
+
siteType: SITE_TYPES.MARKETING,
|
|
421
|
+
confidence: 0.3
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
siteType,
|
|
427
|
+
confidence: Math.round(confidence * 100) / 100
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Detect what capabilities the site actually supports
|
|
433
|
+
* @param {Array} signals - Collected signals
|
|
434
|
+
* @returns {Object} Capability map with evidence
|
|
435
|
+
*/
|
|
436
|
+
function detectCapabilities(signals) {
|
|
437
|
+
const capabilities = {};
|
|
438
|
+
|
|
439
|
+
// Login detection
|
|
440
|
+
const hasLoginSignals = signals.some(s => s.type === 'login_indicator');
|
|
441
|
+
capabilities[CAPABILITIES.SUPPORTS_LOGIN] = {
|
|
442
|
+
supported: hasLoginSignals,
|
|
443
|
+
confidence: hasLoginSignals ? 0.9 : 0.1,
|
|
444
|
+
evidence: signals.filter(s => s.type === 'login_indicator').slice(0, 2)
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// Signup detection
|
|
448
|
+
const hasSignupSignals = signals.some(s => s.type === 'signup_indicator');
|
|
449
|
+
capabilities[CAPABILITIES.SUPPORTS_SIGNUP] = {
|
|
450
|
+
supported: hasSignupSignals,
|
|
451
|
+
confidence: hasSignupSignals ? 0.9 : 0.1,
|
|
452
|
+
evidence: signals.filter(s => s.type === 'signup_indicator').slice(0, 2)
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// Checkout detection
|
|
456
|
+
const hasCommerceSignals = signals.some(s => s.type === 'commerce_indicator');
|
|
457
|
+
capabilities[CAPABILITIES.SUPPORTS_CHECKOUT] = {
|
|
458
|
+
supported: hasCommerceSignals,
|
|
459
|
+
confidence: hasCommerceSignals ? 0.9 : 0.1,
|
|
460
|
+
evidence: signals.filter(s => s.type === 'commerce_indicator').slice(0, 2)
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// Forms detection
|
|
464
|
+
const hasFormSignals = signals.some(s => s.type === 'form_indicator');
|
|
465
|
+
capabilities[CAPABILITIES.SUPPORTS_FORMS] = {
|
|
466
|
+
supported: hasFormSignals,
|
|
467
|
+
confidence: hasFormSignals ? 0.8 : 0.2,
|
|
468
|
+
evidence: signals.filter(s => s.type === 'form_indicator').slice(0, 1)
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// Primary CTA detection
|
|
472
|
+
const hasCtaSignals = signals.some(s => s.type === 'cta_indicator');
|
|
473
|
+
capabilities[CAPABILITIES.SUPPORTS_PRIMARY_CTA] = {
|
|
474
|
+
supported: hasCtaSignals,
|
|
475
|
+
confidence: hasCtaSignals ? 0.8 : 0.2,
|
|
476
|
+
evidence: signals.filter(s => s.type === 'cta_indicator').slice(0, 1)
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// Pricing detection
|
|
480
|
+
const hasPricingSignals = signals.some(s => s.type === 'pricing_indicator');
|
|
481
|
+
capabilities[CAPABILITIES.SUPPORTS_PRICING] = {
|
|
482
|
+
supported: hasPricingSignals,
|
|
483
|
+
confidence: hasPricingSignals ? 0.8 : 0.2,
|
|
484
|
+
evidence: signals.filter(s => s.type === 'pricing_indicator').slice(0, 2)
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// Contact detection
|
|
488
|
+
const hasContactSignals = signals.some(s => s.type === 'contact_indicator');
|
|
489
|
+
capabilities[CAPABILITIES.SUPPORTS_CONTACT] = {
|
|
490
|
+
supported: hasContactSignals,
|
|
491
|
+
confidence: hasContactSignals ? 0.8 : 0.2,
|
|
492
|
+
evidence: signals.filter(s => s.type === 'contact_indicator').slice(0, 2)
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
return capabilities;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Determine which flows are applicable based on capabilities
|
|
500
|
+
* @param {Object} capabilities - Detected capabilities
|
|
501
|
+
* @param {string} siteType - Classified site type
|
|
502
|
+
* @returns {Object} Flow applicability map
|
|
503
|
+
*/
|
|
504
|
+
function determineFlowApplicability(capabilities, siteType) {
|
|
505
|
+
const applicability = {};
|
|
506
|
+
|
|
507
|
+
// Signup flow
|
|
508
|
+
const supportsSignup = capabilities[CAPABILITIES.SUPPORTS_SIGNUP]?.supported;
|
|
509
|
+
applicability.signup_flow = {
|
|
510
|
+
applicable: supportsSignup === true,
|
|
511
|
+
reason: supportsSignup ? 'Signup elements detected' : 'No signup elements found',
|
|
512
|
+
confidence: capabilities[CAPABILITIES.SUPPORTS_SIGNUP]?.confidence || 0
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// Login flow
|
|
516
|
+
const supportsLogin = capabilities[CAPABILITIES.SUPPORTS_LOGIN]?.supported;
|
|
517
|
+
applicability.login_flow = {
|
|
518
|
+
applicable: supportsLogin === true,
|
|
519
|
+
reason: supportsLogin ? 'Login elements detected' : 'No login elements found',
|
|
520
|
+
confidence: capabilities[CAPABILITIES.SUPPORTS_LOGIN]?.confidence || 0
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// Checkout flow
|
|
524
|
+
const supportsCheckout = capabilities[CAPABILITIES.SUPPORTS_CHECKOUT]?.supported;
|
|
525
|
+
applicability.checkout_flow = {
|
|
526
|
+
applicable: supportsCheckout === true,
|
|
527
|
+
reason: supportsCheckout ? 'Checkout/commerce elements detected' : 'No checkout elements found',
|
|
528
|
+
confidence: capabilities[CAPABILITIES.SUPPORTS_CHECKOUT]?.confidence || 0
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
return applicability;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Get conservative capabilities when analysis fails
|
|
536
|
+
* @returns {Object} Conservative capability map
|
|
537
|
+
*/
|
|
538
|
+
function getConservativeCapabilities() {
|
|
539
|
+
return {
|
|
540
|
+
[CAPABILITIES.SUPPORTS_LOGIN]: { supported: false, confidence: 0, evidence: [] },
|
|
541
|
+
[CAPABILITIES.SUPPORTS_SIGNUP]: { supported: false, confidence: 0, evidence: [] },
|
|
542
|
+
[CAPABILITIES.SUPPORTS_CHECKOUT]: { supported: false, confidence: 0, evidence: [] },
|
|
543
|
+
[CAPABILITIES.SUPPORTS_FORMS]: { supported: false, confidence: 0, evidence: [] },
|
|
544
|
+
[CAPABILITIES.SUPPORTS_PRIMARY_CTA]: { supported: true, confidence: 0.5, evidence: [] },
|
|
545
|
+
[CAPABILITIES.SUPPORTS_PRICING]: { supported: false, confidence: 0, evidence: [] },
|
|
546
|
+
[CAPABILITIES.SUPPORTS_CONTACT]: { supported: false, confidence: 0, evidence: [] }
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Get conservative flow applicability when analysis fails
|
|
552
|
+
* @returns {Object} Conservative applicability map
|
|
553
|
+
*/
|
|
554
|
+
function getConservativeFlowApplicability() {
|
|
555
|
+
return {
|
|
556
|
+
signup_flow: { applicable: false, reason: 'Analysis failed; conservative default', confidence: 0 },
|
|
557
|
+
login_flow: { applicable: false, reason: 'Analysis failed; conservative default', confidence: 0 },
|
|
558
|
+
checkout_flow: { applicable: false, reason: 'Analysis failed; conservative default', confidence: 0 }
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Check if a specific flow is applicable based on intelligence
|
|
564
|
+
* @param {Object} intelligence - Site intelligence data
|
|
565
|
+
* @param {string} flowId - Flow identifier
|
|
566
|
+
* @returns {boolean} True if flow is applicable
|
|
567
|
+
*/
|
|
568
|
+
function isFlowApplicable(intelligence, flowId) {
|
|
569
|
+
if (!intelligence || !intelligence.flowApplicability) {
|
|
570
|
+
// Conservative: if no intelligence, don't run the flow
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const applicability = intelligence.flowApplicability[flowId];
|
|
575
|
+
if (!applicability) {
|
|
576
|
+
// Unknown flow, conservative approach
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return applicability.applicable === true;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
module.exports = {
|
|
584
|
+
analyzeSite,
|
|
585
|
+
isFlowApplicable,
|
|
586
|
+
SITE_TYPES,
|
|
587
|
+
CAPABILITIES
|
|
588
|
+
};
|