@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,185 @@
1
+ /**
2
+ * Core types for the autonomous testing system
3
+ *
4
+ * This file contains the essential types for specification parsing
5
+ * and the spec-to-Playwright bridge. The heavy lifting (test generation,
6
+ * execution, healing) is delegated to Playwright's native agents.
7
+ */
8
+ /**
9
+ * Feature Specification - parsed from specification document (PDF, MD, JSON)
10
+ *
11
+ * This is the primary data structure extracted from user specifications.
12
+ * It represents a feature to be tested with its scenarios and acceptance criteria.
13
+ */
14
+ export interface FeatureSpecification {
15
+ /**
16
+ * Unique identifier (derived from feature name)
17
+ */
18
+ id: string;
19
+ /**
20
+ * Feature name
21
+ */
22
+ name: string;
23
+ /**
24
+ * Feature description
25
+ */
26
+ description: string;
27
+ /**
28
+ * Priority level
29
+ */
30
+ priority: 'critical' | 'high' | 'medium' | 'low';
31
+ /**
32
+ * Target URLs to prioritize (optional - PDF specs describe behavior, not routes)
33
+ */
34
+ targetUrls: string[];
35
+ /**
36
+ * Business scenarios to test (Given-When-Then format)
37
+ */
38
+ scenarios: BusinessScenario[];
39
+ /**
40
+ * Reference screenshots from spec
41
+ */
42
+ screenshots: SpecScreenshot[];
43
+ /**
44
+ * Acceptance criteria (testable statements)
45
+ */
46
+ acceptanceCriteria: string[];
47
+ /**
48
+ * Related feature IDs
49
+ */
50
+ relatedFeatures?: string[];
51
+ /**
52
+ * Source file path
53
+ */
54
+ sourcePath: string;
55
+ /**
56
+ * Hash of source file (for cache validation)
57
+ */
58
+ sourceHash?: string;
59
+ /**
60
+ * Additional metadata (for PDFs and other formats)
61
+ */
62
+ metadata?: {
63
+ /**
64
+ * UI elements mentioned in the spec
65
+ */
66
+ uiElements?: string[];
67
+ /**
68
+ * User flows described in the spec
69
+ */
70
+ userFlows?: string[];
71
+ /**
72
+ * Source format (pdf, markdown, json, etc.)
73
+ */
74
+ extractedFrom?: string;
75
+ /**
76
+ * Any other custom metadata
77
+ */
78
+ [key: string]: any;
79
+ };
80
+ }
81
+ /**
82
+ * Business scenario from specification (Given-When-Then format)
83
+ *
84
+ * This format is widely used in BDD (Behavior-Driven Development)
85
+ * and maps well to Playwright test structure.
86
+ */
87
+ export interface BusinessScenario {
88
+ /**
89
+ * Scenario name (describes the test case)
90
+ */
91
+ name: string;
92
+ /**
93
+ * Given (precondition) - the initial state
94
+ */
95
+ given: string;
96
+ /**
97
+ * When (action) - the user action being tested
98
+ */
99
+ when: string;
100
+ /**
101
+ * Then (expected outcome) - the expected result
102
+ */
103
+ then: string;
104
+ /**
105
+ * Scenario priority (for test ordering)
106
+ */
107
+ priority: 'must-have' | 'should-have' | 'nice-to-have';
108
+ /**
109
+ * Test IDs that cover this scenario (populated after test generation)
110
+ */
111
+ coveredByTests?: string[];
112
+ }
113
+ /**
114
+ * Specification screenshot (for visual reference)
115
+ */
116
+ export interface SpecScreenshot {
117
+ /**
118
+ * File path or URL
119
+ */
120
+ path: string;
121
+ /**
122
+ * Description of what the screenshot shows
123
+ */
124
+ description: string;
125
+ /**
126
+ * Page number (for PDF screenshots)
127
+ */
128
+ pageNumber?: number;
129
+ /**
130
+ * Base64-encoded image data (loaded from path)
131
+ */
132
+ data?: string;
133
+ }
134
+ /**
135
+ * Generated test metadata
136
+ *
137
+ * Tracks information about tests generated from specifications.
138
+ * The actual test code is generated by Playwright's agents.
139
+ */
140
+ export interface GeneratedTest {
141
+ /**
142
+ * Unique test ID
143
+ */
144
+ id: string;
145
+ /**
146
+ * Test title
147
+ */
148
+ title: string;
149
+ /**
150
+ * Test file path
151
+ */
152
+ filePath: string;
153
+ /**
154
+ * Test code (Playwright test)
155
+ */
156
+ code: string;
157
+ /**
158
+ * Source UI states that led to this test
159
+ */
160
+ sourceStates: string[];
161
+ /**
162
+ * Related specification (if spec-driven)
163
+ */
164
+ relatedSpec?: {
165
+ specId: string;
166
+ scenarioName: string;
167
+ };
168
+ /**
169
+ * AI confidence score (0.0 - 1.0)
170
+ */
171
+ confidence: number;
172
+ /**
173
+ * Test tags
174
+ */
175
+ tags: string[];
176
+ /**
177
+ * When test was generated
178
+ */
179
+ generatedAt: Date;
180
+ /**
181
+ * Generation method
182
+ */
183
+ generatedBy: 'playwright-planner' | 'playwright-generator' | 'manual' | 'autonomous-system' | 'spec-driven-generator';
184
+ }
185
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/e2e-test-gen/types.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AAMH;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACjC;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IAEjD;;OAEG;IACH,UAAU,EAAE,MAAM,EAAE,CAAC;IAErB;;OAEG;IACH,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAE9B;;OAEG;IACH,WAAW,EAAE,cAAc,EAAE,CAAC;IAE9B;;OAEG;IACH,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAE7B;;OAEG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAE3B;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,QAAQ,CAAC,EAAE;QACP;;WAEG;QACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QAEtB;;WAEG;QACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QAErB;;WAEG;QACH,aAAa,CAAC,EAAE,MAAM,CAAC;QAEvB;;WAEG;QACH,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACtB,CAAC;CACL;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC7B;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,QAAQ,EAAE,WAAW,GAAG,aAAa,GAAG,cAAc,CAAC;IAEvD;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC3B;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAMD;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC1B;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,YAAY,EAAE,MAAM,EAAE,CAAC;IAEvB;;OAEG;IACH,WAAW,CAAC,EAAE;QACV,MAAM,EAAE,MAAM,CAAC;QACf,YAAY,EAAE,MAAM,CAAC;KACxB,CAAC;IAEF;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,IAAI,EAAE,MAAM,EAAE,CAAC;IAEf;;OAEG;IACH,WAAW,EAAE,IAAI,CAAC;IAElB;;OAEG;IACH,WAAW,EACL,oBAAoB,GACpB,sBAAsB,GACtB,QAAQ,GACR,mBAAmB,GACnB,uBAAuB,CAAC;CACjC"}
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
3
+ // See LICENSE.txt for license information.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,287 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { globSync } from 'glob';
6
+ import { extractFlagHits, inferAudienceFromPath, mergeFlags, normalizeRoles } from './flags.js';
7
+ import { baseNameWithoutExt, fileExtension, normalizePath, safeReadTextFile, titleCase, tokenize, uniqueTokens, } from './utils.js';
8
+ import { loadSubsystemRiskResolver } from './subsystem_risk.js';
9
+ const TEST_PATH_PATTERN = /(^|\/)__tests__(\/|$)|(^|\/)tests?(\/|$)|\.(spec|test)\.[a-z0-9]+$/i;
10
+ const SCREEN_DIRS = new Set(['pages', 'screens', 'views', 'routes']);
11
+ const FEATURE_DIRS = new Set(['features', 'modules', 'flows']);
12
+ const COMPONENT_DIRS = new Set(['components', 'widgets', 'ui']);
13
+ const STATE_DIRS = new Set(['state', 'store', 'stores', 'reducers', 'actions', 'context', 'hooks']);
14
+ const STYLE_EXTS = new Set(['css', 'scss', 'sass', 'less', 'styl']);
15
+ const UI_EXTS = new Set(['tsx', 'jsx']);
16
+ const CODE_EXTS = new Set(['ts', 'js', 'tsx', 'jsx']);
17
+ const INTERACTION_PATTERN = /(onClick|onSubmit|onChange|type=['"]submit['"]|role=['"]button['"]|aria-label=)/;
18
+ const PRIORITY_RANK = {
19
+ P0: 0,
20
+ P1: 1,
21
+ P2: 2,
22
+ };
23
+ function deriveFlowFromPath(relativePath) {
24
+ const segments = normalizePath(relativePath).split('/').filter(Boolean);
25
+ const baseName = baseNameWithoutExt(relativePath);
26
+ const base = baseName.toLowerCase() === 'index' && segments.length > 1 ? segments[segments.length - 2] : baseName;
27
+ for (let i = 0; i < segments.length; i += 1) {
28
+ const segment = segments[i].toLowerCase();
29
+ if (SCREEN_DIRS.has(segment)) {
30
+ const next = segments[i + 1] ? segments[i + 1] : base;
31
+ return { id: normalizePath(next), name: titleCase(next), kind: 'screen' };
32
+ }
33
+ if (FEATURE_DIRS.has(segment)) {
34
+ const next = segments[i + 1] ? segments[i + 1] : base;
35
+ return { id: normalizePath(next), name: titleCase(next), kind: 'flow' };
36
+ }
37
+ }
38
+ return { id: normalizePath(base), name: titleCase(base), kind: 'flow' };
39
+ }
40
+ function extractKeywords(relativePath) {
41
+ const segments = normalizePath(relativePath).split('/').filter(Boolean);
42
+ const base = baseNameWithoutExt(relativePath);
43
+ const tokens = segments.flatMap((segment) => tokenize(segment));
44
+ tokens.push(...tokenize(base));
45
+ return uniqueTokens(tokens);
46
+ }
47
+ function detectInteractions(content) {
48
+ if (!content)
49
+ return false;
50
+ return INTERACTION_PATTERN.test(content);
51
+ }
52
+ function isScreenPath(relativePath) {
53
+ const segments = normalizePath(relativePath).split('/').map((segment) => segment.toLowerCase());
54
+ if (segments.some((segment) => segment === 'selectors' || segment === 'reducers' || segment === 'actions')) {
55
+ return false;
56
+ }
57
+ return segments.some((segment) => SCREEN_DIRS.has(segment));
58
+ }
59
+ function isComponentPath(relativePath) {
60
+ const segments = normalizePath(relativePath).split('/').map((segment) => segment.toLowerCase());
61
+ return segments.some((segment) => COMPONENT_DIRS.has(segment));
62
+ }
63
+ function isStatePath(relativePath) {
64
+ const segments = normalizePath(relativePath).split('/').map((segment) => segment.toLowerCase());
65
+ return segments.some((segment) => STATE_DIRS.has(segment));
66
+ }
67
+ function scoreFile(file, risk) {
68
+ let score = 1;
69
+ const reasons = [];
70
+ if (file.isScreen) {
71
+ score += 3;
72
+ reasons.push('Screen-level change');
73
+ }
74
+ if (file.isComponent) {
75
+ score += 2;
76
+ reasons.push('Shared component change');
77
+ }
78
+ if (file.isUI) {
79
+ score += 2;
80
+ reasons.push('UI logic change');
81
+ }
82
+ if (file.isState) {
83
+ score += 2;
84
+ reasons.push('State or data flow change');
85
+ }
86
+ if (file.isStyle) {
87
+ score += 1;
88
+ reasons.push('Visual styling change');
89
+ }
90
+ if (file.hasInteractions) {
91
+ score += 2;
92
+ reasons.push('Interactive element change');
93
+ }
94
+ const keywordHit = file.keywords.find((keyword) => risk.criticalKeywords.includes(keyword));
95
+ if (keywordHit) {
96
+ score += 2;
97
+ reasons.push(`Critical keyword: ${keywordHit}`);
98
+ }
99
+ if (file.subsystemRisk) {
100
+ const scoreDelta = file.subsystemRisk.scoreDelta;
101
+ if (scoreDelta !== 0) {
102
+ score += scoreDelta;
103
+ reasons.push(`Subsystem risk adjustment: ${scoreDelta > 0 ? '+' : ''}${scoreDelta}`);
104
+ }
105
+ reasons.push(...file.subsystemRisk.reasons);
106
+ }
107
+ return { score, reasons };
108
+ }
109
+ function mergePriorityFloor(current, candidate) {
110
+ if (!candidate) {
111
+ return current;
112
+ }
113
+ if (!current) {
114
+ return candidate;
115
+ }
116
+ return PRIORITY_RANK[candidate] < PRIORITY_RANK[current] ? candidate : current;
117
+ }
118
+ export function analyzeFiles(appRoot, relativePaths, config) {
119
+ const files = [];
120
+ const defaultAudience = config.audience.defaultRoles;
121
+ const defaultFlagState = config.flags.defaultState;
122
+ const warnings = [];
123
+ const subsystemRisk = loadSubsystemRiskResolver(config.impact.subsystemRisk);
124
+ warnings.push(...subsystemRisk.warnings);
125
+ let subsystemRiskMatchedFiles = 0;
126
+ let subsystemRiskRuleMatches = 0;
127
+ for (const relativePath of relativePaths) {
128
+ if (isTestFilePath(relativePath)) {
129
+ continue;
130
+ }
131
+ const fullPath = join(appRoot, relativePath);
132
+ const exists = existsSync(fullPath);
133
+ const extension = fileExtension(relativePath);
134
+ const content = exists && CODE_EXTS.has(extension) ? safeReadTextFile(fullPath) : null;
135
+ const { id, name, kind } = deriveFlowFromPath(relativePath);
136
+ const audience = inferAudienceFromPath(relativePath, config);
137
+ const flags = extractFlagHits(content, config);
138
+ const subsystemRiskMatches = subsystemRisk.matchFile(normalizePath(relativePath));
139
+ let subsystemRiskScoreDelta = 0;
140
+ let subsystemRiskPriorityFloor;
141
+ const subsystemRiskRules = uniqueTokens(subsystemRiskMatches.map((entry) => entry.ruleId));
142
+ const subsystemRiskReasons = uniqueTokens(subsystemRiskMatches.flatMap((entry) => entry.reasons));
143
+ if (subsystemRiskMatches.length > 0) {
144
+ subsystemRiskMatchedFiles += 1;
145
+ subsystemRiskRuleMatches += subsystemRiskMatches.length;
146
+ subsystemRiskScoreDelta = subsystemRiskMatches.reduce((acc, entry) => acc + entry.scoreDelta, 0);
147
+ for (const match of subsystemRiskMatches) {
148
+ subsystemRiskPriorityFloor = mergePriorityFloor(subsystemRiskPriorityFloor, match.priorityFloor);
149
+ }
150
+ }
151
+ const subsystemKeywords = uniqueTokens(subsystemRiskMatches.flatMap((entry) => entry.keywords));
152
+ const analysis = {
153
+ relativePath: normalizePath(relativePath),
154
+ extension,
155
+ exists,
156
+ content,
157
+ isUI: UI_EXTS.has(extension),
158
+ isScreen: isScreenPath(relativePath),
159
+ isComponent: isComponentPath(relativePath),
160
+ isState: isStatePath(relativePath),
161
+ isStyle: STYLE_EXTS.has(extension),
162
+ hasInteractions: detectInteractions(content),
163
+ keywords: uniqueTokens([...extractKeywords(relativePath), ...subsystemKeywords]),
164
+ flowId: id,
165
+ flowName: name,
166
+ flowKind: kind,
167
+ audience,
168
+ flags,
169
+ subsystemRisk: subsystemRiskMatches.length > 0
170
+ ? {
171
+ rules: subsystemRiskRules,
172
+ scoreDelta: subsystemRiskScoreDelta,
173
+ priorityFloor: subsystemRiskPriorityFloor,
174
+ reasons: subsystemRiskReasons,
175
+ }
176
+ : undefined,
177
+ };
178
+ files.push(analysis);
179
+ }
180
+ const flowMap = new Map();
181
+ for (const file of files) {
182
+ const { score, reasons } = scoreFile(file, config.risk);
183
+ const existing = flowMap.get(file.flowId);
184
+ if (!existing) {
185
+ flowMap.set(file.flowId, {
186
+ id: file.flowId,
187
+ name: file.flowName,
188
+ kind: file.flowKind,
189
+ score,
190
+ priority: 'P2',
191
+ reasons: [...reasons],
192
+ keywords: [...file.keywords],
193
+ files: [file.relativePath],
194
+ audience: file.audience,
195
+ flags: file.flags,
196
+ priorityFloor: file.subsystemRisk?.priorityFloor,
197
+ subsystemRiskBoost: file.subsystemRisk?.scoreDelta || 0,
198
+ subsystemRiskRules: file.subsystemRisk?.rules || [],
199
+ });
200
+ }
201
+ else {
202
+ existing.score += score;
203
+ existing.files.push(file.relativePath);
204
+ existing.reasons.push(...reasons);
205
+ existing.keywords.push(...file.keywords);
206
+ existing.audience = normalizeRoles([...(existing.audience || []), ...file.audience], defaultAudience);
207
+ existing.flags = mergeFlags([...(existing.flags || []), ...file.flags], defaultFlagState);
208
+ existing.priorityFloor = mergePriorityFloor(existing.priorityFloor, file.subsystemRisk?.priorityFloor);
209
+ existing.subsystemRiskBoost = (existing.subsystemRiskBoost || 0) + (file.subsystemRisk?.scoreDelta || 0);
210
+ existing.subsystemRiskRules = uniqueTokens([...(existing.subsystemRiskRules || []), ...(file.subsystemRisk?.rules || [])]);
211
+ }
212
+ }
213
+ let boostedFlows = 0;
214
+ const flows = Array.from(flowMap.values()).map((flow) => {
215
+ const uniqueReason = uniqueTokens(flow.reasons);
216
+ const uniqueKeywords = uniqueTokens(flow.keywords);
217
+ const computedPriority = flow.score >= config.risk.p0Threshold
218
+ ? 'P0'
219
+ : flow.score >= config.risk.p1Threshold
220
+ ? 'P1'
221
+ : 'P2';
222
+ const priority = mergePriorityFloor(computedPriority, flow.priorityFloor) || computedPriority;
223
+ if ((flow.subsystemRiskBoost || 0) !== 0 || (flow.priorityFloor && flow.priorityFloor !== computedPriority)) {
224
+ boostedFlows += 1;
225
+ }
226
+ return {
227
+ ...flow,
228
+ reasons: uniqueReason.map((reason) => reason),
229
+ keywords: uniqueKeywords,
230
+ priority,
231
+ };
232
+ });
233
+ return {
234
+ files,
235
+ flows,
236
+ warnings,
237
+ subsystemRisk: {
238
+ source: 'map',
239
+ enabled: subsystemRisk.info.enabled,
240
+ mapPath: subsystemRisk.info.mapPath,
241
+ mapFound: subsystemRisk.info.mapFound,
242
+ rulesLoaded: subsystemRisk.info.rulesLoaded,
243
+ filesMatched: subsystemRiskMatchedFiles,
244
+ ruleMatches: subsystemRiskRuleMatches,
245
+ boostedFlows,
246
+ },
247
+ };
248
+ }
249
+ export function isTestFilePath(relativePath) {
250
+ return TEST_PATH_PATTERN.test(normalizePath(relativePath));
251
+ }
252
+ export function scanRepositoryFlows(appRoot, limit = 250, patterns, exclude) {
253
+ const defaultPatterns = [
254
+ '**/pages/**/*.{tsx,jsx,ts,js}',
255
+ '**/screens/**/*.{tsx,jsx,ts,js}',
256
+ '**/views/**/*.{tsx,jsx,ts,js}',
257
+ '**/routes/**/*.{tsx,jsx,ts,js}',
258
+ ];
259
+ const useDefaults = !(patterns && patterns.length > 0);
260
+ const activePatterns = useDefaults ? defaultPatterns : patterns;
261
+ const matches = new Set();
262
+ const ignorePatterns = [
263
+ '**/node_modules/**',
264
+ '**/.git/**',
265
+ '**/__tests__/**',
266
+ '**/tests/**',
267
+ ...(useDefaults ? ['**/selectors/**', '**/reducers/**', '**/actions/**'] : []),
268
+ ...(exclude || []),
269
+ ];
270
+ for (const pattern of activePatterns) {
271
+ const files = globSync(pattern, {
272
+ cwd: appRoot,
273
+ ignore: ignorePatterns,
274
+ nodir: true,
275
+ });
276
+ for (const file of files) {
277
+ matches.add(normalizePath(file));
278
+ if (matches.size >= limit) {
279
+ break;
280
+ }
281
+ }
282
+ if (matches.size >= limit) {
283
+ break;
284
+ }
285
+ }
286
+ return Array.from(matches);
287
+ }
@@ -0,0 +1,34 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { computeBlastRadius, mergeFlags, normalizeRoles } from './flags.js';
4
+ export function applyBlastRadius(flows, files, config) {
5
+ if (flows.length === 0) {
6
+ return flows;
7
+ }
8
+ const fileMap = new Map();
9
+ for (const file of files) {
10
+ fileMap.set(file.relativePath, file);
11
+ }
12
+ return flows.map((flow) => {
13
+ const collectedFlags = [...(flow.flags || [])];
14
+ const collectedAudience = [...(flow.audience || [])];
15
+ for (const filePath of flow.files) {
16
+ const file = fileMap.get(filePath);
17
+ if (!file) {
18
+ continue;
19
+ }
20
+ collectedFlags.push(...file.flags);
21
+ collectedAudience.push(...file.audience);
22
+ }
23
+ const mergedFlags = mergeFlags(collectedFlags, config.flags.defaultState);
24
+ const mergedAudience = normalizeRoles(collectedAudience, config.audience.defaultRoles);
25
+ const blastRadius = computeBlastRadius(mergedAudience, mergedFlags, config);
26
+ return {
27
+ ...flow,
28
+ audience: mergedAudience,
29
+ flags: mergedFlags,
30
+ blastRadius,
31
+ score: flow.score + blastRadius.scoreDelta,
32
+ };
33
+ });
34
+ }
@@ -0,0 +1,63 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ export class SimpleCache {
4
+ constructor(ttlMs = 5 * 60 * 1000) {
5
+ this.cache = new Map();
6
+ // Default: 5 minutes
7
+ this.ttlMs = ttlMs;
8
+ }
9
+ /**
10
+ * Get value from cache if it exists and hasn't expired
11
+ */
12
+ get(key) {
13
+ const entry = this.cache.get(key);
14
+ if (!entry) {
15
+ return undefined;
16
+ }
17
+ // Check if entry has expired
18
+ if (Date.now() - entry.timestamp > this.ttlMs) {
19
+ this.cache.delete(key);
20
+ return undefined;
21
+ }
22
+ return entry.value;
23
+ }
24
+ /**
25
+ * Set value in cache with current timestamp
26
+ */
27
+ set(key, value) {
28
+ this.cache.set(key, {
29
+ value,
30
+ timestamp: Date.now(),
31
+ });
32
+ }
33
+ /**
34
+ * Clear all entries from cache
35
+ */
36
+ clear() {
37
+ this.cache.clear();
38
+ }
39
+ /**
40
+ * Get cache size
41
+ */
42
+ size() {
43
+ return this.cache.size;
44
+ }
45
+ /**
46
+ * Get cache statistics
47
+ */
48
+ stats() {
49
+ // Clean expired entries
50
+ const now = Date.now();
51
+ let expired = 0;
52
+ for (const [key, entry] of this.cache.entries()) {
53
+ if (now - entry.timestamp > this.ttlMs) {
54
+ this.cache.delete(key);
55
+ expired++;
56
+ }
57
+ }
58
+ return {
59
+ size: this.cache.size,
60
+ entries: this.cache.size,
61
+ };
62
+ }
63
+ }