@odavl/guardian 0.1.0-rc1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +155 -97
  3. package/bin/guardian.js +1544 -55
  4. package/config/README.md +59 -0
  5. package/config/profiles/landing-demo.yaml +16 -0
  6. package/package.json +26 -11
  7. package/policies/landing-demo.json +22 -0
  8. package/src/enterprise/audit-logger.js +166 -0
  9. package/src/enterprise/pdf-exporter.js +267 -0
  10. package/src/enterprise/rbac-gate.js +142 -0
  11. package/src/enterprise/rbac.js +239 -0
  12. package/src/enterprise/site-manager.js +180 -0
  13. package/src/founder/feedback-system.js +156 -0
  14. package/src/founder/founder-tracker.js +213 -0
  15. package/src/founder/usage-signals.js +141 -0
  16. package/src/guardian/alert-ledger.js +121 -0
  17. package/src/guardian/attempt-engine.js +587 -12
  18. package/src/guardian/attempt-registry.js +42 -1
  19. package/src/guardian/attempt-relevance.js +106 -0
  20. package/src/guardian/attempt.js +85 -39
  21. package/src/guardian/attempts-filter.js +63 -0
  22. package/src/guardian/baseline.js +50 -8
  23. package/src/guardian/breakage-intelligence.js +1 -0
  24. package/src/guardian/browser-pool.js +131 -0
  25. package/src/guardian/browser.js +28 -1
  26. package/src/guardian/ci-cli.js +121 -0
  27. package/src/guardian/ci-mode.js +15 -0
  28. package/src/guardian/ci-output.js +38 -0
  29. package/src/guardian/cli-summary.js +167 -67
  30. package/src/guardian/config-loader.js +162 -0
  31. package/src/guardian/data-guardian-detector.js +189 -0
  32. package/src/guardian/detection-layers.js +271 -0
  33. package/src/guardian/drift-detector.js +100 -0
  34. package/src/guardian/enhanced-html-reporter.js +221 -4
  35. package/src/guardian/env-guard.js +127 -0
  36. package/src/guardian/failure-intelligence.js +173 -0
  37. package/src/guardian/first-run-profile.js +89 -0
  38. package/src/guardian/first-run.js +54 -0
  39. package/src/guardian/flag-validator.js +111 -0
  40. package/src/guardian/flow-executor.js +309 -44
  41. package/src/guardian/html-reporter.js +2 -0
  42. package/src/guardian/human-reporter.js +431 -0
  43. package/src/guardian/index.js +22 -19
  44. package/src/guardian/init-command.js +9 -5
  45. package/src/guardian/intent-detector.js +146 -0
  46. package/src/guardian/journey-definitions.js +132 -0
  47. package/src/guardian/journey-scan-cli.js +145 -0
  48. package/src/guardian/journey-scanner.js +583 -0
  49. package/src/guardian/junit-reporter.js +18 -1
  50. package/src/guardian/language-detection.js +99 -0
  51. package/src/guardian/live-cli.js +95 -0
  52. package/src/guardian/live-scheduler-runner.js +137 -0
  53. package/src/guardian/live-scheduler.js +146 -0
  54. package/src/guardian/market-reporter.js +357 -82
  55. package/src/guardian/parallel-executor.js +116 -0
  56. package/src/guardian/pattern-analyzer.js +348 -0
  57. package/src/guardian/policy.js +80 -3
  58. package/src/guardian/prerequisite-checker.js +101 -0
  59. package/src/guardian/preset-loader.js +27 -18
  60. package/src/guardian/profile-loader.js +96 -0
  61. package/src/guardian/reality.js +1612 -115
  62. package/src/guardian/reporter.js +27 -41
  63. package/src/guardian/run-artifacts.js +212 -0
  64. package/src/guardian/run-cleanup.js +207 -0
  65. package/src/guardian/run-latest.js +90 -0
  66. package/src/guardian/run-list.js +211 -0
  67. package/src/guardian/run-summary.js +20 -0
  68. package/src/guardian/scan-presets.js +100 -11
  69. package/src/guardian/selector-fallbacks.js +394 -0
  70. package/src/guardian/semantic-contact-detection.js +255 -0
  71. package/src/guardian/semantic-contact-finder.js +201 -0
  72. package/src/guardian/semantic-targets.js +234 -0
  73. package/src/guardian/site-introspection.js +257 -0
  74. package/src/guardian/smoke.js +258 -0
  75. package/src/guardian/snapshot-schema.js +25 -1
  76. package/src/guardian/snapshot.js +69 -3
  77. package/src/guardian/stability-scorer.js +169 -0
  78. package/src/guardian/success-evaluator.js +214 -0
  79. package/src/guardian/template-command.js +184 -0
  80. package/src/guardian/text-formatters.js +426 -0
  81. package/src/guardian/timeout-profiles.js +57 -0
  82. package/src/guardian/verdict.js +320 -0
  83. package/src/guardian/verdicts.js +74 -0
  84. package/src/guardian/wait-for-outcome.js +120 -0
  85. package/src/guardian/watch-runner.js +181 -0
  86. package/src/payments/stripe-checkout.js +169 -0
  87. package/src/plans/plan-definitions.js +148 -0
  88. package/src/plans/plan-manager.js +211 -0
  89. package/src/plans/usage-tracker.js +210 -0
  90. package/src/recipes/recipe-engine.js +188 -0
  91. package/src/recipes/recipe-failure-analysis.js +159 -0
  92. package/src/recipes/recipe-registry.js +134 -0
  93. package/src/recipes/recipe-runtime.js +507 -0
  94. package/src/recipes/recipe-store.js +410 -0
  95. package/guardian-contract-v1.md +0 -149
  96. /package/{guardian.config.json → config/guardian.config.json} +0 -0
  97. /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
  98. /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
  99. /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
  100. /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
  101. /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Data Guardian Attribute Detector
3
+ *
4
+ * Deterministic detection of data-guardian attributes for stable element targeting.
5
+ * Supports attribute variants and tokenized values.
6
+ *
7
+ * LAYER 1 in the detection priority hierarchy (highest priority).
8
+ */
9
+
10
+ const { normalizeText } = require('./semantic-targets');
11
+
12
+ /**
13
+ * Supported data-guardian attribute targets
14
+ */
15
+ const GUARDIAN_TARGETS = {
16
+ CONTACT: 'contact',
17
+ ABOUT: 'about',
18
+ FORM: 'form',
19
+ SUBMIT: 'submit'
20
+ };
21
+
22
+ /**
23
+ * Find elements by data-guardian attribute
24
+ *
25
+ * @param {Page} page - Playwright page object
26
+ * @param {string} target - Target to search for (contact, about, form, submit)
27
+ * @returns {Promise<Array>} Array of matching elements with metadata
28
+ */
29
+ async function findByGuardianAttribute(page, target) {
30
+ if (!GUARDIAN_TARGETS[target.toUpperCase()]) {
31
+ throw new Error(`Unsupported guardian target: ${target}`);
32
+ }
33
+
34
+ const normalizedTarget = target.toLowerCase();
35
+
36
+ try {
37
+ const results = await page.evaluate((targetName) => {
38
+ const elements = [];
39
+
40
+ // Rule 1: Exact match on data-guardian attribute
41
+ const exactMatches = document.querySelectorAll(`[data-guardian="${targetName}"]`);
42
+ for (const el of exactMatches) {
43
+ elements.push({
44
+ element: el,
45
+ tagName: el.tagName.toLowerCase(),
46
+ text: el.textContent?.trim() || '',
47
+ href: el.href || el.getAttribute('href') || '',
48
+ ariaLabel: el.getAttribute('aria-label') || '',
49
+ title: el.getAttribute('title') || '',
50
+ dataGuardian: el.getAttribute('data-guardian'),
51
+ dataGuardianVariant: null,
52
+ matchType: 'exact', // Exact attribute match
53
+ confidence: 'high'
54
+ });
55
+ }
56
+
57
+ // Rule 2: Tokenized match (e.g., "contact primary" contains "contact")
58
+ const allGuardianElements = document.querySelectorAll('[data-guardian]');
59
+ for (const el of allGuardianElements) {
60
+ const dataGuardian = el.getAttribute('data-guardian') || '';
61
+ const tokens = dataGuardian.split(/[\s\-_]+/).map(t => t.toLowerCase());
62
+
63
+ if (tokens.includes(targetName)) {
64
+ // Only add if not already in exact matches
65
+ const alreadyFound = Array.from(exactMatches).includes(el);
66
+ if (!alreadyFound) {
67
+ elements.push({
68
+ element: el,
69
+ tagName: el.tagName.toLowerCase(),
70
+ text: el.textContent?.trim() || '',
71
+ href: el.href || el.getAttribute('href') || '',
72
+ ariaLabel: el.getAttribute('aria-label') || '',
73
+ title: el.getAttribute('title') || '',
74
+ dataGuardian: el.getAttribute('data-guardian'),
75
+ dataGuardianVariant: null,
76
+ matchType: 'tokenized', // Tokenized match
77
+ confidence: 'high'
78
+ });
79
+ }
80
+ }
81
+ }
82
+
83
+ // Rule 3: Variant attributes (e.g., data-guardian-role="contact")
84
+ const variantAttr = `data-guardian-${targetName}`;
85
+ const variantMatches = document.querySelectorAll(`[${variantAttr}]`);
86
+ for (const el of variantMatches) {
87
+ const variantValue = el.getAttribute(variantAttr);
88
+ elements.push({
89
+ element: el,
90
+ tagName: el.tagName.toLowerCase(),
91
+ text: el.textContent?.trim() || '',
92
+ href: el.href || el.getAttribute('href') || '',
93
+ ariaLabel: el.getAttribute('aria-label') || '',
94
+ title: el.getAttribute('title') || '',
95
+ dataGuardian: el.getAttribute('data-guardian'),
96
+ dataGuardianVariant: variantAttr, // Track the variant attribute name
97
+ matchType: 'variant', // Variant attribute match
98
+ confidence: 'medium'
99
+ });
100
+ }
101
+
102
+ return elements;
103
+ }, normalizedTarget);
104
+
105
+ return results;
106
+ } catch (error) {
107
+ console.warn(`Failed to find elements by guardian attribute: ${error.message}`);
108
+ return [];
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Check if element has any data-guardian attribute
114
+ * Returns the attribute value and match type
115
+ */
116
+ async function getGuardianAttribute(page, selector) {
117
+ try {
118
+ const result = await page.evaluate((sel) => {
119
+ const el = document.querySelector(sel);
120
+ if (!el) return null;
121
+
122
+ const dataGuardian = el.getAttribute('data-guardian');
123
+ const allAttrs = el.attributes;
124
+ let variantAttr = null;
125
+
126
+ // Check for variant attributes
127
+ for (const attr of allAttrs) {
128
+ if (attr.name.startsWith('data-guardian-')) {
129
+ variantAttr = {
130
+ name: attr.name,
131
+ value: attr.value
132
+ };
133
+ break;
134
+ }
135
+ }
136
+
137
+ return {
138
+ dataGuardian: dataGuardian,
139
+ variantAttribute: variantAttr,
140
+ hasGuardianAttr: !!dataGuardian || !!variantAttr
141
+ };
142
+ }, selector);
143
+
144
+ return result;
145
+ } catch (error) {
146
+ console.warn(`Failed to get guardian attribute: ${error.message}`);
147
+ return null;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Build CSS selector for a data-guardian attribute
153
+ */
154
+ function buildGuardianSelector(target, variant = null) {
155
+ if (variant) {
156
+ // Variant attribute selector
157
+ return `[data-guardian-${target}]`;
158
+ } else {
159
+ // Standard data-guardian attribute selector
160
+ return `[data-guardian="${target}"]`;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Check if an element's data-guardian value matches a target
166
+ */
167
+ function matchesGuardianTarget(elementDataGuardian, target) {
168
+ if (!elementDataGuardian) return false;
169
+
170
+ const normalized = normalizeText(elementDataGuardian);
171
+ const normalizedTarget = normalizeText(target);
172
+
173
+ // Exact match
174
+ if (normalized === normalizedTarget) return true;
175
+
176
+ // Tokenized match (split by whitespace/hyphens)
177
+ const tokens = normalized.split(/[\s\-_]+/);
178
+ if (tokens.includes(normalizedTarget)) return true;
179
+
180
+ return false;
181
+ }
182
+
183
+ module.exports = {
184
+ findByGuardianAttribute,
185
+ getGuardianAttribute,
186
+ buildGuardianSelector,
187
+ matchesGuardianTarget,
188
+ GUARDIAN_TARGETS
189
+ };
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Detection Priority Layers
3
+ *
4
+ * Implements deterministic, layered detection with strict priority order:
5
+ * LAYER 0: profile overrides (highest)
6
+ * LAYER 1: data-guardian attributes
7
+ * LAYER 2: semantic href matching
8
+ * LAYER 3: semantic visible text matching
9
+ * LAYER 4: structural heuristics (nav/footer proximity)
10
+ *
11
+ * Each detection includes metadata: target, source layer, confidence, evidence.
12
+ */
13
+
14
+ const { findByGuardianAttribute, matchesGuardianTarget, buildGuardianSelector } = require('./data-guardian-detector');
15
+ const { detectContactCandidates, DETECTION_SOURCE, CONFIDENCE } = require('./semantic-contact-detection');
16
+ const { resolveProfileForUrl } = require('./profile-loader');
17
+
18
+ /**
19
+ * Detection layer names (for reporting)
20
+ */
21
+ const LAYER = {
22
+ PROFILE: 'profile',
23
+ DATA_GUARDIAN: 'data-guardian',
24
+ HREF: 'href',
25
+ TEXT: 'text',
26
+ STRUCTURE: 'structure'
27
+ };
28
+
29
+ /**
30
+ * Detect element by layer, respecting strict priority
31
+ *
32
+ * @param {Page} page - Playwright page object
33
+ * @param {string} target - Target to detect (contact, about, form, submit)
34
+ * @param {string} baseUrl - Base URL for relative links
35
+ * @returns {Promise<Object>} Detection result with layer, confidence, evidence
36
+ */
37
+ async function detectByLayers(page, target, baseUrl = '') {
38
+ const result = {
39
+ target: target,
40
+ found: false,
41
+ layer: null,
42
+ confidence: null,
43
+ evidence: null,
44
+ candidates: [],
45
+ primaryCandidate: null,
46
+ reason: ''
47
+ };
48
+
49
+ try {
50
+ const profile = resolveProfileForUrl(baseUrl || page.url());
51
+ if (profile) {
52
+ console.log(`🔧 Loaded profile for ${profile.site}`);
53
+ const profileSelector = (profile.selectors || {})[target];
54
+ if (profileSelector) {
55
+ let nodes = [];
56
+ try {
57
+ nodes = await page.$$(profileSelector);
58
+ } catch (err) {
59
+ console.log(` ❌ Profile selector error for ${target}: ${err.message}`);
60
+ return {
61
+ ...result,
62
+ layer: LAYER.PROFILE,
63
+ reason: `Profile selector error for ${target}: ${err.message}`,
64
+ profileSite: profile.site,
65
+ hardFailure: true
66
+ };
67
+ }
68
+
69
+ if (!nodes || nodes.length === 0) {
70
+ console.log(` ❌ Profile selector for ${target} not found on page (${profile.site})`);
71
+ return {
72
+ ...result,
73
+ layer: LAYER.PROFILE,
74
+ reason: `Profile selector for ${target} not found (${profileSelector})`,
75
+ profileSite: profile.site,
76
+ hardFailure: true
77
+ };
78
+ }
79
+
80
+ console.log(` 🎯 Using profile selector for ${target}: ${profileSelector}`);
81
+ const candidates = [{
82
+ selector: profileSelector,
83
+ matchedText: null,
84
+ matchedToken: target,
85
+ source: 'profile',
86
+ confidence: CONFIDENCE.HIGH,
87
+ href: null,
88
+ ariaLabel: null,
89
+ tagName: nodes[0] ? await nodes[0].evaluate(el => el.tagName.toLowerCase()).catch(() => undefined) : undefined
90
+ }];
91
+ return {
92
+ ...result,
93
+ found: true,
94
+ layer: LAYER.PROFILE,
95
+ confidence: CONFIDENCE.HIGH,
96
+ candidates,
97
+ primaryCandidate: candidates[0],
98
+ evidence: `Profile selector ${profileSelector}`,
99
+ reason: `Detected via profile override (site: ${profile.site})`,
100
+ profileSite: profile.site
101
+ };
102
+ } else {
103
+ console.log(` 🔧 Profile loaded for ${profile.site} (no selector for ${target}, falling back)`);
104
+ }
105
+ }
106
+
107
+ // LAYER 1: data-guardian attributes (HIGHEST priority)
108
+ const guardianResults = await detectLayer1DataGuardian(page, target);
109
+ if (guardianResults.length > 0) {
110
+ result.found = true;
111
+ result.layer = LAYER.DATA_GUARDIAN;
112
+ result.confidence = CONFIDENCE.HIGH;
113
+ result.candidates = guardianResults;
114
+ result.primaryCandidate = guardianResults[0];
115
+ result.evidence = `Exact data-guardian="${target}" attribute match`;
116
+ result.reason = 'Highest priority layer matched: data-guardian attribute provides guaranteed stability.';
117
+ return result;
118
+ }
119
+
120
+ // LAYER 2: semantic href matching
121
+ const hrefResults = await detectLayer2Href(page, target, baseUrl);
122
+ if (hrefResults.length > 0) {
123
+ result.found = true;
124
+ result.layer = LAYER.HREF;
125
+ result.confidence = CONFIDENCE.HIGH;
126
+ result.candidates = hrefResults;
127
+ result.primaryCandidate = hrefResults[0];
128
+ result.evidence = hrefResults[0].matchedToken;
129
+ result.reason = 'Matched via href attribute using semantic tokens (e.g., "/kontakt" matches German token "kontakt").';
130
+ return result;
131
+ }
132
+
133
+ // LAYER 3: semantic visible text matching
134
+ const textResults = await detectLayer3Text(page, target, baseUrl);
135
+ if (textResults.length > 0) {
136
+ result.found = true;
137
+ result.layer = LAYER.TEXT;
138
+ result.confidence = textResults[0].confidence; // medium or high if in nav/footer
139
+ result.candidates = textResults;
140
+ result.primaryCandidate = textResults[0];
141
+ result.evidence = `Text "${textResults[0].matchedText}" matched token "${textResults[0].matchedToken}"`;
142
+ result.reason = `Matched via visible text using semantic tokens. ${
143
+ result.confidence === CONFIDENCE.HIGH ?
144
+ 'Located in navigation/footer (high confidence).' :
145
+ 'Consider adding data-guardian="' + target + '" for guaranteed stability.'
146
+ }`;
147
+ return result;
148
+ }
149
+
150
+ // LAYER 4: structural heuristics (LOWEST priority)
151
+ const structureResults = await detectLayer4Structure(page, target, baseUrl);
152
+ if (structureResults.length > 0) {
153
+ result.found = true;
154
+ result.layer = LAYER.STRUCTURE;
155
+ result.confidence = CONFIDENCE.LOW;
156
+ result.candidates = structureResults;
157
+ result.primaryCandidate = structureResults[0];
158
+ result.evidence = `Located in ${structureResults[0].source} based on page structure`;
159
+ result.reason = `Heuristic detection only (low confidence). Add data-guardian="${target}" attribute for guaranteed stability.`;
160
+ return result;
161
+ }
162
+
163
+ // Nothing found
164
+ result.reason = `No ${target} element detected. Consider:
165
+ 1. Adding data-guardian="${target}" attribute
166
+ 2. Using semantic-friendly text (e.g., "Contact", "Kontakt", "Contacto")
167
+ 3. Using semantic-friendly href (e.g., "/contact", "/kontakt")`;
168
+
169
+ return result;
170
+ } catch (error) {
171
+ console.warn(`Detection by layers failed: ${error.message}`);
172
+ result.reason = `Detection error: ${error.message}`;
173
+ return result;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * LAYER 1: data-guardian attribute detection (HIGHEST priority)
179
+ */
180
+ async function detectLayer1DataGuardian(page, target) {
181
+ try {
182
+ const elements = await findByGuardianAttribute(page, target);
183
+ return elements.map(el => ({
184
+ selector: buildGuardianSelector(target),
185
+ matchedText: el.text || el.dataGuardian,
186
+ matchedToken: target,
187
+ source: DETECTION_SOURCE.DATA_GUARDIAN,
188
+ confidence: CONFIDENCE.HIGH,
189
+ href: el.href,
190
+ ariaLabel: el.ariaLabel,
191
+ tagName: el.tagName
192
+ }));
193
+ } catch (error) {
194
+ // Guardian target not supported or error occurred
195
+ return [];
196
+ }
197
+ }
198
+
199
+ /**
200
+ * LAYER 2: semantic href matching (semantic + deterministic)
201
+ */
202
+ async function detectLayer2Href(page, target, baseUrl) {
203
+ try {
204
+ // Use Wave 1.1 semantic detection but filter by target and href-only
205
+ const allCandidates = await detectContactCandidates(page, baseUrl);
206
+
207
+ // Filter: only href-based matches for the requested target
208
+ const hrefMatches = allCandidates.filter(c =>
209
+ c.source === DETECTION_SOURCE.HREF &&
210
+ isTargetMatch(target, c.matchedToken)
211
+ );
212
+
213
+ return hrefMatches;
214
+ } catch (error) {
215
+ console.warn(`Layer 2 href detection failed: ${error.message}`);
216
+ return [];
217
+ }
218
+ }
219
+
220
+ /**
221
+ * LAYER 3: semantic visible text matching
222
+ */
223
+ async function detectLayer3Text(page, target, baseUrl) {
224
+ try {
225
+ // Use Wave 1.1 semantic detection but filter by target and text source
226
+ const allCandidates = await detectContactCandidates(page, baseUrl);
227
+
228
+ // Filter: text-based or nav/footer matches for the requested target
229
+ const textMatches = allCandidates.filter(c =>
230
+ (c.source === DETECTION_SOURCE.TEXT || c.source === DETECTION_SOURCE.NAV_FOOTER) &&
231
+ isTargetMatch(target, c.matchedToken)
232
+ );
233
+
234
+ return textMatches;
235
+ } catch (error) {
236
+ console.warn(`Layer 3 text detection failed: ${error.message}`);
237
+ return [];
238
+ }
239
+ }
240
+
241
+ /**
242
+ * LAYER 4: structural heuristics (LOWEST priority, fallback only)
243
+ */
244
+ async function detectLayer4Structure(page, target, baseUrl) {
245
+ // Structural heuristics would look for elements in footer, nav, etc.
246
+ // by position alone (no semantic matching)
247
+ // Currently a placeholder — can be enhanced in future waves
248
+ return [];
249
+ }
250
+
251
+ /**
252
+ * Check if a matched token corresponds to the requested target
253
+ * (e.g., "kontakt" token matches "contact" target in German)
254
+ */
255
+ function isTargetMatch(target, matchedToken) {
256
+ // For Wave 1.2, we simplify: contact tokens match contact target, etc.
257
+ // This can be expanded to support token-to-target mappings if needed
258
+ if (target.toLowerCase() === 'contact') {
259
+ // Wave 1.1 already groups all contact tokens under 'contact'
260
+ return matchedToken && matchedToken.toLowerCase() !== 'about';
261
+ }
262
+ // Extend as needed for other targets
263
+ return false;
264
+ }
265
+
266
+ module.exports = {
267
+ detectByLayers,
268
+ LAYER,
269
+ DETECTION_SOURCE,
270
+ CONFIDENCE
271
+ };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Drift Detection Engine for Journey Results
3
+ * Deterministic comparison against stored baseline snapshot.
4
+ */
5
+
6
+ function buildBaselineFromJourneyResult(result) {
7
+ const keySteps = (result.executedSteps || []).map(s => ({ id: s.id, success: !!s.success }));
8
+ const baseline = {
9
+ decision: result.finalDecision,
10
+ intent: (result.intentDetection && result.intentDetection.intent) || 'unknown',
11
+ goalReached: !!(result.goal && result.goal.goalReached),
12
+ keySteps,
13
+ timestamp: new Date().toISOString()
14
+ };
15
+ return baseline;
16
+ }
17
+
18
+ function compareAgainstBaseline(baseline, current) {
19
+ const reasons = [];
20
+ const currentSteps = {};
21
+ (current.executedSteps || []).forEach(s => { currentSteps[s.id] = !!s.success; });
22
+
23
+ // Decision drift
24
+ const before = baseline.decision;
25
+ const after = current.finalDecision;
26
+ if (before === 'SAFE' && (after === 'RISK' || after === 'DO_NOT_LAUNCH')) {
27
+ reasons.push(`Decision regressed: ${before} → ${after}`);
28
+ }
29
+
30
+ // Goal drift
31
+ const baselineGoal = !!baseline.goalReached;
32
+ const currentGoal = !!(current.goal && current.goal.goalReached);
33
+ if (baselineGoal && !currentGoal) {
34
+ reasons.push('Visitors can no longer reach goal');
35
+ }
36
+
37
+ // Intent drift
38
+ const baselineIntent = (baseline.intent || 'unknown').toLowerCase();
39
+ const currentIntent = ((current.intentDetection && current.intentDetection.intent) || 'unknown').toLowerCase();
40
+ if (baselineIntent !== 'unknown' && currentIntent !== 'unknown' && baselineIntent !== currentIntent) {
41
+ reasons.push(`Site intent changed from ${baselineIntent.toUpperCase()} to ${currentIntent.toUpperCase()}`);
42
+ }
43
+
44
+ // Critical step regression: any step that succeeded before now fails
45
+ for (const ks of baseline.keySteps || []) {
46
+ if (ks.success === true) {
47
+ const now = currentSteps[ks.id];
48
+ if (now === false || now === undefined) {
49
+ reasons.push(`Critical step failed: ${ks.id}`);
50
+ }
51
+ }
52
+ }
53
+
54
+ return {
55
+ hasDrift: reasons.length > 0,
56
+ reasons: reasons
57
+ };
58
+ }
59
+
60
+ function classifySeverity(driftInfo, currentResult) {
61
+ if (!driftInfo || !driftInfo.hasDrift) return 'NONE';
62
+
63
+ const reasons = driftInfo.reasons || [];
64
+
65
+ // CRITICAL conditions:
66
+ // 1. Decision drifted to DO_NOT_LAUNCH
67
+ if (currentResult.decision === 'DO_NOT_LAUNCH') {
68
+ // Exception: if run stability is very low (<50), downgrade to WARN
69
+ // unless site is unreachable
70
+ if (!reasons.some(r => r.includes('SITE_UNREACHABLE'))) {
71
+ const stabilityScore = currentResult.stability?.runStabilityScore || 100;
72
+ if (stabilityScore < 50) {
73
+ return 'WARN'; // Downgrade: unstable run, re-check recommended
74
+ }
75
+ }
76
+ return 'CRITICAL';
77
+ }
78
+
79
+ // 2. Site unreachable - always CRITICAL (do not downgrade)
80
+ if (reasons.some(r => r.includes('SITE_UNREACHABLE'))) return 'CRITICAL';
81
+
82
+ // 3. Goal drift (visitors can no longer reach goal)
83
+ if (reasons.some(r => r.includes('goal drift: true → false'))) {
84
+ // Check stability before deciding
85
+ const stabilityScore = currentResult.stability?.runStabilityScore || 100;
86
+ if (stabilityScore < 50) {
87
+ return 'WARN'; // Downgrade: unstable, suspicious drift
88
+ }
89
+ return 'CRITICAL';
90
+ }
91
+
92
+ // Default: WARN for all other drift
93
+ return 'WARN';
94
+ }
95
+
96
+ module.exports = {
97
+ buildBaselineFromJourneyResult,
98
+ compareAgainstBaseline,
99
+ classifySeverity
100
+ };