@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.
Files changed (172) hide show
  1. package/CHANGELOG.md +210 -210
  2. package/LICENSE +21 -21
  3. package/README.md +297 -184
  4. package/bin/guardian.js +2242 -2221
  5. package/config/README.md +59 -59
  6. package/config/guardian.config.json +54 -54
  7. package/config/guardian.policy.json +12 -12
  8. package/config/profiles/docs.yaml +18 -18
  9. package/config/profiles/ecommerce.yaml +17 -17
  10. package/config/profiles/landing-demo.yaml +16 -16
  11. package/config/profiles/marketing.yaml +18 -18
  12. package/config/profiles/saas.yaml +21 -21
  13. package/flows/example-login-flow.json +36 -36
  14. package/flows/example-signup-flow.json +44 -44
  15. package/package.json +124 -116
  16. package/policies/enterprise.json +12 -12
  17. package/policies/landing-demo.json +22 -22
  18. package/policies/saas.json +12 -12
  19. package/policies/startup.json +12 -12
  20. package/src/enterprise/audit-logger.js +166 -166
  21. package/src/enterprise/pdf-exporter.js +267 -267
  22. package/src/enterprise/rbac-gate.js +142 -142
  23. package/src/enterprise/rbac.js +239 -239
  24. package/src/enterprise/site-manager.js +180 -180
  25. package/src/founder/feedback-system.js +156 -156
  26. package/src/founder/founder-tracker.js +213 -213
  27. package/src/founder/usage-signals.js +141 -141
  28. package/src/guardian/action-hints.js +439 -439
  29. package/src/guardian/alert-ledger.js +121 -121
  30. package/src/guardian/artifact-sanitizer.js +56 -56
  31. package/src/guardian/attempt-engine.js +1069 -1029
  32. package/src/guardian/attempt-registry.js +267 -267
  33. package/src/guardian/attempt-relevance.js +106 -106
  34. package/src/guardian/attempt-reporter.js +513 -507
  35. package/src/guardian/attempt.js +274 -273
  36. package/src/guardian/attempts-filter.js +63 -63
  37. package/src/guardian/auto-attempt-builder.js +283 -283
  38. package/src/guardian/baseline-registry.js +177 -177
  39. package/src/guardian/baseline-reporter.js +143 -143
  40. package/src/guardian/baseline-storage.js +285 -285
  41. package/src/guardian/baseline.js +535 -534
  42. package/src/guardian/behavioral-signals.js +261 -261
  43. package/src/guardian/breakage-intelligence.js +224 -224
  44. package/src/guardian/browser-pool.js +131 -131
  45. package/src/guardian/browser.js +119 -119
  46. package/src/guardian/canonical-truth.js +308 -308
  47. package/src/guardian/ci-cli.js +121 -121
  48. package/src/guardian/ci-gate.js +96 -96
  49. package/src/guardian/ci-mode.js +15 -15
  50. package/src/guardian/ci-output.js +55 -38
  51. package/src/guardian/cli-summary.js +102 -102
  52. package/src/guardian/confidence-signals.js +251 -251
  53. package/src/guardian/config-loader.js +161 -161
  54. package/src/guardian/config-validator.js +285 -283
  55. package/src/guardian/coverage-model.js +239 -239
  56. package/src/guardian/coverage-packs.js +58 -58
  57. package/src/guardian/crawler.js +142 -142
  58. package/src/guardian/data-guardian-detector.js +189 -189
  59. package/src/guardian/decision-authority.js +746 -725
  60. package/src/guardian/detection-layers.js +271 -271
  61. package/src/guardian/determinism.js +146 -146
  62. package/src/guardian/discovery-engine.js +661 -661
  63. package/src/guardian/drift-detector.js +100 -100
  64. package/src/guardian/enhanced-html-reporter.js +522 -522
  65. package/src/guardian/env-guard.js +128 -127
  66. package/src/guardian/error-clarity.js +399 -399
  67. package/src/guardian/export-contract.js +196 -196
  68. package/src/guardian/fail-safe.js +212 -212
  69. package/src/guardian/failure-intelligence.js +173 -173
  70. package/src/guardian/failure-taxonomy.js +169 -169
  71. package/src/guardian/final-outcome.js +206 -206
  72. package/src/guardian/first-run-profile.js +89 -89
  73. package/src/guardian/first-run.js +65 -67
  74. package/src/guardian/flag-validator.js +111 -111
  75. package/src/guardian/flow-executor.js +641 -639
  76. package/src/guardian/flow-registry.js +67 -67
  77. package/src/guardian/honesty.js +394 -394
  78. package/src/guardian/html-reporter.js +416 -416
  79. package/src/guardian/human-intent-resolver.js +296 -296
  80. package/src/guardian/human-interaction-model.js +351 -351
  81. package/src/guardian/human-journey-context.js +184 -184
  82. package/src/guardian/human-navigator.js +544 -544
  83. package/src/guardian/human-reporter.js +435 -431
  84. package/src/guardian/index.js +226 -221
  85. package/src/guardian/init-command.js +143 -143
  86. package/src/guardian/intent-detector.js +148 -146
  87. package/src/guardian/journey-definitions.js +132 -132
  88. package/src/guardian/journey-scan-cli.js +142 -145
  89. package/src/guardian/journey-scanner.js +583 -583
  90. package/src/guardian/junit-reporter.js +281 -281
  91. package/src/guardian/language-detection.js +99 -99
  92. package/src/guardian/live-alert.js +56 -56
  93. package/src/guardian/live-baseline-compare.js +146 -146
  94. package/src/guardian/live-cli.js +95 -95
  95. package/src/guardian/live-guardian.js +210 -210
  96. package/src/guardian/live-scheduler-runner.js +137 -137
  97. package/src/guardian/live-scheduler-state.js +167 -168
  98. package/src/guardian/live-scheduler.js +146 -146
  99. package/src/guardian/live-state.js +110 -110
  100. package/src/guardian/market-criticality.js +335 -335
  101. package/src/guardian/market-reporter.js +577 -577
  102. package/src/guardian/network-trace.js +178 -178
  103. package/src/guardian/obs-logger.js +110 -110
  104. package/src/guardian/observed-capabilities.js +427 -427
  105. package/src/guardian/output-contract.js +154 -0
  106. package/src/guardian/output-readability.js +264 -264
  107. package/src/guardian/parallel-executor.js +116 -116
  108. package/src/guardian/path-safety.js +56 -56
  109. package/src/guardian/pattern-analyzer.js +348 -348
  110. package/src/guardian/policy.js +432 -434
  111. package/src/guardian/prelaunch-gate.js +193 -193
  112. package/src/guardian/prerequisite-checker.js +101 -101
  113. package/src/guardian/preset-loader.js +152 -157
  114. package/src/guardian/profile-loader.js +96 -96
  115. package/src/guardian/reality.js +3025 -2826
  116. package/src/guardian/realworld-scenarios.js +94 -94
  117. package/src/guardian/reporter.js +167 -167
  118. package/src/guardian/retry-policy.js +123 -123
  119. package/src/guardian/root-cause-analysis.js +171 -171
  120. package/src/guardian/rules-engine.js +558 -558
  121. package/src/guardian/run-artifacts.js +212 -212
  122. package/src/guardian/run-cleanup.js +207 -207
  123. package/src/guardian/run-export.js +522 -522
  124. package/src/guardian/run-latest.js +90 -90
  125. package/src/guardian/run-list.js +211 -211
  126. package/src/guardian/run-summary.js +20 -20
  127. package/src/guardian/runtime-root.js +246 -246
  128. package/src/guardian/safety.js +248 -248
  129. package/src/guardian/scan-presets.js +133 -149
  130. package/src/guardian/screenshot.js +152 -152
  131. package/src/guardian/secret-hygiene.js +44 -44
  132. package/src/guardian/selector-fallbacks.js +394 -394
  133. package/src/guardian/semantic-contact-detection.js +255 -255
  134. package/src/guardian/semantic-contact-finder.js +201 -201
  135. package/src/guardian/semantic-targets.js +234 -234
  136. package/src/guardian/site-intelligence.js +588 -588
  137. package/src/guardian/site-introspection.js +257 -257
  138. package/src/guardian/sitemap.js +225 -225
  139. package/src/guardian/smoke.js +283 -258
  140. package/src/guardian/snapshot-schema.js +177 -290
  141. package/src/guardian/snapshot.js +430 -397
  142. package/src/guardian/stability-scorer.js +169 -169
  143. package/src/guardian/success-evaluator.js +214 -214
  144. package/src/guardian/template-command.js +184 -184
  145. package/src/guardian/text-formatters.js +426 -426
  146. package/src/guardian/timeout-profiles.js +57 -57
  147. package/src/guardian/truth/attempt.contract.js +158 -0
  148. package/src/guardian/truth/decision.contract.js +275 -0
  149. package/src/guardian/truth/snapshot.contract.js +363 -0
  150. package/src/guardian/validators.js +323 -323
  151. package/src/guardian/verdict-card.js +474 -474
  152. package/src/guardian/verdict-clarity.js +298 -298
  153. package/src/guardian/verdict-policy.js +363 -363
  154. package/src/guardian/verdict.js +333 -333
  155. package/src/guardian/verdicts.js +79 -74
  156. package/src/guardian/visual-diff.js +247 -247
  157. package/src/guardian/wait-for-outcome.js +119 -119
  158. package/src/guardian/watch-runner.js +181 -181
  159. package/src/guardian/watchdog-diff.js +167 -167
  160. package/src/guardian/webhook.js +206 -206
  161. package/src/payments/stripe-checkout.js +169 -169
  162. package/src/plans/plan-definitions.js +148 -148
  163. package/src/plans/plan-manager.js +211 -211
  164. package/src/plans/usage-tracker.js +210 -210
  165. package/src/recipes/recipe-engine.js +188 -188
  166. package/src/recipes/recipe-failure-analysis.js +159 -159
  167. package/src/recipes/recipe-registry.js +134 -134
  168. package/src/recipes/recipe-runtime.js +507 -507
  169. package/src/recipes/recipe-store.js +410 -410
  170. package/SECURITY.md +0 -77
  171. package/VERSIONING.md +0 -100
  172. 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
+ };