@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,427 +1,427 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Observed Capabilities Extractor
|
|
3
|
-
* Phase 11: Extract what's actually observable on the site
|
|
4
|
-
*
|
|
5
|
-
* CORE PRINCIPLE: VISIBLE = MUST WORK, NOT VISIBLE = NOT APPLICABLE
|
|
6
|
-
*
|
|
7
|
-
* This module maps observable UI elements to capabilities.
|
|
8
|
-
* If a capability is not observed, any attempt targeting it is NOT_APPLICABLE.
|
|
9
|
-
* NOT_APPLICABLE results do NOT count as failures or friction.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Capability mappings: what to look for on the page
|
|
14
|
-
*/
|
|
15
|
-
const CAPABILITY_MAPPINGS = {
|
|
16
|
-
// Login/Authentication capabilities
|
|
17
|
-
login: {
|
|
18
|
-
selectors: [
|
|
19
|
-
'a[href*="login"]',
|
|
20
|
-
'a[href*="signin"]',
|
|
21
|
-
'a[href*="auth"]',
|
|
22
|
-
'button:has-text("Login")',
|
|
23
|
-
'button:has-text("Sign in")',
|
|
24
|
-
'button:has-text("Sign In")',
|
|
25
|
-
'a:has-text("Login")',
|
|
26
|
-
'a:has-text("Sign in")',
|
|
27
|
-
'a:has-text("Sign In")',
|
|
28
|
-
'[data-testid*="login"]',
|
|
29
|
-
'[data-guardian*="login"]'
|
|
30
|
-
],
|
|
31
|
-
textPatterns: ['login', 'sign in', 'signin', 'sign-in'],
|
|
32
|
-
attemptIds: ['login', 'login_flow']
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
// Signup/Registration capabilities
|
|
36
|
-
signup: {
|
|
37
|
-
selectors: [
|
|
38
|
-
'a[href*="signup"]',
|
|
39
|
-
'a[href*="register"]',
|
|
40
|
-
'a[href*="join"]',
|
|
41
|
-
'a[href*="onboarding"]',
|
|
42
|
-
'button:has-text("Sign up")',
|
|
43
|
-
'button:has-text("Sign Up")',
|
|
44
|
-
'button:has-text("Register")',
|
|
45
|
-
'button:has-text("Get started")',
|
|
46
|
-
'a:has-text("Sign up")',
|
|
47
|
-
'a:has-text("Sign Up")',
|
|
48
|
-
'a:has-text("Register")',
|
|
49
|
-
'[data-testid*="signup"]',
|
|
50
|
-
'[data-guardian*="signup"]'
|
|
51
|
-
],
|
|
52
|
-
textPatterns: ['signup', 'sign up', 'register', 'get started', 'join'],
|
|
53
|
-
attemptIds: ['signup', 'signup_flow']
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
// Checkout/Payment capabilities
|
|
57
|
-
checkout: {
|
|
58
|
-
selectors: [
|
|
59
|
-
'a[href*="checkout"]',
|
|
60
|
-
'a[href*="cart"]',
|
|
61
|
-
'a[href*="cart"]',
|
|
62
|
-
'button:has-text("Checkout")',
|
|
63
|
-
'button:has-text("Buy")',
|
|
64
|
-
'button:has-text("Purchase")',
|
|
65
|
-
'button:has-text("Add to cart")',
|
|
66
|
-
'a:has-text("Checkout")',
|
|
67
|
-
'[data-testid*="checkout"]',
|
|
68
|
-
'[data-guardian*="checkout"]'
|
|
69
|
-
],
|
|
70
|
-
textPatterns: ['checkout', 'add to cart', 'buy', 'purchase', 'add to cart'],
|
|
71
|
-
attemptIds: ['checkout', 'checkout_flow']
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
// Contact form capability
|
|
75
|
-
contact_form: {
|
|
76
|
-
selectors: [
|
|
77
|
-
'a[href*="contact"]',
|
|
78
|
-
'form[action*="contact"]',
|
|
79
|
-
'input[name*="email"]',
|
|
80
|
-
'textarea[name*="message"]',
|
|
81
|
-
'button:has-text("Contact")',
|
|
82
|
-
'a:has-text("Contact")',
|
|
83
|
-
'[data-guardian*="contact"]',
|
|
84
|
-
'[data-testid*="contact"]'
|
|
85
|
-
],
|
|
86
|
-
textPatterns: ['contact', 'contact us', 'get in touch'],
|
|
87
|
-
attemptIds: ['contact_form', 'contact_discovery_v2']
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
// Newsletter signup capability
|
|
91
|
-
newsletter_signup: {
|
|
92
|
-
selectors: [
|
|
93
|
-
'a[href*="newsletter"]',
|
|
94
|
-
'input[name*="newsletter"]',
|
|
95
|
-
'input[placeholder*="email"]',
|
|
96
|
-
'button:has-text("Subscribe")',
|
|
97
|
-
'button:has-text("Sign up")',
|
|
98
|
-
'a:has-text("Subscribe")',
|
|
99
|
-
'[data-guardian*="newsletter"]',
|
|
100
|
-
'[data-testid*="newsletter"]'
|
|
101
|
-
],
|
|
102
|
-
textPatterns: ['newsletter', 'subscribe', 'email'],
|
|
103
|
-
attemptIds: ['newsletter_signup']
|
|
104
|
-
},
|
|
105
|
-
|
|
106
|
-
// Language switch capability
|
|
107
|
-
language_switch: {
|
|
108
|
-
selectors: [
|
|
109
|
-
'button:has-text("Language")',
|
|
110
|
-
'button:has-text("Lang")',
|
|
111
|
-
'[data-guardian*="lang"]',
|
|
112
|
-
'[data-testid*="lang"]',
|
|
113
|
-
'a[href*="lang"]',
|
|
114
|
-
'a[hreflang]'
|
|
115
|
-
],
|
|
116
|
-
textPatterns: ['language', 'lang', 'deutsch', 'english', 'français'],
|
|
117
|
-
attemptIds: ['language_switch']
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
// Primary CTAs (always present on marketing sites)
|
|
121
|
-
primary_ctas: {
|
|
122
|
-
selectors: [
|
|
123
|
-
'button',
|
|
124
|
-
'a[class*="button"]',
|
|
125
|
-
'a[class*="cta"]',
|
|
126
|
-
'[role="button"]'
|
|
127
|
-
],
|
|
128
|
-
minCount: 1,
|
|
129
|
-
attemptIds: ['primary_ctas']
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Extract observed capabilities from site intelligence data
|
|
135
|
-
*
|
|
136
|
-
* @param {Object} siteIntelligence - Result from analyzeSite()
|
|
137
|
-
* @returns {Object} Observed capabilities map
|
|
138
|
-
*/
|
|
139
|
-
function extractObservedCapabilities(siteIntelligence) {
|
|
140
|
-
const observed = {
|
|
141
|
-
timestamp: new Date().toISOString(),
|
|
142
|
-
capabilities: {},
|
|
143
|
-
mapping: {} // Maps capability name to evidence
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
if (!siteIntelligence || !siteIntelligence.capabilities) {
|
|
147
|
-
// Conservative: if no intelligence, assume ALL capabilities present
|
|
148
|
-
// This prevents false NOT_APPLICABLE when analysis fails
|
|
149
|
-
return {
|
|
150
|
-
...observed,
|
|
151
|
-
capabilities: {
|
|
152
|
-
login: true,
|
|
153
|
-
signup: true,
|
|
154
|
-
checkout: true,
|
|
155
|
-
contact_form: true,
|
|
156
|
-
newsletter_signup: true,
|
|
157
|
-
language_switch: true,
|
|
158
|
-
primary_ctas: true
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Map site-intelligence capabilities to observed capabilities
|
|
164
|
-
const caps = siteIntelligence.capabilities || {};
|
|
165
|
-
|
|
166
|
-
observed.capabilities.login = (
|
|
167
|
-
caps.supports_login?.supported === true ||
|
|
168
|
-
caps[Object.keys(CAPABILITY_MAPPINGS).find(k => k === 'login')]?.supported === true
|
|
169
|
-
);
|
|
170
|
-
observed.mapping.login = caps.supports_login || {};
|
|
171
|
-
|
|
172
|
-
observed.capabilities.signup = (
|
|
173
|
-
caps.supports_signup?.supported === true ||
|
|
174
|
-
caps[Object.keys(CAPABILITY_MAPPINGS).find(k => k === 'signup')]?.supported === true
|
|
175
|
-
);
|
|
176
|
-
observed.mapping.signup = caps.supports_signup || {};
|
|
177
|
-
|
|
178
|
-
observed.capabilities.checkout = (
|
|
179
|
-
caps.supports_checkout?.supported === true ||
|
|
180
|
-
caps[Object.keys(CAPABILITY_MAPPINGS).find(k => k === 'checkout')]?.supported === true
|
|
181
|
-
);
|
|
182
|
-
observed.mapping.checkout = caps.supports_checkout || {};
|
|
183
|
-
|
|
184
|
-
observed.capabilities.contact_form = (
|
|
185
|
-
caps.supports_contact?.supported === true ||
|
|
186
|
-
caps[Object.keys(CAPABILITY_MAPPINGS).find(k => k === 'contact_form')]?.supported === true
|
|
187
|
-
);
|
|
188
|
-
observed.mapping.contact_form = caps.supports_contact || {};
|
|
189
|
-
|
|
190
|
-
observed.capabilities.newsletter_signup = (
|
|
191
|
-
caps.supports_newsletter?.supported === true ||
|
|
192
|
-
caps[Object.keys(CAPABILITY_MAPPINGS).find(k => k === 'newsletter_signup')]?.supported === true
|
|
193
|
-
);
|
|
194
|
-
observed.mapping.newsletter_signup = caps.supports_newsletter || {};
|
|
195
|
-
|
|
196
|
-
observed.capabilities.language_switch = (
|
|
197
|
-
caps.supports_language_switch?.supported === true ||
|
|
198
|
-
caps[Object.keys(CAPABILITY_MAPPINGS).find(k => k === 'language_switch')]?.supported === true
|
|
199
|
-
);
|
|
200
|
-
observed.mapping.language_switch = caps.supports_language_switch || {};
|
|
201
|
-
|
|
202
|
-
// Primary CTAs: assume true unless we have explicit evidence of none
|
|
203
|
-
observed.capabilities.primary_ctas = (
|
|
204
|
-
caps.supports_primary_cta?.supported !== false
|
|
205
|
-
);
|
|
206
|
-
observed.mapping.primary_ctas = caps.supports_primary_cta || { supported: true };
|
|
207
|
-
|
|
208
|
-
// PHASE 11: Internal/admin surfaces detection
|
|
209
|
-
// Requires BOTH URL pattern AND UI label evidence to avoid false positives
|
|
210
|
-
observed.capabilities.internal_admin = detectInternalSurfaces(siteIntelligence);
|
|
211
|
-
observed.mapping.internal_admin = { detected: observed.capabilities.internal_admin };
|
|
212
|
-
|
|
213
|
-
return observed;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Detect internal/admin surfaces with strong evidence requirements
|
|
218
|
-
* PHASE 11: Internal surfaces must NOT penalize public readiness
|
|
219
|
-
*
|
|
220
|
-
* Detection requires BOTH:
|
|
221
|
-
* - URL pattern (/admin, /wp-admin, /internal, /dashboard, /staff)
|
|
222
|
-
* - UI label evidence ("Admin", "Staff", "Internal", "Management")
|
|
223
|
-
*
|
|
224
|
-
* @param {Object} siteIntelligence - Site intelligence data
|
|
225
|
-
* @returns {boolean} - true if internal admin surface detected
|
|
226
|
-
*/
|
|
227
|
-
function detectInternalSurfaces(siteIntelligence) {
|
|
228
|
-
if (!siteIntelligence || !siteIntelligence.crawl || !siteIntelligence.crawl.discovered) {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const discovered = siteIntelligence.crawl.discovered;
|
|
233
|
-
const internalUrlPatterns = ['/admin', '/wp-admin', '/internal', '/dashboard', '/staff', '/management'];
|
|
234
|
-
const internalLabelPatterns = ['admin', 'staff', 'internal', 'management', 'dashboard'];
|
|
235
|
-
|
|
236
|
-
// Check each discovered URL (can be string or object with url + metadata)
|
|
237
|
-
for (const urlEntry of discovered) {
|
|
238
|
-
// Handle both string URLs and object format
|
|
239
|
-
let url = '';
|
|
240
|
-
let text = '';
|
|
241
|
-
|
|
242
|
-
if (typeof urlEntry === 'string') {
|
|
243
|
-
url = urlEntry;
|
|
244
|
-
} else if (urlEntry && typeof urlEntry === 'object') {
|
|
245
|
-
url = urlEntry.url || urlEntry.href || '';
|
|
246
|
-
text = urlEntry.text || urlEntry.label || urlEntry.linkText || '';
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Check if URL matches internal pattern
|
|
250
|
-
const hasInternalUrl = internalUrlPatterns.some(pattern => url.toLowerCase().includes(pattern));
|
|
251
|
-
|
|
252
|
-
// Check if text/label matches internal pattern (case-insensitive)
|
|
253
|
-
const hasInternalLabel = text && internalLabelPatterns.some(pattern =>
|
|
254
|
-
text.toLowerCase().includes(pattern)
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
// Require BOTH URL and label evidence to minimize false positives
|
|
258
|
-
if (hasInternalUrl && hasInternalLabel) {
|
|
259
|
-
return true;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return false;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Check if a capability is observed
|
|
268
|
-
*
|
|
269
|
-
* @param {Object} observedCapabilities - Result from extractObservedCapabilities()
|
|
270
|
-
* @param {string} capabilityName - Name of capability (e.g., 'login', 'signup')
|
|
271
|
-
* @returns {boolean} True if capability is observed
|
|
272
|
-
*/
|
|
273
|
-
function isCapabilityObserved(observedCapabilities, capabilityName) {
|
|
274
|
-
if (!observedCapabilities || !observedCapabilities.capabilities) {
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
return observedCapabilities.capabilities[capabilityName] === true;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Get applicable attempts based on observed capabilities
|
|
282
|
-
* Maps attempt IDs to the capabilities they require
|
|
283
|
-
*
|
|
284
|
-
* @param {Array} attemptIds - List of attempt IDs to filter
|
|
285
|
-
* @param {Object} observedCapabilities - Result from extractObservedCapabilities()
|
|
286
|
-
* @returns {Object} { applicable: Array, notApplicable: Array }
|
|
287
|
-
*/
|
|
288
|
-
function filterAttemptsByObservedCapabilities(attemptIds, observedCapabilities) {
|
|
289
|
-
const applicable = [];
|
|
290
|
-
const notApplicable = [];
|
|
291
|
-
|
|
292
|
-
if (!attemptIds || !Array.isArray(attemptIds)) {
|
|
293
|
-
return { applicable: [], notApplicable: [] };
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
for (const attemptId of attemptIds) {
|
|
297
|
-
// Find which capability this attempt requires
|
|
298
|
-
let requiredCapability = null;
|
|
299
|
-
for (const [capName, mapping] of Object.entries(CAPABILITY_MAPPINGS)) {
|
|
300
|
-
if (mapping.attemptIds.includes(attemptId)) {
|
|
301
|
-
requiredCapability = capName;
|
|
302
|
-
break;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// If no capability mapping, always run (universal attempt)
|
|
307
|
-
if (!requiredCapability) {
|
|
308
|
-
applicable.push(attemptId);
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Check if capability is observed
|
|
313
|
-
if (isCapabilityObserved(observedCapabilities, requiredCapability)) {
|
|
314
|
-
applicable.push(attemptId);
|
|
315
|
-
} else {
|
|
316
|
-
notApplicable.push({
|
|
317
|
-
attemptId,
|
|
318
|
-
reason: `Capability not observed: ${requiredCapability}`,
|
|
319
|
-
capabilityRequired: requiredCapability
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return { applicable, notApplicable };
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Get applicable flows based on observed capabilities
|
|
329
|
-
*
|
|
330
|
-
* @param {Array} flowIds - List of flow IDs to filter
|
|
331
|
-
* @param {Object} observedCapabilities - Result from extractObservedCapabilities()
|
|
332
|
-
* @returns {Object} { applicable: Array, notApplicable: Array }
|
|
333
|
-
*/
|
|
334
|
-
function filterFlowsByObservedCapabilities(flowIds, observedCapabilities) {
|
|
335
|
-
const applicable = [];
|
|
336
|
-
const notApplicable = [];
|
|
337
|
-
|
|
338
|
-
if (!flowIds || !Array.isArray(flowIds)) {
|
|
339
|
-
return { applicable: [], notApplicable: [] };
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const flowCapabilityMap = {
|
|
343
|
-
'login_flow': 'login',
|
|
344
|
-
'signup_flow': 'signup',
|
|
345
|
-
'checkout_flow': 'checkout',
|
|
346
|
-
'contact_flow': 'contact_form'
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
for (const flowId of flowIds) {
|
|
350
|
-
const requiredCapability = flowCapabilityMap[flowId];
|
|
351
|
-
|
|
352
|
-
// If no mapping, always run
|
|
353
|
-
if (!requiredCapability) {
|
|
354
|
-
applicable.push(flowId);
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Check if capability is observed
|
|
359
|
-
if (isCapabilityObserved(observedCapabilities, requiredCapability)) {
|
|
360
|
-
applicable.push(flowId);
|
|
361
|
-
} else {
|
|
362
|
-
notApplicable.push({
|
|
363
|
-
flowId,
|
|
364
|
-
reason: `Capability not observed: ${requiredCapability}`,
|
|
365
|
-
capabilityRequired: requiredCapability
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return { applicable, notApplicable };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Create NOT_APPLICABLE result for an attempt
|
|
375
|
-
*
|
|
376
|
-
* @param {string} attemptId - Attempt ID
|
|
377
|
-
* @param {string} capabilityName - Name of unobserved capability
|
|
378
|
-
* @returns {Object} Result object with NOT_APPLICABLE outcome
|
|
379
|
-
*/
|
|
380
|
-
function createNotApplicableAttemptResult(attemptId, capabilityName) {
|
|
381
|
-
return {
|
|
382
|
-
attemptId,
|
|
383
|
-
attemptName: attemptId,
|
|
384
|
-
goal: 'Unknown',
|
|
385
|
-
riskCategory: 'UNKNOWN',
|
|
386
|
-
source: 'capability-check',
|
|
387
|
-
outcome: 'NOT_APPLICABLE',
|
|
388
|
-
skipReason: `Capability not observed: ${capabilityName}`,
|
|
389
|
-
skipReasonCode: 'NOT_APPLICABLE', // CRITICAL: Must match SKIP_CODES.NOT_APPLICABLE for coverage calculation
|
|
390
|
-
exitCode: 0, // NOT_APPLICABLE doesn't affect exit code
|
|
391
|
-
executed: false,
|
|
392
|
-
steps: [],
|
|
393
|
-
friction: null,
|
|
394
|
-
error: null
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Create NOT_APPLICABLE result for a flow
|
|
400
|
-
*
|
|
401
|
-
* @param {string} flowId - Flow ID
|
|
402
|
-
* @param {string} capabilityName - Name of unobserved capability
|
|
403
|
-
* @returns {Object} Result object with NOT_APPLICABLE outcome
|
|
404
|
-
*/
|
|
405
|
-
function createNotApplicableFlowResult(flowId, capabilityName) {
|
|
406
|
-
return {
|
|
407
|
-
flowId,
|
|
408
|
-
flowName: flowId,
|
|
409
|
-
outcome: 'NOT_APPLICABLE',
|
|
410
|
-
skipReason: `Capability not observed: ${capabilityName}`,
|
|
411
|
-
skipReasonCode: 'NOT_OBSERVABLE',
|
|
412
|
-
success: null,
|
|
413
|
-
executed: false,
|
|
414
|
-
steps: [],
|
|
415
|
-
error: null
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
module.exports = {
|
|
420
|
-
CAPABILITY_MAPPINGS,
|
|
421
|
-
extractObservedCapabilities,
|
|
422
|
-
isCapabilityObserved,
|
|
423
|
-
filterAttemptsByObservedCapabilities,
|
|
424
|
-
filterFlowsByObservedCapabilities,
|
|
425
|
-
createNotApplicableAttemptResult,
|
|
426
|
-
createNotApplicableFlowResult
|
|
427
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Observed Capabilities Extractor
|
|
3
|
+
* Phase 11: Extract what's actually observable on the site
|
|
4
|
+
*
|
|
5
|
+
* CORE PRINCIPLE: VISIBLE = MUST WORK, NOT VISIBLE = NOT APPLICABLE
|
|
6
|
+
*
|
|
7
|
+
* This module maps observable UI elements to capabilities.
|
|
8
|
+
* If a capability is not observed, any attempt targeting it is NOT_APPLICABLE.
|
|
9
|
+
* NOT_APPLICABLE results do NOT count as failures or friction.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Capability mappings: what to look for on the page
|
|
14
|
+
*/
|
|
15
|
+
const CAPABILITY_MAPPINGS = {
|
|
16
|
+
// Login/Authentication capabilities
|
|
17
|
+
login: {
|
|
18
|
+
selectors: [
|
|
19
|
+
'a[href*="login"]',
|
|
20
|
+
'a[href*="signin"]',
|
|
21
|
+
'a[href*="auth"]',
|
|
22
|
+
'button:has-text("Login")',
|
|
23
|
+
'button:has-text("Sign in")',
|
|
24
|
+
'button:has-text("Sign In")',
|
|
25
|
+
'a:has-text("Login")',
|
|
26
|
+
'a:has-text("Sign in")',
|
|
27
|
+
'a:has-text("Sign In")',
|
|
28
|
+
'[data-testid*="login"]',
|
|
29
|
+
'[data-guardian*="login"]'
|
|
30
|
+
],
|
|
31
|
+
textPatterns: ['login', 'sign in', 'signin', 'sign-in'],
|
|
32
|
+
attemptIds: ['login', 'login_flow']
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Signup/Registration capabilities
|
|
36
|
+
signup: {
|
|
37
|
+
selectors: [
|
|
38
|
+
'a[href*="signup"]',
|
|
39
|
+
'a[href*="register"]',
|
|
40
|
+
'a[href*="join"]',
|
|
41
|
+
'a[href*="onboarding"]',
|
|
42
|
+
'button:has-text("Sign up")',
|
|
43
|
+
'button:has-text("Sign Up")',
|
|
44
|
+
'button:has-text("Register")',
|
|
45
|
+
'button:has-text("Get started")',
|
|
46
|
+
'a:has-text("Sign up")',
|
|
47
|
+
'a:has-text("Sign Up")',
|
|
48
|
+
'a:has-text("Register")',
|
|
49
|
+
'[data-testid*="signup"]',
|
|
50
|
+
'[data-guardian*="signup"]'
|
|
51
|
+
],
|
|
52
|
+
textPatterns: ['signup', 'sign up', 'register', 'get started', 'join'],
|
|
53
|
+
attemptIds: ['signup', 'signup_flow']
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// Checkout/Payment capabilities
|
|
57
|
+
checkout: {
|
|
58
|
+
selectors: [
|
|
59
|
+
'a[href*="checkout"]',
|
|
60
|
+
'a[href*="cart"]',
|
|
61
|
+
'a[href*="cart"]',
|
|
62
|
+
'button:has-text("Checkout")',
|
|
63
|
+
'button:has-text("Buy")',
|
|
64
|
+
'button:has-text("Purchase")',
|
|
65
|
+
'button:has-text("Add to cart")',
|
|
66
|
+
'a:has-text("Checkout")',
|
|
67
|
+
'[data-testid*="checkout"]',
|
|
68
|
+
'[data-guardian*="checkout"]'
|
|
69
|
+
],
|
|
70
|
+
textPatterns: ['checkout', 'add to cart', 'buy', 'purchase', 'add to cart'],
|
|
71
|
+
attemptIds: ['checkout', 'checkout_flow']
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Contact form capability
|
|
75
|
+
contact_form: {
|
|
76
|
+
selectors: [
|
|
77
|
+
'a[href*="contact"]',
|
|
78
|
+
'form[action*="contact"]',
|
|
79
|
+
'input[name*="email"]',
|
|
80
|
+
'textarea[name*="message"]',
|
|
81
|
+
'button:has-text("Contact")',
|
|
82
|
+
'a:has-text("Contact")',
|
|
83
|
+
'[data-guardian*="contact"]',
|
|
84
|
+
'[data-testid*="contact"]'
|
|
85
|
+
],
|
|
86
|
+
textPatterns: ['contact', 'contact us', 'get in touch'],
|
|
87
|
+
attemptIds: ['contact_form', 'contact_discovery_v2']
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// Newsletter signup capability
|
|
91
|
+
newsletter_signup: {
|
|
92
|
+
selectors: [
|
|
93
|
+
'a[href*="newsletter"]',
|
|
94
|
+
'input[name*="newsletter"]',
|
|
95
|
+
'input[placeholder*="email"]',
|
|
96
|
+
'button:has-text("Subscribe")',
|
|
97
|
+
'button:has-text("Sign up")',
|
|
98
|
+
'a:has-text("Subscribe")',
|
|
99
|
+
'[data-guardian*="newsletter"]',
|
|
100
|
+
'[data-testid*="newsletter"]'
|
|
101
|
+
],
|
|
102
|
+
textPatterns: ['newsletter', 'subscribe', 'email'],
|
|
103
|
+
attemptIds: ['newsletter_signup']
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// Language switch capability
|
|
107
|
+
language_switch: {
|
|
108
|
+
selectors: [
|
|
109
|
+
'button:has-text("Language")',
|
|
110
|
+
'button:has-text("Lang")',
|
|
111
|
+
'[data-guardian*="lang"]',
|
|
112
|
+
'[data-testid*="lang"]',
|
|
113
|
+
'a[href*="lang"]',
|
|
114
|
+
'a[hreflang]'
|
|
115
|
+
],
|
|
116
|
+
textPatterns: ['language', 'lang', 'deutsch', 'english', 'français'],
|
|
117
|
+
attemptIds: ['language_switch']
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// Primary CTAs (always present on marketing sites)
|
|
121
|
+
primary_ctas: {
|
|
122
|
+
selectors: [
|
|
123
|
+
'button',
|
|
124
|
+
'a[class*="button"]',
|
|
125
|
+
'a[class*="cta"]',
|
|
126
|
+
'[role="button"]'
|
|
127
|
+
],
|
|
128
|
+
minCount: 1,
|
|
129
|
+
attemptIds: ['primary_ctas']
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract observed capabilities from site intelligence data
|
|
135
|
+
*
|
|
136
|
+
* @param {Object} siteIntelligence - Result from analyzeSite()
|
|
137
|
+
* @returns {Object} Observed capabilities map
|
|
138
|
+
*/
|
|
139
|
+
function extractObservedCapabilities(siteIntelligence) {
|
|
140
|
+
const observed = {
|
|
141
|
+
timestamp: new Date().toISOString(),
|
|
142
|
+
capabilities: {},
|
|
143
|
+
mapping: {} // Maps capability name to evidence
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (!siteIntelligence || !siteIntelligence.capabilities) {
|
|
147
|
+
// Conservative: if no intelligence, assume ALL capabilities present
|
|
148
|
+
// This prevents false NOT_APPLICABLE when analysis fails
|
|
149
|
+
return {
|
|
150
|
+
...observed,
|
|
151
|
+
capabilities: {
|
|
152
|
+
login: true,
|
|
153
|
+
signup: true,
|
|
154
|
+
checkout: true,
|
|
155
|
+
contact_form: true,
|
|
156
|
+
newsletter_signup: true,
|
|
157
|
+
language_switch: true,
|
|
158
|
+
primary_ctas: true
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Map site-intelligence capabilities to observed capabilities
|
|
164
|
+
const caps = siteIntelligence.capabilities || {};
|
|
165
|
+
|
|
166
|
+
observed.capabilities.login = (
|
|
167
|
+
caps.supports_login?.supported === true ||
|
|
168
|
+
caps[Object.keys(CAPABILITY_MAPPINGS).find(k => k === 'login')]?.supported === true
|
|
169
|
+
);
|
|
170
|
+
observed.mapping.login = caps.supports_login || {};
|
|
171
|
+
|
|
172
|
+
observed.capabilities.signup = (
|
|
173
|
+
caps.supports_signup?.supported === true ||
|
|
174
|
+
caps[Object.keys(CAPABILITY_MAPPINGS).find(k => k === 'signup')]?.supported === true
|
|
175
|
+
);
|
|
176
|
+
observed.mapping.signup = caps.supports_signup || {};
|
|
177
|
+
|
|
178
|
+
observed.capabilities.checkout = (
|
|
179
|
+
caps.supports_checkout?.supported === true ||
|
|
180
|
+
caps[Object.keys(CAPABILITY_MAPPINGS).find(k => k === 'checkout')]?.supported === true
|
|
181
|
+
);
|
|
182
|
+
observed.mapping.checkout = caps.supports_checkout || {};
|
|
183
|
+
|
|
184
|
+
observed.capabilities.contact_form = (
|
|
185
|
+
caps.supports_contact?.supported === true ||
|
|
186
|
+
caps[Object.keys(CAPABILITY_MAPPINGS).find(k => k === 'contact_form')]?.supported === true
|
|
187
|
+
);
|
|
188
|
+
observed.mapping.contact_form = caps.supports_contact || {};
|
|
189
|
+
|
|
190
|
+
observed.capabilities.newsletter_signup = (
|
|
191
|
+
caps.supports_newsletter?.supported === true ||
|
|
192
|
+
caps[Object.keys(CAPABILITY_MAPPINGS).find(k => k === 'newsletter_signup')]?.supported === true
|
|
193
|
+
);
|
|
194
|
+
observed.mapping.newsletter_signup = caps.supports_newsletter || {};
|
|
195
|
+
|
|
196
|
+
observed.capabilities.language_switch = (
|
|
197
|
+
caps.supports_language_switch?.supported === true ||
|
|
198
|
+
caps[Object.keys(CAPABILITY_MAPPINGS).find(k => k === 'language_switch')]?.supported === true
|
|
199
|
+
);
|
|
200
|
+
observed.mapping.language_switch = caps.supports_language_switch || {};
|
|
201
|
+
|
|
202
|
+
// Primary CTAs: assume true unless we have explicit evidence of none
|
|
203
|
+
observed.capabilities.primary_ctas = (
|
|
204
|
+
caps.supports_primary_cta?.supported !== false
|
|
205
|
+
);
|
|
206
|
+
observed.mapping.primary_ctas = caps.supports_primary_cta || { supported: true };
|
|
207
|
+
|
|
208
|
+
// PHASE 11: Internal/admin surfaces detection
|
|
209
|
+
// Requires BOTH URL pattern AND UI label evidence to avoid false positives
|
|
210
|
+
observed.capabilities.internal_admin = detectInternalSurfaces(siteIntelligence);
|
|
211
|
+
observed.mapping.internal_admin = { detected: observed.capabilities.internal_admin };
|
|
212
|
+
|
|
213
|
+
return observed;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Detect internal/admin surfaces with strong evidence requirements
|
|
218
|
+
* PHASE 11: Internal surfaces must NOT penalize public readiness
|
|
219
|
+
*
|
|
220
|
+
* Detection requires BOTH:
|
|
221
|
+
* - URL pattern (/admin, /wp-admin, /internal, /dashboard, /staff)
|
|
222
|
+
* - UI label evidence ("Admin", "Staff", "Internal", "Management")
|
|
223
|
+
*
|
|
224
|
+
* @param {Object} siteIntelligence - Site intelligence data
|
|
225
|
+
* @returns {boolean} - true if internal admin surface detected
|
|
226
|
+
*/
|
|
227
|
+
function detectInternalSurfaces(siteIntelligence) {
|
|
228
|
+
if (!siteIntelligence || !siteIntelligence.crawl || !siteIntelligence.crawl.discovered) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const discovered = siteIntelligence.crawl.discovered;
|
|
233
|
+
const internalUrlPatterns = ['/admin', '/wp-admin', '/internal', '/dashboard', '/staff', '/management'];
|
|
234
|
+
const internalLabelPatterns = ['admin', 'staff', 'internal', 'management', 'dashboard'];
|
|
235
|
+
|
|
236
|
+
// Check each discovered URL (can be string or object with url + metadata)
|
|
237
|
+
for (const urlEntry of discovered) {
|
|
238
|
+
// Handle both string URLs and object format
|
|
239
|
+
let url = '';
|
|
240
|
+
let text = '';
|
|
241
|
+
|
|
242
|
+
if (typeof urlEntry === 'string') {
|
|
243
|
+
url = urlEntry;
|
|
244
|
+
} else if (urlEntry && typeof urlEntry === 'object') {
|
|
245
|
+
url = urlEntry.url || urlEntry.href || '';
|
|
246
|
+
text = urlEntry.text || urlEntry.label || urlEntry.linkText || '';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check if URL matches internal pattern
|
|
250
|
+
const hasInternalUrl = internalUrlPatterns.some(pattern => url.toLowerCase().includes(pattern));
|
|
251
|
+
|
|
252
|
+
// Check if text/label matches internal pattern (case-insensitive)
|
|
253
|
+
const hasInternalLabel = text && internalLabelPatterns.some(pattern =>
|
|
254
|
+
text.toLowerCase().includes(pattern)
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Require BOTH URL and label evidence to minimize false positives
|
|
258
|
+
if (hasInternalUrl && hasInternalLabel) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Check if a capability is observed
|
|
268
|
+
*
|
|
269
|
+
* @param {Object} observedCapabilities - Result from extractObservedCapabilities()
|
|
270
|
+
* @param {string} capabilityName - Name of capability (e.g., 'login', 'signup')
|
|
271
|
+
* @returns {boolean} True if capability is observed
|
|
272
|
+
*/
|
|
273
|
+
function isCapabilityObserved(observedCapabilities, capabilityName) {
|
|
274
|
+
if (!observedCapabilities || !observedCapabilities.capabilities) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
return observedCapabilities.capabilities[capabilityName] === true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get applicable attempts based on observed capabilities
|
|
282
|
+
* Maps attempt IDs to the capabilities they require
|
|
283
|
+
*
|
|
284
|
+
* @param {Array} attemptIds - List of attempt IDs to filter
|
|
285
|
+
* @param {Object} observedCapabilities - Result from extractObservedCapabilities()
|
|
286
|
+
* @returns {Object} { applicable: Array, notApplicable: Array }
|
|
287
|
+
*/
|
|
288
|
+
function filterAttemptsByObservedCapabilities(attemptIds, observedCapabilities) {
|
|
289
|
+
const applicable = [];
|
|
290
|
+
const notApplicable = [];
|
|
291
|
+
|
|
292
|
+
if (!attemptIds || !Array.isArray(attemptIds)) {
|
|
293
|
+
return { applicable: [], notApplicable: [] };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for (const attemptId of attemptIds) {
|
|
297
|
+
// Find which capability this attempt requires
|
|
298
|
+
let requiredCapability = null;
|
|
299
|
+
for (const [capName, mapping] of Object.entries(CAPABILITY_MAPPINGS)) {
|
|
300
|
+
if (mapping.attemptIds.includes(attemptId)) {
|
|
301
|
+
requiredCapability = capName;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// If no capability mapping, always run (universal attempt)
|
|
307
|
+
if (!requiredCapability) {
|
|
308
|
+
applicable.push(attemptId);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check if capability is observed
|
|
313
|
+
if (isCapabilityObserved(observedCapabilities, requiredCapability)) {
|
|
314
|
+
applicable.push(attemptId);
|
|
315
|
+
} else {
|
|
316
|
+
notApplicable.push({
|
|
317
|
+
attemptId,
|
|
318
|
+
reason: `Capability not observed: ${requiredCapability}`,
|
|
319
|
+
capabilityRequired: requiredCapability
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return { applicable, notApplicable };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get applicable flows based on observed capabilities
|
|
329
|
+
*
|
|
330
|
+
* @param {Array} flowIds - List of flow IDs to filter
|
|
331
|
+
* @param {Object} observedCapabilities - Result from extractObservedCapabilities()
|
|
332
|
+
* @returns {Object} { applicable: Array, notApplicable: Array }
|
|
333
|
+
*/
|
|
334
|
+
function filterFlowsByObservedCapabilities(flowIds, observedCapabilities) {
|
|
335
|
+
const applicable = [];
|
|
336
|
+
const notApplicable = [];
|
|
337
|
+
|
|
338
|
+
if (!flowIds || !Array.isArray(flowIds)) {
|
|
339
|
+
return { applicable: [], notApplicable: [] };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const flowCapabilityMap = {
|
|
343
|
+
'login_flow': 'login',
|
|
344
|
+
'signup_flow': 'signup',
|
|
345
|
+
'checkout_flow': 'checkout',
|
|
346
|
+
'contact_flow': 'contact_form'
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
for (const flowId of flowIds) {
|
|
350
|
+
const requiredCapability = flowCapabilityMap[flowId];
|
|
351
|
+
|
|
352
|
+
// If no mapping, always run
|
|
353
|
+
if (!requiredCapability) {
|
|
354
|
+
applicable.push(flowId);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Check if capability is observed
|
|
359
|
+
if (isCapabilityObserved(observedCapabilities, requiredCapability)) {
|
|
360
|
+
applicable.push(flowId);
|
|
361
|
+
} else {
|
|
362
|
+
notApplicable.push({
|
|
363
|
+
flowId,
|
|
364
|
+
reason: `Capability not observed: ${requiredCapability}`,
|
|
365
|
+
capabilityRequired: requiredCapability
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return { applicable, notApplicable };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Create NOT_APPLICABLE result for an attempt
|
|
375
|
+
*
|
|
376
|
+
* @param {string} attemptId - Attempt ID
|
|
377
|
+
* @param {string} capabilityName - Name of unobserved capability
|
|
378
|
+
* @returns {Object} Result object with NOT_APPLICABLE outcome
|
|
379
|
+
*/
|
|
380
|
+
function createNotApplicableAttemptResult(attemptId, capabilityName) {
|
|
381
|
+
return {
|
|
382
|
+
attemptId,
|
|
383
|
+
attemptName: attemptId,
|
|
384
|
+
goal: 'Unknown',
|
|
385
|
+
riskCategory: 'UNKNOWN',
|
|
386
|
+
source: 'capability-check',
|
|
387
|
+
outcome: 'NOT_APPLICABLE',
|
|
388
|
+
skipReason: `Capability not observed: ${capabilityName}`,
|
|
389
|
+
skipReasonCode: 'NOT_APPLICABLE', // CRITICAL: Must match SKIP_CODES.NOT_APPLICABLE for coverage calculation
|
|
390
|
+
exitCode: 0, // NOT_APPLICABLE doesn't affect exit code
|
|
391
|
+
executed: false,
|
|
392
|
+
steps: [],
|
|
393
|
+
friction: null,
|
|
394
|
+
error: null
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Create NOT_APPLICABLE result for a flow
|
|
400
|
+
*
|
|
401
|
+
* @param {string} flowId - Flow ID
|
|
402
|
+
* @param {string} capabilityName - Name of unobserved capability
|
|
403
|
+
* @returns {Object} Result object with NOT_APPLICABLE outcome
|
|
404
|
+
*/
|
|
405
|
+
function createNotApplicableFlowResult(flowId, capabilityName) {
|
|
406
|
+
return {
|
|
407
|
+
flowId,
|
|
408
|
+
flowName: flowId,
|
|
409
|
+
outcome: 'NOT_APPLICABLE',
|
|
410
|
+
skipReason: `Capability not observed: ${capabilityName}`,
|
|
411
|
+
skipReasonCode: 'NOT_OBSERVABLE',
|
|
412
|
+
success: null,
|
|
413
|
+
executed: false,
|
|
414
|
+
steps: [],
|
|
415
|
+
error: null
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
module.exports = {
|
|
420
|
+
CAPABILITY_MAPPINGS,
|
|
421
|
+
extractObservedCapabilities,
|
|
422
|
+
isCapabilityObserved,
|
|
423
|
+
filterAttemptsByObservedCapabilities,
|
|
424
|
+
filterFlowsByObservedCapabilities,
|
|
425
|
+
createNotApplicableAttemptResult,
|
|
426
|
+
createNotApplicableFlowResult
|
|
427
|
+
};
|