@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,426 @@
1
+ /**
2
+ * Text Formatters - Single Source of Truth
3
+ *
4
+ * All verdict, findings, limits, and pattern text is generated here
5
+ * and rendered identically across CLI, HTML, JUnit, and decision.json.
6
+ *
7
+ * Layer 4 / Step 4.1: Consistency Lock Across Outputs
8
+ */
9
+
10
+ /**
11
+ * Format verdict summary line
12
+ */
13
+ function formatVerdictStatus(verdict) {
14
+ if (!verdict) return 'No verdict available';
15
+ return verdict.verdict || 'UNKNOWN';
16
+ }
17
+
18
+ /**
19
+ * Format confidence line
20
+ */
21
+ function formatConfidence(verdict) {
22
+ if (!verdict || !verdict.confidence) return 'n/a';
23
+ const cf = verdict.confidence;
24
+ const level = cf.level || 'n/a';
25
+ const score = typeof cf.score === 'number' ? cf.score.toFixed(2) : 'n/a';
26
+ return `${level} (${score})`;
27
+ }
28
+
29
+ /**
30
+ * Format verdict why
31
+ */
32
+ function formatVerdictWhy(verdict) {
33
+ if (!verdict || !verdict.why) return null;
34
+ return verdict.why;
35
+ }
36
+
37
+ /**
38
+ * Format key findings (returns array of strings)
39
+ */
40
+ function formatKeyFindings(verdict) {
41
+ if (!verdict || !Array.isArray(verdict.keyFindings)) return [];
42
+ return verdict.keyFindings.slice(0, 7);
43
+ }
44
+
45
+ /**
46
+ * Format limits (returns array of strings)
47
+ */
48
+ function formatLimits(verdict) {
49
+ if (!verdict || !Array.isArray(verdict.limits)) return [];
50
+ return verdict.limits.slice(0, 6);
51
+ }
52
+
53
+ /**
54
+ * Format pattern summary
55
+ */
56
+ function formatPatternSummary(pattern) {
57
+ if (!pattern) return '';
58
+ return pattern.summary || '';
59
+ }
60
+
61
+ /**
62
+ * Format pattern why it matters
63
+ */
64
+ function formatPatternWhy(pattern) {
65
+ if (!pattern) return '';
66
+ return pattern.whyItMatters || '';
67
+ }
68
+
69
+ /**
70
+ * Format pattern recommended focus
71
+ */
72
+ function formatPatternFocus(pattern) {
73
+ if (!pattern) return null;
74
+ return pattern.recommendedFocus || null;
75
+ }
76
+
77
+ /**
78
+ * Format pattern limits
79
+ */
80
+ function formatPatternLimits(pattern) {
81
+ if (!pattern) return null;
82
+ return pattern.limits || null;
83
+ }
84
+
85
+ /**
86
+ * Format confidence interpretation micro-line
87
+ */
88
+ function formatConfidenceMicroLine() {
89
+ return 'Confidence reflects the strength of evidence from outcomes, coverage, and captured artifacts.';
90
+ }
91
+
92
+ /**
93
+ * Format first-run note
94
+ */
95
+ function formatFirstRunNote() {
96
+ return 'This verdict reflects this moment; repeat runs strengthen confidence.';
97
+ }
98
+
99
+ /**
100
+ * Format journey message for run N
101
+ */
102
+ function formatJourneyMessage(runIndex) {
103
+ if (runIndex === 0) {
104
+ return 'Run 1/3: establishing a baseline for this site.';
105
+ } else if (runIndex === 1) {
106
+ return 'Run 2/3: checking for repeat signals.';
107
+ }
108
+ return null;
109
+ }
110
+
111
+ /**
112
+ * Format next-run hint
113
+ */
114
+ function formatNextRunHint(verdict) {
115
+ if (!verdict || !verdict.nextRunHint) return null;
116
+ return verdict.nextRunHint;
117
+ }
118
+
119
+ /**
120
+ * Format confidence drivers (returns array of strings)
121
+ */
122
+ function formatConfidenceDrivers(verdict) {
123
+ if (!verdict || !verdict.confidence || !Array.isArray(verdict.confidence.reasons)) return [];
124
+ return verdict.confidence.reasons.slice(0, 3);
125
+ }
126
+
127
+ /**
128
+ * Format focus summary (Layer 5 - Advisor Mode)
129
+ *
130
+ * Derives prioritization from existing verdict, confidence, patterns, and limits.
131
+ * Returns array of focus lines (max 3) — NOT advice, NOT commands, only attention priorities.
132
+ *
133
+ * Display when: verdict !== READY OR confidence !== high OR patterns.length > 0
134
+ * Suppress when: READY + high confidence + no patterns
135
+ *
136
+ * Derivation logic:
137
+ * - High-confidence patterns dominate
138
+ * - Single point of failure outranks others
139
+ * - Confidence degradation outranks friction
140
+ * - Repeated "not executed" indicates coverage focus
141
+ * - Limits provide context, not priority
142
+ *
143
+ * @param {object} verdict - Verdict object
144
+ * @param {array} patterns - Array of detected patterns (from analyzePatterns)
145
+ * @returns {array} Array of focus lines (max 3)
146
+ */
147
+ function formatFocusSummary(verdict, patterns = []) {
148
+ if (!verdict) return [];
149
+
150
+ const vStatus = verdict.verdict || 'UNKNOWN';
151
+ const cfLevel = (verdict.confidence || {}).level || 'n/a';
152
+
153
+ // Suppress when READY + high confidence + no patterns
154
+ if (vStatus === 'READY' && cfLevel === 'high' && patterns.length === 0) {
155
+ return [];
156
+ }
157
+
158
+ const focus = [];
159
+
160
+ // Priority 1: Single point of failure patterns (critical blockers)
161
+ const spofPatterns = patterns.filter(p => p.type === 'single_point_failure');
162
+ spofPatterns.forEach(p => {
163
+ if (focus.length >= 3) return;
164
+ const path = p.pathName || 'a critical path';
165
+ focus.push(`${path} is blocked and prevents user progress`);
166
+ });
167
+
168
+ // Priority 2: Confidence degradation (quality trending down)
169
+ const degradationPatterns = patterns.filter(p => p.type === 'confidence_degradation');
170
+ degradationPatterns.forEach(p => {
171
+ if (focus.length >= 3) return;
172
+ const path = p.pathName || 'flow quality';
173
+ focus.push(`${path} declining across recent runs`);
174
+ });
175
+
176
+ // Priority 3: Recurring friction (persistent issues)
177
+ const frictionPatterns = patterns.filter(p => p.type === 'recurring_friction');
178
+ frictionPatterns.forEach(p => {
179
+ if (focus.length >= 3) return;
180
+ const path = p.pathName || 'this path';
181
+ focus.push(`${path} experiencing repeated friction`);
182
+ });
183
+
184
+ // Priority 4: Repeated skipped/not-executed (coverage gaps)
185
+ const skippedPatterns = patterns.filter(p => p.type === 'repeated_skipped_attempts');
186
+ skippedPatterns.forEach(p => {
187
+ if (focus.length >= 3) return;
188
+ const path = p.pathName || 'path';
189
+ focus.push(`Coverage gap: ${path} not yet exercised`);
190
+ });
191
+
192
+ // If no patterns but verdict is not READY or confidence is not high, derive from verdict
193
+ if (focus.length === 0 && (vStatus !== 'READY' || cfLevel !== 'high')) {
194
+ if (vStatus === 'BLOCKED') {
195
+ focus.push('Site functionality is blocked');
196
+ } else if (vStatus === 'FRICTION') {
197
+ focus.push('Site experiencing friction in user flows');
198
+ }
199
+
200
+ // Add confidence-based focus if low/medium
201
+ if (cfLevel === 'low' && focus.length < 3) {
202
+ focus.push('Evidence strength is limited for current verdict');
203
+ } else if (cfLevel === 'medium' && focus.length < 3) {
204
+ focus.push('Confidence moderate; additional runs would strengthen assessment');
205
+ }
206
+ }
207
+
208
+ return focus.slice(0, 3); // Hard limit: max 3
209
+ }
210
+
211
+ /**
212
+ * Format delta insight (Stage V / Step 5.1)
213
+ *
214
+ * Compares current run (N) vs previous run (N-1) and generates minimal delta insight:
215
+ * - What improved
216
+ * - What regressed
217
+ *
218
+ * Max 2 lines total (1 improved + 1 regressed).
219
+ * Suppresses if no meaningful change detected.
220
+ *
221
+ * @param {object} currentVerdict - Current run verdict
222
+ * @param {object} previousVerdict - Previous run verdict (N-1)
223
+ * @param {array} currentPatterns - Current run patterns
224
+ * @param {array} previousPatterns - Previous run patterns (N-1)
225
+ * @returns {object} - { improved: string[], regressed: string[] }
226
+ */
227
+ function formatDeltaInsight(currentVerdict, previousVerdict, currentPatterns = [], previousPatterns = []) {
228
+ const result = { improved: [], regressed: [] };
229
+
230
+ // Early return if no previous run
231
+ if (!previousVerdict) {
232
+ return result;
233
+ }
234
+
235
+ const currentStatus = (currentVerdict && currentVerdict.verdict) || 'UNKNOWN';
236
+ const previousStatus = (previousVerdict && previousVerdict.verdict) || 'UNKNOWN';
237
+
238
+ const currentConfLevel = (currentVerdict && currentVerdict.confidence && currentVerdict.confidence.level) || 'n/a';
239
+ const previousConfLevel = (previousVerdict && previousVerdict.confidence && previousVerdict.confidence.level) || 'n/a';
240
+
241
+ // Verdict hierarchy: READY > FRICTION > DO_NOT_LAUNCH/BLOCKED
242
+ const verdictRank = { 'READY': 3, 'FRICTION': 2, 'DO_NOT_LAUNCH': 1, 'BLOCKED': 1, 'UNKNOWN': 0 };
243
+ const currentRank = verdictRank[currentStatus] || 0;
244
+ const previousRank = verdictRank[previousStatus] || 0;
245
+
246
+ // Priority 1: Verdict change
247
+ if (currentRank > previousRank) {
248
+ result.improved.push('Overall readiness improved compared to the previous run');
249
+ } else if (currentRank < previousRank) {
250
+ result.regressed.push('Overall readiness declined compared to the previous run');
251
+ }
252
+
253
+ // Priority 2: Confidence level change (only if verdict didn't change)
254
+ if (currentRank === previousRank && currentConfLevel !== previousConfLevel) {
255
+ const confRank = { 'high': 3, 'medium': 2, 'low': 1, 'n/a': 0 };
256
+ const currentConfRank = confRank[currentConfLevel] || 0;
257
+ const previousConfRank = confRank[previousConfLevel] || 0;
258
+
259
+ if (currentConfRank > previousConfRank && result.improved.length === 0) {
260
+ result.improved.push('Confidence in verdict strengthened compared to the previous run');
261
+ } else if (currentConfRank < previousConfRank && result.regressed.length === 0) {
262
+ result.regressed.push('Confidence in verdict weakened compared to the previous run');
263
+ }
264
+ }
265
+
266
+ // Priority 3: Pattern changes (only if no verdict or confidence change)
267
+ if (result.improved.length === 0 && result.regressed.length === 0) {
268
+ // Check for resolved critical patterns
269
+ const previousCriticalPatterns = previousPatterns.filter(p =>
270
+ p.type === 'single_point_failure' || p.severity === 'critical'
271
+ );
272
+ const currentCriticalPatterns = currentPatterns.filter(p =>
273
+ p.type === 'single_point_failure' || p.severity === 'critical'
274
+ );
275
+
276
+ // Pattern resolved
277
+ if (previousCriticalPatterns.length > 0 && currentCriticalPatterns.length === 0) {
278
+ result.improved.push('Previously observed friction was not detected in this run');
279
+ }
280
+
281
+ // New critical pattern appeared
282
+ if (previousCriticalPatterns.length === 0 && currentCriticalPatterns.length > 0) {
283
+ result.regressed.push('New blocking issues were observed since the last run');
284
+ } else if (currentCriticalPatterns.length > previousCriticalPatterns.length) {
285
+ result.regressed.push('A recurring failure pattern emerged in this run');
286
+ }
287
+ }
288
+
289
+ // Enforce max 2 lines total (1 improved + 1 regressed)
290
+ return {
291
+ improved: result.improved.slice(0, 1),
292
+ regressed: result.regressed.slice(0, 1)
293
+ };
294
+ }
295
+
296
+ /**
297
+ * ═══════════════════════════════════════════════════════════════════
298
+ * STAGE V / STEP 5.2: SILENCE DISCIPLINE — SUPPRESSION HELPERS
299
+ * ═══════════════════════════════════════════════════════════════════
300
+ *
301
+ * Centralized boolean helpers to enforce strict suppression rules.
302
+ * Guardian speaks ONLY when there is clear, meaningful value.
303
+ * Silence is the default state. Output is an exception.
304
+ */
305
+
306
+ /**
307
+ * Should render Focus Summary?
308
+ * Suppress when: verdict === READY + confidence === high + no patterns
309
+ */
310
+ function shouldRenderFocusSummary(verdict, patterns) {
311
+ if (!verdict) return false;
312
+
313
+ // Handle null/undefined patterns: safer to show when uncertain
314
+ if (patterns === null || patterns === undefined) {
315
+ return true;
316
+ }
317
+
318
+ const vStatus = verdict.verdict || 'UNKNOWN';
319
+ const cfLevel = (verdict.confidence || {}).level || 'n/a';
320
+
321
+ // Suppress when READY + high confidence + no patterns
322
+ if (vStatus === 'READY' && cfLevel === 'high' && patterns.length === 0) {
323
+ return false;
324
+ }
325
+
326
+ return true;
327
+ }
328
+
329
+ /**
330
+ * Should render Delta Insight?
331
+ * Suppress when: no improved and no regressed lines
332
+ */
333
+ function shouldRenderDeltaInsight(delta) {
334
+ if (!delta) return false;
335
+ return (delta.improved && delta.improved.length > 0) || (delta.regressed && delta.regressed.length > 0);
336
+ }
337
+
338
+ /**
339
+ * Should render Observed Patterns?
340
+ * Suppress when: no patterns detected
341
+ */
342
+ function shouldRenderPatterns(patterns) {
343
+ // Handle null/undefined patterns
344
+ if (!patterns || !Array.isArray(patterns)) {
345
+ return false;
346
+ }
347
+ return patterns.length > 0;
348
+ }
349
+
350
+ /**
351
+ * Should render Confidence Drivers?
352
+ * Suppress when: confidence === high AND runIndex >= 3
353
+ */
354
+ function shouldRenderConfidenceDrivers(verdict, runIndex = 0) {
355
+ if (!verdict) return false;
356
+
357
+ const cfLevel = (verdict.confidence || {}).level || 'n/a';
358
+
359
+ // Canonical rule: show unless confidence is high on runIndex >= 2
360
+ if (cfLevel === 'high' && runIndex >= 2) {
361
+ return false;
362
+ }
363
+
364
+ return true;
365
+ }
366
+
367
+ /**
368
+ * Should render Three-Runs Journey messaging?
369
+ * Suppress when: runIndex >= 3
370
+ */
371
+ function shouldRenderJourneyMessage(runIndex = 0) {
372
+ return runIndex < 2;
373
+ }
374
+
375
+ /**
376
+ * Should render Next-Run Hint?
377
+ * Suppress when: verdict === READY OR no gaps/limits exist
378
+ */
379
+ function shouldRenderNextRunHint(verdict) {
380
+ if (!verdict) return false;
381
+
382
+ const vStatus = verdict.verdict || 'UNKNOWN';
383
+
384
+ // Suppress when READY
385
+ if (vStatus === 'READY') {
386
+ return false;
387
+ }
388
+
389
+ // Show when not READY (hints may be valuable)
390
+ return true;
391
+ }
392
+
393
+ /**
394
+ * Should render First-Run Note?
395
+ * Suppress when: runIndex >= 2
396
+ */
397
+ function shouldRenderFirstRunNote(runIndex = 0) {
398
+ return runIndex < 2;
399
+ }
400
+
401
+ module.exports = {
402
+ formatVerdictStatus,
403
+ formatConfidence,
404
+ formatVerdictWhy,
405
+ formatKeyFindings,
406
+ formatLimits,
407
+ formatPatternSummary,
408
+ formatPatternWhy,
409
+ formatPatternFocus,
410
+ formatPatternLimits,
411
+ formatConfidenceMicroLine,
412
+ formatFirstRunNote,
413
+ formatJourneyMessage,
414
+ formatNextRunHint,
415
+ formatConfidenceDrivers,
416
+ formatFocusSummary,
417
+ formatDeltaInsight,
418
+ // Stage V / Step 5.2: Silence Discipline helpers
419
+ shouldRenderFocusSummary,
420
+ shouldRenderDeltaInsight,
421
+ shouldRenderPatterns,
422
+ shouldRenderConfidenceDrivers,
423
+ shouldRenderJourneyMessage,
424
+ shouldRenderNextRunHint,
425
+ shouldRenderFirstRunNote
426
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Guardian Timeout Profiles
3
+ * Defines deterministic timeout values for different performance modes
4
+ */
5
+
6
+ const TIMEOUT_PROFILES = {
7
+ fast: {
8
+ // Fast mode: aggressive timeouts for quick feedback
9
+ pageLoad: 8000, // page navigation
10
+ elementWait: 3000, // finding elements
11
+ actionWait: 2000, // click/type settlement (used by wait-for-outcome)
12
+ submitSettle: 2500, // form submission settlement
13
+ networkWait: 1500, // network response wait
14
+ default: 8000
15
+ },
16
+ default: {
17
+ // Default mode: current behavior (balanced)
18
+ pageLoad: 20000,
19
+ elementWait: 5000,
20
+ actionWait: 3500, // matches DEFAULT_MAX_WAIT in wait-for-outcome
21
+ submitSettle: 4000,
22
+ networkWait: 3500,
23
+ default: 20000
24
+ },
25
+ slow: {
26
+ // Slow mode: patient timeouts for flaky networks
27
+ pageLoad: 30000,
28
+ elementWait: 10000,
29
+ actionWait: 5000,
30
+ submitSettle: 6000,
31
+ networkWait: 5000,
32
+ default: 30000
33
+ }
34
+ };
35
+
36
+ function getTimeoutProfile(profileName = 'default') {
37
+ const profile = TIMEOUT_PROFILES[profileName];
38
+ if (!profile) {
39
+ throw new Error(`Invalid timeout profile: ${profileName}. Valid values: ${Object.keys(TIMEOUT_PROFILES).join(', ')}`);
40
+ }
41
+ return profile;
42
+ }
43
+
44
+ function resolveTimeout(configValue, profile) {
45
+ // If config explicitly sets a timeout, use it (allows override)
46
+ if (configValue && typeof configValue === 'number') {
47
+ return configValue;
48
+ }
49
+ // Otherwise use profile default
50
+ return profile.default;
51
+ }
52
+
53
+ module.exports = {
54
+ TIMEOUT_PROFILES,
55
+ getTimeoutProfile,
56
+ resolveTimeout
57
+ };