@yasserkhanorg/e2e-agents 0.3.2

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 (221) hide show
  1. package/LICENSE +168 -0
  2. package/README.md +620 -0
  3. package/dist/agent/analysis.d.ts +62 -0
  4. package/dist/agent/analysis.d.ts.map +1 -0
  5. package/dist/agent/analysis.js +292 -0
  6. package/dist/agent/blast_radius.d.ts +4 -0
  7. package/dist/agent/blast_radius.d.ts.map +1 -0
  8. package/dist/agent/blast_radius.js +37 -0
  9. package/dist/agent/cache_utils.d.ts +38 -0
  10. package/dist/agent/cache_utils.d.ts.map +1 -0
  11. package/dist/agent/cache_utils.js +67 -0
  12. package/dist/agent/config.d.ts +148 -0
  13. package/dist/agent/config.d.ts.map +1 -0
  14. package/dist/agent/config.js +640 -0
  15. package/dist/agent/dependency_graph.d.ts +14 -0
  16. package/dist/agent/dependency_graph.d.ts.map +1 -0
  17. package/dist/agent/dependency_graph.js +227 -0
  18. package/dist/agent/feedback.d.ts +55 -0
  19. package/dist/agent/feedback.d.ts.map +1 -0
  20. package/dist/agent/feedback.js +257 -0
  21. package/dist/agent/flags.d.ts +23 -0
  22. package/dist/agent/flags.d.ts.map +1 -0
  23. package/dist/agent/flags.js +171 -0
  24. package/dist/agent/flow_catalog.d.ts +25 -0
  25. package/dist/agent/flow_catalog.d.ts.map +1 -0
  26. package/dist/agent/flow_catalog.js +106 -0
  27. package/dist/agent/flow_mapping.d.ts +10 -0
  28. package/dist/agent/flow_mapping.d.ts.map +1 -0
  29. package/dist/agent/flow_mapping.js +84 -0
  30. package/dist/agent/framework.d.ts +13 -0
  31. package/dist/agent/framework.d.ts.map +1 -0
  32. package/dist/agent/framework.js +149 -0
  33. package/dist/agent/gap_suggestions.d.ts +14 -0
  34. package/dist/agent/gap_suggestions.d.ts.map +1 -0
  35. package/dist/agent/gap_suggestions.js +101 -0
  36. package/dist/agent/generator.d.ts +10 -0
  37. package/dist/agent/generator.d.ts.map +1 -0
  38. package/dist/agent/generator.js +115 -0
  39. package/dist/agent/git.d.ts +11 -0
  40. package/dist/agent/git.d.ts.map +1 -0
  41. package/dist/agent/git.js +90 -0
  42. package/dist/agent/handoff.d.ts +22 -0
  43. package/dist/agent/handoff.d.ts.map +1 -0
  44. package/dist/agent/handoff.js +180 -0
  45. package/dist/agent/impact-analyzer.d.ts +114 -0
  46. package/dist/agent/impact-analyzer.d.ts.map +1 -0
  47. package/dist/agent/impact-analyzer.js +557 -0
  48. package/dist/agent/index.d.ts +21 -0
  49. package/dist/agent/index.d.ts.map +1 -0
  50. package/dist/agent/index.js +38 -0
  51. package/dist/agent/model-router.d.ts +57 -0
  52. package/dist/agent/model-router.d.ts.map +1 -0
  53. package/dist/agent/model-router.js +154 -0
  54. package/dist/agent/operational_insights.d.ts +41 -0
  55. package/dist/agent/operational_insights.d.ts.map +1 -0
  56. package/dist/agent/operational_insights.js +126 -0
  57. package/dist/agent/pipeline.d.ts +23 -0
  58. package/dist/agent/pipeline.d.ts.map +1 -0
  59. package/dist/agent/pipeline.js +609 -0
  60. package/dist/agent/plan.d.ts +91 -0
  61. package/dist/agent/plan.d.ts.map +1 -0
  62. package/dist/agent/plan.js +331 -0
  63. package/dist/agent/playwright_report.d.ts +8 -0
  64. package/dist/agent/playwright_report.d.ts.map +1 -0
  65. package/dist/agent/playwright_report.js +126 -0
  66. package/dist/agent/report-generator.d.ts +24 -0
  67. package/dist/agent/report-generator.d.ts.map +1 -0
  68. package/dist/agent/report-generator.js +250 -0
  69. package/dist/agent/report.d.ts +81 -0
  70. package/dist/agent/report.d.ts.map +1 -0
  71. package/dist/agent/report.js +147 -0
  72. package/dist/agent/runner.d.ts +7 -0
  73. package/dist/agent/runner.d.ts.map +1 -0
  74. package/dist/agent/runner.js +576 -0
  75. package/dist/agent/selectors.d.ts +10 -0
  76. package/dist/agent/selectors.d.ts.map +1 -0
  77. package/dist/agent/selectors.js +75 -0
  78. package/dist/agent/spec-bridge.d.ts +101 -0
  79. package/dist/agent/spec-bridge.d.ts.map +1 -0
  80. package/dist/agent/spec-bridge.js +273 -0
  81. package/dist/agent/spec-builder.d.ts +102 -0
  82. package/dist/agent/spec-builder.d.ts.map +1 -0
  83. package/dist/agent/spec-builder.js +273 -0
  84. package/dist/agent/subsystem_risk.d.ts +23 -0
  85. package/dist/agent/subsystem_risk.d.ts.map +1 -0
  86. package/dist/agent/subsystem_risk.js +207 -0
  87. package/dist/agent/telemetry.d.ts +84 -0
  88. package/dist/agent/telemetry.d.ts.map +1 -0
  89. package/dist/agent/telemetry.js +220 -0
  90. package/dist/agent/test_path.d.ts +2 -0
  91. package/dist/agent/test_path.d.ts.map +1 -0
  92. package/dist/agent/test_path.js +23 -0
  93. package/dist/agent/tests.d.ts +18 -0
  94. package/dist/agent/tests.d.ts.map +1 -0
  95. package/dist/agent/tests.js +106 -0
  96. package/dist/agent/traceability.d.ts +22 -0
  97. package/dist/agent/traceability.d.ts.map +1 -0
  98. package/dist/agent/traceability.js +183 -0
  99. package/dist/agent/traceability_capture.d.ts +18 -0
  100. package/dist/agent/traceability_capture.d.ts.map +1 -0
  101. package/dist/agent/traceability_capture.js +313 -0
  102. package/dist/agent/traceability_ingest.d.ts +21 -0
  103. package/dist/agent/traceability_ingest.d.ts.map +1 -0
  104. package/dist/agent/traceability_ingest.js +237 -0
  105. package/dist/agent/utils.d.ts +13 -0
  106. package/dist/agent/utils.d.ts.map +1 -0
  107. package/dist/agent/utils.js +152 -0
  108. package/dist/agent/validators/selector-validator.d.ts +74 -0
  109. package/dist/agent/validators/selector-validator.d.ts.map +1 -0
  110. package/dist/agent/validators/selector-validator.js +165 -0
  111. package/dist/anthropic_provider.d.ts +65 -0
  112. package/dist/anthropic_provider.d.ts.map +1 -0
  113. package/dist/anthropic_provider.js +332 -0
  114. package/dist/api.d.ts +48 -0
  115. package/dist/api.d.ts.map +1 -0
  116. package/dist/api.js +113 -0
  117. package/dist/base_provider.d.ts +53 -0
  118. package/dist/base_provider.d.ts.map +1 -0
  119. package/dist/base_provider.js +81 -0
  120. package/dist/cli.d.ts +3 -0
  121. package/dist/cli.d.ts.map +1 -0
  122. package/dist/cli.js +843 -0
  123. package/dist/custom_provider.d.ts +20 -0
  124. package/dist/custom_provider.d.ts.map +1 -0
  125. package/dist/custom_provider.js +276 -0
  126. package/dist/e2e-test-gen/index.d.ts +51 -0
  127. package/dist/e2e-test-gen/index.d.ts.map +1 -0
  128. package/dist/e2e-test-gen/index.js +57 -0
  129. package/dist/e2e-test-gen/spec_parser.d.ts +142 -0
  130. package/dist/e2e-test-gen/spec_parser.d.ts.map +1 -0
  131. package/dist/e2e-test-gen/spec_parser.js +786 -0
  132. package/dist/e2e-test-gen/types.d.ts +185 -0
  133. package/dist/e2e-test-gen/types.d.ts.map +1 -0
  134. package/dist/e2e-test-gen/types.js +4 -0
  135. package/dist/esm/agent/analysis.js +287 -0
  136. package/dist/esm/agent/blast_radius.js +34 -0
  137. package/dist/esm/agent/cache_utils.js +63 -0
  138. package/dist/esm/agent/config.js +637 -0
  139. package/dist/esm/agent/dependency_graph.js +224 -0
  140. package/dist/esm/agent/feedback.js +253 -0
  141. package/dist/esm/agent/flags.js +160 -0
  142. package/dist/esm/agent/flow_catalog.js +103 -0
  143. package/dist/esm/agent/flow_mapping.js +81 -0
  144. package/dist/esm/agent/framework.js +145 -0
  145. package/dist/esm/agent/gap_suggestions.js +98 -0
  146. package/dist/esm/agent/generator.js +112 -0
  147. package/dist/esm/agent/git.js +87 -0
  148. package/dist/esm/agent/handoff.js +177 -0
  149. package/dist/esm/agent/impact-analyzer.js +548 -0
  150. package/dist/esm/agent/index.js +22 -0
  151. package/dist/esm/agent/model-router.js +150 -0
  152. package/dist/esm/agent/operational_insights.js +123 -0
  153. package/dist/esm/agent/pipeline.js +605 -0
  154. package/dist/esm/agent/plan.js +324 -0
  155. package/dist/esm/agent/playwright_report.js +123 -0
  156. package/dist/esm/agent/report-generator.js +247 -0
  157. package/dist/esm/agent/report.js +144 -0
  158. package/dist/esm/agent/runner.js +572 -0
  159. package/dist/esm/agent/selectors.js +71 -0
  160. package/dist/esm/agent/spec-bridge.js +267 -0
  161. package/dist/esm/agent/spec-builder.js +267 -0
  162. package/dist/esm/agent/subsystem_risk.js +204 -0
  163. package/dist/esm/agent/telemetry.js +216 -0
  164. package/dist/esm/agent/test_path.js +20 -0
  165. package/dist/esm/agent/tests.js +101 -0
  166. package/dist/esm/agent/traceability.js +180 -0
  167. package/dist/esm/agent/traceability_capture.js +310 -0
  168. package/dist/esm/agent/traceability_ingest.js +234 -0
  169. package/dist/esm/agent/utils.js +138 -0
  170. package/dist/esm/agent/validators/selector-validator.js +160 -0
  171. package/dist/esm/anthropic_provider.js +324 -0
  172. package/dist/esm/api.js +105 -0
  173. package/dist/esm/base_provider.js +77 -0
  174. package/dist/esm/cli.js +841 -0
  175. package/dist/esm/custom_provider.js +272 -0
  176. package/dist/esm/e2e-test-gen/index.js +50 -0
  177. package/dist/esm/e2e-test-gen/spec_parser.js +782 -0
  178. package/dist/esm/e2e-test-gen/types.js +3 -0
  179. package/dist/esm/index.js +16 -0
  180. package/dist/esm/logger.js +89 -0
  181. package/dist/esm/mcp-server.js +465 -0
  182. package/dist/esm/ollama_provider.js +300 -0
  183. package/dist/esm/openai_provider.js +242 -0
  184. package/dist/esm/package.json +3 -0
  185. package/dist/esm/plan-and-test-constants.js +126 -0
  186. package/dist/esm/provider_factory.js +336 -0
  187. package/dist/esm/provider_interface.js +23 -0
  188. package/dist/esm/provider_utils.js +96 -0
  189. package/dist/index.d.ts +31 -0
  190. package/dist/index.d.ts.map +1 -0
  191. package/dist/index.js +41 -0
  192. package/dist/logger.d.ts +23 -0
  193. package/dist/logger.d.ts.map +1 -0
  194. package/dist/logger.js +93 -0
  195. package/dist/mcp-server.d.ts +35 -0
  196. package/dist/mcp-server.d.ts.map +1 -0
  197. package/dist/mcp-server.js +469 -0
  198. package/dist/ollama_provider.d.ts +65 -0
  199. package/dist/ollama_provider.d.ts.map +1 -0
  200. package/dist/ollama_provider.js +308 -0
  201. package/dist/openai_provider.d.ts +23 -0
  202. package/dist/openai_provider.d.ts.map +1 -0
  203. package/dist/openai_provider.js +250 -0
  204. package/dist/plan-and-test-constants.d.ts +110 -0
  205. package/dist/plan-and-test-constants.d.ts.map +1 -0
  206. package/dist/plan-and-test-constants.js +132 -0
  207. package/dist/provider_factory.d.ts +99 -0
  208. package/dist/provider_factory.d.ts.map +1 -0
  209. package/dist/provider_factory.js +341 -0
  210. package/dist/provider_interface.d.ts +358 -0
  211. package/dist/provider_interface.d.ts.map +1 -0
  212. package/dist/provider_interface.js +28 -0
  213. package/dist/provider_utils.d.ts +39 -0
  214. package/dist/provider_utils.d.ts.map +1 -0
  215. package/dist/provider_utils.js +103 -0
  216. package/package.json +101 -0
  217. package/schemas/gap.schema.json +18 -0
  218. package/schemas/impact.schema.json +418 -0
  219. package/schemas/plan.schema.json +285 -0
  220. package/schemas/subsystem-risk-map.schema.json +62 -0
  221. package/schemas/traceability-input.schema.json +122 -0
@@ -0,0 +1,150 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ const MODEL_RATES = {
4
+ 'claude-haiku-4-0-20250430': 0.25 / 1000000,
5
+ 'claude-sonnet-4-5-20250929': 3 / 1000000,
6
+ 'claude-opus-4-6-20250820': 15 / 1000000,
7
+ };
8
+ export class ModelRouter {
9
+ constructor(config = {}) {
10
+ this.modelConfig = {
11
+ simpleModel: config.simpleModel || 'claude-haiku-4-0-20250430',
12
+ moderateModel: config.moderateModel || 'claude-sonnet-4-5-20250929',
13
+ complexModel: config.complexModel || 'claude-opus-4-6-20250820',
14
+ criticalModel: config.criticalModel || 'claude-opus-4-6-20250820',
15
+ };
16
+ }
17
+ /**
18
+ * Classify task complexity based on operation, attempt number, and context
19
+ */
20
+ classifyTask(context) {
21
+ // Healing: Haiku for attempts 1-2, Sonnet for attempt 3
22
+ if (context.operation === 'heal') {
23
+ if (context.attemptNumber && context.attemptNumber <= 2) {
24
+ // First two healing attempts use simple classification
25
+ return {
26
+ type: 'simple',
27
+ confidence: 95,
28
+ reasoning: `Healing attempt ${context.attemptNumber}/3 - re-exploration with targeted fixes`,
29
+ };
30
+ }
31
+ // Final healing attempt is more complex
32
+ return {
33
+ type: 'moderate',
34
+ confidence: 90,
35
+ reasoning: 'Final healing attempt - may need comprehensive refactoring',
36
+ };
37
+ }
38
+ // Validation: Always simple (use Haiku)
39
+ if (context.operation === 'validate') {
40
+ return {
41
+ type: 'simple',
42
+ confidence: 100,
43
+ reasoning: 'Selector/code validation is lightweight',
44
+ };
45
+ }
46
+ // Scoring: Simple (quick analysis)
47
+ if (context.operation === 'score') {
48
+ return {
49
+ type: 'simple',
50
+ confidence: 95,
51
+ reasoning: 'Test quality scoring via static analysis',
52
+ };
53
+ }
54
+ // Exploration: Simple (just navigation and snapshot)
55
+ if (context.operation === 'explore') {
56
+ return {
57
+ type: 'simple',
58
+ confidence: 90,
59
+ reasoning: 'UI exploration is mostly navigation',
60
+ };
61
+ }
62
+ // Generation: Varies by UI map coverage
63
+ if (context.operation === 'generate') {
64
+ // Strong signal: Use Sonnet but can optimize
65
+ if (context.uiMapCoverage && context.uiMapCoverage >= 75) {
66
+ return {
67
+ type: 'moderate',
68
+ confidence: 90,
69
+ reasoning: `Strong UI signal (${context.uiMapCoverage}% coverage) - moderate complexity generation`,
70
+ };
71
+ }
72
+ // Moderate signal: Use Sonnet
73
+ if (context.uiMapCoverage && context.uiMapCoverage >= 50) {
74
+ return {
75
+ type: 'moderate',
76
+ confidence: 75,
77
+ reasoning: `Moderate UI signal (${context.uiMapCoverage}% coverage) - standard generation`,
78
+ };
79
+ }
80
+ // Weak signal: Complex task (need better reasoning)
81
+ if (context.uiMapCoverage && context.uiMapCoverage < 50) {
82
+ return {
83
+ type: 'complex',
84
+ confidence: 70,
85
+ reasoning: `Weak UI signal (${context.uiMapCoverage}% coverage) - requires advanced reasoning`,
86
+ };
87
+ }
88
+ // Unknown coverage: Assume moderate
89
+ return {
90
+ type: 'moderate',
91
+ confidence: 50,
92
+ reasoning: 'Generation with unknown UI coverage - use standard model',
93
+ };
94
+ }
95
+ // Default to moderate
96
+ return {
97
+ type: 'moderate',
98
+ confidence: 50,
99
+ reasoning: 'Unknown operation - defaulting to moderate complexity',
100
+ };
101
+ }
102
+ /**
103
+ * Select appropriate model based on task complexity
104
+ */
105
+ selectModel(complexity) {
106
+ switch (complexity.type) {
107
+ case 'simple':
108
+ return this.modelConfig.simpleModel;
109
+ case 'moderate':
110
+ return this.modelConfig.moderateModel;
111
+ case 'complex':
112
+ return this.modelConfig.complexModel;
113
+ case 'critical':
114
+ return this.modelConfig.criticalModel;
115
+ }
116
+ }
117
+ /**
118
+ * Get estimated cost for a task
119
+ */
120
+ estimateCost(complexity, estimatedTokens = 5000) {
121
+ const model = this.selectModel(complexity);
122
+ const rate = MODEL_RATES[model] || 0.003 / 1000000;
123
+ return estimatedTokens * rate;
124
+ }
125
+ /**
126
+ * Get cost savings vs always using Sonnet
127
+ */
128
+ estimateSavings(complexity, estimatedTokens = 5000) {
129
+ const selectedModel = this.selectModel(complexity);
130
+ const selectedRate = MODEL_RATES[selectedModel] || 0.003 / 1000000;
131
+ const sonnetRate = MODEL_RATES['claude-sonnet-4-5-20250929'];
132
+ const selectedCost = estimatedTokens * selectedRate;
133
+ const sonnetCost = estimatedTokens * sonnetRate;
134
+ const savedCost = sonnetCost - selectedCost;
135
+ const savingsPercent = (savedCost / sonnetCost) * 100;
136
+ return { savedCost, savingsPercent };
137
+ }
138
+ /**
139
+ * Format complexity for logging
140
+ */
141
+ formatComplexity(complexity, tokensUsed) {
142
+ const model = this.selectModel(complexity);
143
+ const modelShort = model.includes('haiku') ? 'Haiku' : model.includes('sonnet') ? 'Sonnet' : 'Opus';
144
+ const confidence = `${complexity.confidence}%`;
145
+ const cost = tokensUsed
146
+ ? ` ($${(tokensUsed * (MODEL_RATES[model] || 0)).toFixed(4)})`
147
+ : '';
148
+ return `${complexity.type.toUpperCase()}/${modelShort}/${confidence}${cost} - ${complexity.reasoning}`;
149
+ }
150
+ }
@@ -0,0 +1,123 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { inferSubsystemFromTestPath } from './test_path.js';
6
+ function readJson(path) {
7
+ if (!existsSync(path)) {
8
+ return null;
9
+ }
10
+ try {
11
+ return JSON.parse(readFileSync(path, 'utf-8'));
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ function normalizeTestName(test) {
18
+ return test.replace(/ \(flags:.*\)$/, '').trim();
19
+ }
20
+ function subsystemForTest(test) {
21
+ return inferSubsystemFromTestPath(test);
22
+ }
23
+ function riskyRate(entry) {
24
+ if (entry.flakeRate30d !== undefined) {
25
+ return entry.flakeRate30d;
26
+ }
27
+ return entry.flakeRate;
28
+ }
29
+ function loadFlakyManifest(appRoot) {
30
+ const path = join(appRoot, '.e2e-ai-agents', 'flaky-tests.json');
31
+ return readJson(path);
32
+ }
33
+ function loadQualityGates(appRoot) {
34
+ const path = join(appRoot, '.e2e-ai-agents', 'quality-gates.json');
35
+ return readJson(path);
36
+ }
37
+ function loadCalibration(appRoot) {
38
+ const path = join(appRoot, '.e2e-ai-agents', 'calibration.json');
39
+ return readJson(path);
40
+ }
41
+ export function applyOperationalInsights(plan, appRoot) {
42
+ const enhanced = { ...plan };
43
+ const insights = {};
44
+ const flaky = loadFlakyManifest(appRoot);
45
+ if (flaky && Array.isArray(flaky.tests)) {
46
+ const recommended = new Set(plan.recommendedTests.map(normalizeTestName));
47
+ const risky = flaky.tests
48
+ .filter((entry) => recommended.has(normalizeTestName(entry.test)) && riskyRate(entry) >= 0.2)
49
+ .sort((a, b) => riskyRate(b) - riskyRate(a))
50
+ .slice(0, 10);
51
+ const quarantined = risky.filter((entry) => entry.quarantine).map((entry) => entry.test);
52
+ const owners = Array.from(new Set(risky
53
+ .flatMap((entry) => entry.owners || [])
54
+ .filter(Boolean)));
55
+ insights.flaky = {
56
+ highRiskRecommendedTests: risky,
57
+ quarantinedRecommendedTests: quarantined,
58
+ ownerMentions: owners,
59
+ };
60
+ if (quarantined.length > 0) {
61
+ enhanced.reasons = [...enhanced.reasons, `Quarantined flaky tests in recommendation: ${quarantined.join(', ')}`];
62
+ }
63
+ if (owners.length > 0) {
64
+ enhanced.reasons = [...enhanced.reasons, `Subsystem owners to notify for flaky risk: ${owners.join(', ')}`];
65
+ }
66
+ }
67
+ const gates = loadQualityGates(appRoot);
68
+ if (gates && Array.isArray(gates.gates)) {
69
+ const failed = gates.gates.filter((gate) => gate.status === 'fail');
70
+ const warnings = gates.gates.filter((gate) => gate.status === 'warn');
71
+ insights.qualityGates = { failed, warnings };
72
+ if (failed.length > 0 && enhanced.runSet !== 'full') {
73
+ enhanced.runSet = 'full';
74
+ enhanced.reasons = [...enhanced.reasons, `Quality gates failed: ${failed.map((gate) => gate.name).join(', ')}`];
75
+ enhanced.policy.triggeredRules = [...new Set([...enhanced.policy.triggeredRules, 'quality-gate-failed'])];
76
+ enhanced.decision = {
77
+ action: 'run-now',
78
+ title: 'Run now',
79
+ summary: 'Quality gate failures detected. Full suite is required before merge.',
80
+ };
81
+ }
82
+ }
83
+ const calibration = loadCalibration(appRoot);
84
+ if (calibration) {
85
+ insights.calibration = calibration.overall;
86
+ if (calibration.overall.falseNegativeRate >= 0.2 && enhanced.runSet !== 'full') {
87
+ enhanced.runSet = 'full';
88
+ enhanced.reasons = [...enhanced.reasons, 'Historical false-negative rate is high; escalating to full suite.'];
89
+ enhanced.policy.triggeredRules = [...new Set([...enhanced.policy.triggeredRules, 'historical-fnr-high'])];
90
+ }
91
+ const recommendedSubsystems = Array.from(new Set(plan.recommendedTests.map(subsystemForTest)));
92
+ const highRiskSubsystems = recommendedSubsystems
93
+ .map((subsystem) => {
94
+ const metric = calibration.bySubsystem[subsystem];
95
+ if (!metric) {
96
+ return null;
97
+ }
98
+ if (metric.samples < 5) {
99
+ return null;
100
+ }
101
+ if (metric.recent30d.falseNegativeRate >= 0.2 || metric.falseNegativeRate >= 0.25) {
102
+ return { subsystem, fnr: metric.recent30d.falseNegativeRate || metric.falseNegativeRate };
103
+ }
104
+ return null;
105
+ })
106
+ .filter(Boolean);
107
+ if (highRiskSubsystems.length > 0 && enhanced.runSet !== 'full') {
108
+ enhanced.runSet = 'full';
109
+ enhanced.reasons = [
110
+ ...enhanced.reasons,
111
+ `Historical subsystem false-negative risk is high: ${highRiskSubsystems.map((entry) => `${entry.subsystem}(${entry.fnr})`).join(', ')}`,
112
+ ];
113
+ enhanced.policy.triggeredRules = [...new Set([...enhanced.policy.triggeredRules, 'subsystem-fnr-high'])];
114
+ enhanced.decision = {
115
+ action: 'run-now',
116
+ title: 'Run now',
117
+ summary: 'Subsystem calibration risk is high. Full suite is required before merge.',
118
+ };
119
+ }
120
+ }
121
+ enhanced.insights = insights;
122
+ return enhanced;
123
+ }