@skyramp/mcp 0.3.8 → 0.4.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -1
  2. package/build/commands/commandLibrary.js +3 -3
  3. package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
  4. package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
  5. package/build/commands/testThisEndpointCommand.js +35 -19
  6. package/build/index.js +9 -3
  7. package/build/playwright/blueprintDigest.d.ts +15 -0
  8. package/build/playwright/blueprintDigest.js +152 -0
  9. package/build/playwright/blueprintDigestStore.d.ts +31 -0
  10. package/build/playwright/blueprintDigestStore.js +117 -0
  11. package/build/playwright/registerPlaywrightTools.js +60 -12
  12. package/build/playwright/traceRecordingPrompt.js +8 -7
  13. package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
  14. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
  15. package/build/prompts/promptAssets.d.ts +20 -0
  16. package/build/prompts/promptAssets.js +55 -0
  17. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
  18. package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
  19. package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
  20. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
  21. package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
  22. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
  23. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
  24. package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
  25. package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
  26. package/build/prompts/test-recommendation/recommendationSections.js +67 -309
  27. package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
  28. package/build/prompts/test-recommendation/recommendationShared.js +49 -155
  29. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
  30. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
  31. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
  32. package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
  33. package/build/prompts/testbot/planDeclarations.d.ts +6 -0
  34. package/build/prompts/testbot/planDeclarations.js +9 -0
  35. package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
  36. package/build/prompts/testbot/testbot-prompts.js +256 -381
  37. package/build/recommendation/answers.d.ts +35 -0
  38. package/build/recommendation/answers.js +96 -0
  39. package/build/recommendation/registerPlan.d.ts +49 -0
  40. package/build/recommendation/registerPlan.js +117 -0
  41. package/build/recommendation/runVerifiers.d.ts +10 -0
  42. package/build/recommendation/runVerifiers.js +49 -0
  43. package/build/recommendation/subjectStep.d.ts +42 -0
  44. package/build/recommendation/subjectStep.js +86 -0
  45. package/build/recommendation/types.d.ts +163 -0
  46. package/build/recommendation/types.js +20 -0
  47. package/build/recommendation/verifierContracts.d.ts +382 -0
  48. package/build/recommendation/verifierContracts.js +263 -0
  49. package/build/recommendation/verifiers/changedFile.d.ts +2 -0
  50. package/build/recommendation/verifiers/changedFile.js +82 -0
  51. package/build/recommendation/verifiers/citedPath.d.ts +12 -0
  52. package/build/recommendation/verifiers/citedPath.js +35 -0
  53. package/build/recommendation/verifiers/coverage.d.ts +7 -0
  54. package/build/recommendation/verifiers/coverage.js +617 -0
  55. package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
  56. package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
  57. package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
  58. package/build/recommendation/verifiers/endpointGrounded.js +128 -0
  59. package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
  60. package/build/recommendation/verifiers/existingCoverage.js +51 -0
  61. package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
  62. package/build/recommendation/verifiers/expectedOutcome.js +105 -0
  63. package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
  64. package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
  65. package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
  66. package/build/recommendation/verifiers/reportedCategory.js +84 -0
  67. package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
  68. package/build/recommendation/verifiers/screenRoute.js +118 -0
  69. package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
  70. package/build/recommendation/verifiers/statedDifference.js +140 -0
  71. package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
  72. package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
  73. package/build/resources/analysisResources.js +1 -114
  74. package/build/resources/testbotResource.js +23 -13
  75. package/build/services/ModularizationService.js +2 -1
  76. package/build/services/TestDiscoveryService.d.ts +3 -72
  77. package/build/services/TestDiscoveryService.js +10 -303
  78. package/build/services/containerEnv.d.ts +1 -1
  79. package/build/services/containerEnv.js +12 -0
  80. package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
  81. package/build/skills/fixTestImportErrorsSkill.js +20 -0
  82. package/build/toolNames.d.ts +1 -0
  83. package/build/toolNames.js +1 -0
  84. package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
  85. package/build/tools/code-refactor/modularizationTool.js +2 -1
  86. package/build/tools/executeSkyrampTestTool.d.ts +80 -0
  87. package/build/tools/executeSkyrampTestTool.js +246 -19
  88. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
  89. package/build/tools/generate-tests/generateContractRestTool.js +3 -3
  90. package/build/tools/generate-tests/planGuard.d.ts +2 -2
  91. package/build/tools/generate-tests/planGuard.js +78 -18
  92. package/build/tools/one-click/oneClickTool.d.ts +0 -1
  93. package/build/tools/one-click/oneClickTool.js +0 -5
  94. package/build/tools/submitReportTool.d.ts +48 -42
  95. package/build/tools/submitReportTool.js +576 -193
  96. package/build/tools/test-management/actionsTool.js +72 -4
  97. package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
  98. package/build/tools/test-management/analyzeChangesTool.js +212 -1219
  99. package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
  100. package/build/tools/test-management/index.d.ts +1 -0
  101. package/build/tools/test-management/index.js +1 -0
  102. package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
  103. package/build/tools/test-management/registerTestPlanTool.js +609 -542
  104. package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
  105. package/build/tools/test-management/resolveScreenTool.js +289 -0
  106. package/build/types/BlueprintDigest.d.ts +34 -0
  107. package/build/types/BlueprintDigest.js +1 -0
  108. package/build/types/RepositoryAnalysis.d.ts +20 -1559
  109. package/build/types/RepositoryAnalysis.js +2 -58
  110. package/build/types/StepMethod.d.ts +40 -0
  111. package/build/types/StepMethod.js +77 -0
  112. package/build/types/TestAnalysis.d.ts +12 -0
  113. package/build/types/TestExecution.d.ts +4 -0
  114. package/build/types/TestRecommendation.d.ts +24 -24
  115. package/build/types/TestRecommendation.js +91 -89
  116. package/build/types/TestbotPromptOptions.d.ts +0 -4
  117. package/build/types/TestbotReport.d.ts +64 -2
  118. package/build/utils/AnalysisStateManager.d.ts +79 -113
  119. package/build/utils/AnalysisStateManager.js +147 -57
  120. package/build/utils/assertion-verify/api-shared-lints.js +1 -1
  121. package/build/utils/assertion-verify/metrics.js +85 -36
  122. package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
  123. package/build/utils/assertion-verify/ui-lints.js +32 -0
  124. package/build/utils/branchDiff.d.ts +63 -31
  125. package/build/utils/branchDiff.js +242 -94
  126. package/build/utils/containedPath.d.ts +18 -0
  127. package/build/utils/containedPath.js +73 -0
  128. package/build/utils/dartRouteExtractor.d.ts +18 -34
  129. package/build/utils/dartRouteExtractor.js +101 -173
  130. package/build/utils/featureFlags.d.ts +12 -0
  131. package/build/utils/featureFlags.js +14 -0
  132. package/build/utils/frontendSelectors.d.ts +48 -27
  133. package/build/utils/frontendSelectors.js +241 -80
  134. package/build/utils/pathMatching.d.ts +2 -4
  135. package/build/utils/pathMatching.js +2 -4
  136. package/build/utils/planMatchKeys.d.ts +38 -47
  137. package/build/utils/planMatchKeys.js +143 -81
  138. package/build/utils/rebaselineSnapshots.d.ts +24 -0
  139. package/build/utils/rebaselineSnapshots.js +65 -0
  140. package/build/utils/removedUiElements.d.ts +22 -0
  141. package/build/utils/removedUiElements.js +106 -0
  142. package/build/utils/reportVerification.d.ts +2 -6
  143. package/build/utils/reportVerification.js +61 -2
  144. package/build/utils/screenRoutes.d.ts +66 -0
  145. package/build/utils/screenRoutes.js +727 -0
  146. package/build/utils/sourceRouteExtractor.js +320 -112
  147. package/build/utils/testFileClassification.d.ts +11 -2
  148. package/build/utils/testFileClassification.js +44 -2
  149. package/build/utils/testFixtures.d.ts +5 -0
  150. package/build/utils/testFixtures.js +13 -0
  151. package/build/utils/utils.d.ts +0 -1
  152. package/build/utils/utils.js +0 -11
  153. package/build/utils/versions.d.ts +3 -3
  154. package/build/utils/versions.js +1 -1
  155. package/build/workspace/workspace.d.ts +12 -12
  156. package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
  157. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
  158. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
  159. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
  160. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
  161. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
  162. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  163. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  167. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  168. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
  170. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
  171. package/node_modules/playwright/package.json +1 -1
  172. package/package.json +4 -3
  173. package/plugin/.claude-plugin/plugin.json +8 -0
  174. package/plugin/plugin.json +6 -0
  175. package/plugin/prompts/declaring-a-plan.md +20 -0
  176. package/plugin/prompts/generate-tests/context-fetching.md +4 -0
  177. package/plugin/prompts/generate-tests/execution-plan.md +63 -0
  178. package/plugin/prompts/generate-tests/generation.md +108 -0
  179. package/plugin/prompts/generate-tests/path-parameters.md +1 -0
  180. package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
  181. package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
  182. package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
  183. package/plugin/prompts/plan-tests.md +42 -0
  184. package/plugin/prompts/testbot-task1.md +82 -0
  185. package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
  186. package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
  187. package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
  188. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
  189. package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
  190. package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
  191. package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
  192. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
  193. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
  194. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
  195. package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
  196. package/build/recommendation/budgeters/shared.d.ts +0 -32
  197. package/build/recommendation/budgeters/shared.js +0 -246
  198. package/build/recommendation/discriminators.d.ts +0 -37
  199. package/build/recommendation/discriminators.js +0 -379
  200. package/build/recommendation/diversity.d.ts +0 -47
  201. package/build/recommendation/diversity.js +0 -101
  202. package/build/recommendation/planRanker.d.ts +0 -65
  203. package/build/recommendation/planRanker.js +0 -83
  204. package/build/recommendation/testFixtures.d.ts +0 -25
  205. package/build/recommendation/testFixtures.js +0 -45
  206. package/build/types/FrontendIntegration.d.ts +0 -28
  207. package/build/types/FrontendIntegration.js +0 -22
  208. package/build/types/Recommendation.d.ts +0 -146
  209. package/build/types/Recommendation.js +0 -74
  210. package/build/utils/changedRoutes.d.ts +0 -29
  211. package/build/utils/changedRoutes.js +0 -87
  212. package/build/utils/frontendIntegration.d.ts +0 -9
  213. package/build/utils/frontendIntegration.js +0 -243
  214. package/build/utils/importerHop.d.ts +0 -135
  215. package/build/utils/importerHop.js +0 -489
  216. package/build/utils/pathAffinityClassification.d.ts +0 -49
  217. package/build/utils/pathAffinityClassification.js +0 -180
  218. package/build/utils/pythonMountPrefixes.d.ts +0 -25
  219. package/build/utils/pythonMountPrefixes.js +0 -347
  220. package/build/utils/repoScanner.d.ts +0 -34
  221. package/build/utils/repoScanner.js +0 -300
  222. package/build/utils/routeParsers.d.ts +0 -95
  223. package/build/utils/routeParsers.js +0 -951
  224. package/build/utils/scenarioDrafting.d.ts +0 -92
  225. package/build/utils/scenarioDrafting.js +0 -951
  226. package/build/utils/subjectEndpoints.d.ts +0 -19
  227. package/build/utils/subjectEndpoints.js +0 -98
  228. package/build/utils/uiPageEnumerator.d.ts +0 -172
  229. package/build/utils/uiPageEnumerator.js +0 -474
@@ -32,7 +32,7 @@ import * as path from "path";
32
32
  import * as ts from "typescript";
33
33
  import { collectFiles } from "./fileWalk.js";
34
34
  function makeParseCache() {
35
- return { files: new Map(), tsconfigBaseUrl: new Map() };
35
+ return { files: new Map(), tsconfig: new Map() };
36
36
  }
37
37
  /** Recursion cap for cross-file constant resolution. */
38
38
  const MAX_RESOLVE_DEPTH = 4;
@@ -155,9 +155,7 @@ function extractRoutesFromFile(filePath, cache) {
155
155
  visit(sf, (node) => {
156
156
  // 1. JSX route elements: <Route path="…" element={<X/>} />
157
157
  if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
158
- const r = jsxRouteFromNode(node, filePath, imports, localConsts, cache);
159
- if (r)
160
- routes.push(r);
158
+ routes.push(...jsxRoutesFromNode(node, filePath, imports, localConsts, cache));
161
159
  return;
162
160
  }
163
161
  // 2. defineModule({ id, routes: [...] }) — Directus per-module aggregation.
@@ -179,37 +177,66 @@ function extractRoutesFromFile(filePath, cache) {
179
177
  routes.push(...vueRouterRoutesFromFile(sf, filePath, imports, localConsts, cache));
180
178
  return routes;
181
179
  }
182
- function jsxRouteFromNode(node, filePath, imports, localConsts, cache) {
180
+ function jsxRoutesFromNode(node, filePath, imports, localConsts, cache) {
183
181
  const tag = jsxTagOf(node);
184
182
  if (!tag || !ROUTE_TAG_NAMES.has(tag))
185
- return null;
183
+ return [];
186
184
  const attrs = jsxAttributesOf(node);
185
+ const componentAttr = attrs.find((a) => a.name === "element" || a.name === "component");
186
+ if (!componentAttr || componentAttr.kind !== "identifier")
187
+ return [];
188
+ const mountedAt = (routePath) => ({
189
+ path: routePath,
190
+ componentName: componentAttr.value,
191
+ componentFile: imports.get(componentAttr.value)?.filePath,
192
+ declaredIn: filePath,
193
+ });
194
+ // React Router child paths are relative to the enclosing <Route>; compose
195
+ // with the ancestor chain so nested routes yield full navigable URLs.
196
+ const parentPrefix = enclosingJsxRoutePath(node, filePath, imports, localConsts, cache);
187
197
  const pathAttr = attrs.find((a) => a.name === "path");
188
- let ownPath;
189
198
  if (pathAttr) {
190
- ownPath = resolveAttrToString(pathAttr, filePath, imports, localConsts, cache);
199
+ const ownPath = resolveAttrToString(pathAttr, filePath, imports, localConsts, cache);
191
200
  if (ownPath === null)
192
- return null;
201
+ return [];
202
+ return [mountedAt(composeRoutePath(parentPrefix, ownPath))];
193
203
  }
194
- else if (attrs.some((a) => a.name === "index")) {
204
+ if (attrs.some((a) => a.name === "index")) {
195
205
  // <Route index> has no path — it renders at the parent route's path.
196
- ownPath = "";
197
- }
198
- else {
199
- return null;
206
+ return [mountedAt(composeRoutePath(parentPrefix, ""))];
200
207
  }
201
- const componentAttr = attrs.find((a) => a.name === "element" || a.name === "component");
202
- if (!componentAttr || componentAttr.kind !== "identifier")
203
- return null;
204
- // React Router child paths are relative to the enclosing <Route>; compose
205
- // with the ancestor chain so nested routes yield full navigable URLs.
206
- const parentPrefix = enclosingJsxRoutePath(node, filePath, imports, localConsts, cache);
207
- return {
208
- path: composeRoutePath(parentPrefix, ownPath),
209
- componentName: componentAttr.value,
210
- componentFile: imports.get(componentAttr.value)?.filePath,
211
- declaredIn: filePath,
212
- };
208
+ // A pathless layout route has no URL of its own: it renders on every route
209
+ // nested under it. Reporting nothing would read as "no route renders this
210
+ // file", the one answer that is definitely wrong for a layout, and the parent
211
+ // path on its own would be a URL the app may not serve. Report the pages it
212
+ // appears on, the way readTanStackRouteTree reports a pathless TanStack layout.
213
+ return nestedJsxRoutePaths(node, filePath, imports, localConsts, cache).map(mountedAt);
214
+ }
215
+ /** The composed paths of the routes written inside a JSX route element. */
216
+ function nestedJsxRoutePaths(node, filePath, imports, localConsts, cache) {
217
+ const element = node.parent;
218
+ if (!ts.isJsxElement(element) || element.openingElement !== node)
219
+ return [];
220
+ const paths = [];
221
+ visit(element, (inner) => {
222
+ if (inner === node)
223
+ return;
224
+ if (!ts.isJsxOpeningElement(inner) && !ts.isJsxSelfClosingElement(inner))
225
+ return;
226
+ const tag = jsxTagOf(inner);
227
+ if (!tag || !ROUTE_TAG_NAMES.has(tag))
228
+ return;
229
+ const pathAttr = jsxAttributesOf(inner).find((a) => a.name === "path");
230
+ if (!pathAttr)
231
+ return;
232
+ const ownPath = resolveAttrToString(pathAttr, filePath, imports, localConsts, cache);
233
+ if (ownPath === null)
234
+ return;
235
+ const composed = composeRoutePath(enclosingJsxRoutePath(inner, filePath, imports, localConsts, cache), ownPath);
236
+ if (!paths.includes(composed))
237
+ paths.push(composed);
238
+ });
239
+ return paths;
213
240
  }
214
241
  /**
215
242
  * Walk a JSX route node's ancestor chain and compose the paths of the enclosing
@@ -323,36 +350,19 @@ function parseExpressionAttr(name, expr) {
323
350
  * import { WORKSPACE_URL } from "constants/routes" // baseUrl-resolved
324
351
  *
325
352
  * Bare imports (`react-router-dom`) are skipped — those aren't local files.
326
- * `paths` aliases (e.g. `@/*`, `ee/*`) aren't currently honored; the only
327
- * consequence in target repos is missing some constants in deep-aliased
328
- * subtrees, which downgrades to "couldn't resolve, skip the route" — safe.
353
+ * Aliased imports (`@/components/X`) go through tsconfig `paths`; an alias with
354
+ * no matching pattern downgrades to "couldn't resolve, skip the route" — safe.
329
355
  */
330
356
  function collectImports(sf, filePath, cache) {
331
357
  const result = new Map();
332
358
  const fileDir = path.dirname(filePath);
333
- const baseUrl = findTsconfigBaseUrl(fileDir, cache);
334
359
  for (const stmt of sf.statements) {
335
360
  if (!ts.isImportDeclaration(stmt))
336
361
  continue;
337
362
  const moduleSpec = stmt.moduleSpecifier;
338
363
  if (!ts.isStringLiteral(moduleSpec))
339
364
  continue;
340
- const spec = moduleSpec.text;
341
- let resolved;
342
- if (spec.startsWith("/")) {
343
- // Absolute import specs would let a hostile repo point the analyzer
344
- // at arbitrary host paths during route extraction (e.g. /etc/passwd).
345
- // Real codebases don't use literal absolute imports — they use
346
- // relative paths or tsconfig path aliases.
347
- continue;
348
- }
349
- if (spec.startsWith(".")) {
350
- resolved = resolveImportPath(fileDir, spec);
351
- }
352
- else if (baseUrl) {
353
- // Try baseUrl resolution for non-relative imports.
354
- resolved = resolveImportPath(baseUrl, spec);
355
- }
365
+ const resolved = resolveModuleSpecifier(moduleSpec.text, fileDir, cache);
356
366
  if (!resolved)
357
367
  continue;
358
368
  const clause = stmt.importClause;
@@ -494,7 +504,6 @@ function resolveExportedSymbolToString(filePath, exportedName, cache, depth) {
494
504
  const imports = collectImports(sf, filePath, cache);
495
505
  const localConsts = collectLocalConsts(sf);
496
506
  const fileDir = path.dirname(filePath);
497
- const baseUrl = findTsconfigBaseUrl(fileDir, cache);
498
507
  // 1. Look for `export const X = …` directly in this file.
499
508
  for (const stmt of sf.statements) {
500
509
  if (!ts.isVariableStatement(stmt))
@@ -547,17 +556,7 @@ function resolveExportedSymbolToString(filePath, exportedName, cache, depth) {
547
556
  if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier))
548
557
  continue;
549
558
  const targetSpec = stmt.moduleSpecifier.text;
550
- let targetFile;
551
- if (targetSpec.startsWith("/")) {
552
- // Absolute paths rejected for the same reason as collectImports.
553
- continue;
554
- }
555
- if (targetSpec.startsWith(".")) {
556
- targetFile = resolveImportPath(fileDir, targetSpec);
557
- }
558
- else if (baseUrl) {
559
- targetFile = resolveImportPath(baseUrl, targetSpec);
560
- }
559
+ const targetFile = resolveModuleSpecifier(targetSpec, fileDir, cache);
561
560
  if (!targetFile)
562
561
  continue;
563
562
  if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
@@ -581,18 +580,43 @@ function resolveExportedSymbolToString(filePath, exportedName, cache, depth) {
581
580
  return null;
582
581
  }
583
582
  const IMPORT_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js", ".vue"];
583
+ /**
584
+ * TypeScript's `moduleResolution: node16/nodenext` requires an ESM import to
585
+ * name the file the compiler EMITS, so a `.tsx` component is imported as
586
+ * `./Page.js`. Appending extensions to that specifier only ever tries
587
+ * `Page.js.tsx`, so the TS source has to be looked up under its own extension.
588
+ */
589
+ const TS_SOURCE_EXTENSIONS = {
590
+ ".js": [".ts", ".tsx"],
591
+ ".jsx": [".tsx"],
592
+ ".mjs": [".mts"],
593
+ ".cjs": [".cts"],
594
+ };
584
595
  function resolveImportPath(fromDir, spec) {
585
- const base = path.resolve(fromDir, spec);
596
+ return resolveFilePath(path.resolve(fromDir, spec));
597
+ }
598
+ /** The file an already-joined absolute module path names, if any. */
599
+ function resolveFilePath(base) {
586
600
  // 1. Exact file (with the extension already in spec).
587
601
  if (fileExistsSync(base))
588
602
  return base;
589
- // 2. Try each known extension.
603
+ // 2. A JavaScript extension standing in for the TypeScript source.
604
+ const jsExt = /\.(?:js|jsx|mjs|cjs)$/i.exec(base);
605
+ if (jsExt) {
606
+ const stem = base.slice(0, base.length - jsExt[0].length);
607
+ for (const ext of TS_SOURCE_EXTENSIONS[jsExt[0].toLowerCase()] ?? []) {
608
+ const candidate = stem + ext;
609
+ if (fileExistsSync(candidate))
610
+ return candidate;
611
+ }
612
+ }
613
+ // 3. Try each known extension.
590
614
  for (const ext of IMPORT_EXTENSIONS) {
591
615
  const candidate = base + ext;
592
616
  if (fileExistsSync(candidate))
593
617
  return candidate;
594
618
  }
595
- // 3. Treat as directory with index.{tsx,jsx,ts,js,vue}.
619
+ // 4. Treat as directory with index.{tsx,jsx,ts,js,vue}.
596
620
  for (const ext of IMPORT_EXTENSIONS) {
597
621
  const candidate = path.join(base, "index" + ext);
598
622
  if (fileExistsSync(candidate))
@@ -600,6 +624,51 @@ function resolveImportPath(fromDir, spec) {
600
624
  }
601
625
  return undefined;
602
626
  }
627
+ /**
628
+ * The repository file a module specifier written in `fromDir` names, or
629
+ * undefined when nothing in the project answers it.
630
+ *
631
+ * An absolute specifier is refused: it would let a scanned repository point the
632
+ * analyzer at any file on the host (`import x from "/etc/passwd"`). Real
633
+ * projects write relative paths or tsconfig aliases.
634
+ */
635
+ function resolveModuleSpecifier(spec, fromDir, cache) {
636
+ if (spec.startsWith("/") || path.isAbsolute(spec))
637
+ return undefined;
638
+ if (spec.startsWith("."))
639
+ return resolveImportPath(fromDir, spec);
640
+ const tsconfig = findTsconfig(fromDir, cache);
641
+ if (!tsconfig)
642
+ return undefined;
643
+ const viaPaths = resolveViaTsconfigPaths(spec, tsconfig);
644
+ if (viaPaths)
645
+ return viaPaths;
646
+ return tsconfig.baseUrl ? resolveImportPath(tsconfig.baseUrl, spec) : undefined;
647
+ }
648
+ /** The file a `paths` alias names, trying the most specific pattern first and,
649
+ * within one pattern, the first target that exists. */
650
+ function resolveViaTsconfigPaths(spec, tsconfig) {
651
+ for (const alias of tsconfig.paths) {
652
+ let substitution;
653
+ if (alias.wildcard) {
654
+ if (spec.length < alias.prefix.length + alias.suffix.length)
655
+ continue;
656
+ if (!spec.startsWith(alias.prefix) || !spec.endsWith(alias.suffix))
657
+ continue;
658
+ substitution = spec.slice(alias.prefix.length, spec.length - alias.suffix.length);
659
+ }
660
+ else if (spec !== alias.prefix) {
661
+ continue;
662
+ }
663
+ for (const target of alias.targets) {
664
+ const candidate = substitution === undefined ? target : target.replace("*", substitution);
665
+ const resolved = resolveFilePath(candidate);
666
+ if (resolved)
667
+ return resolved;
668
+ }
669
+ }
670
+ return undefined;
671
+ }
603
672
  function fileExistsSync(p) {
604
673
  try {
605
674
  const stat = fs.statSync(p);
@@ -610,40 +679,37 @@ function fileExistsSync(p) {
610
679
  }
611
680
  }
612
681
  /**
613
- * Walk parent directories until a `tsconfig.json` is found, then resolve its
614
- * `baseUrl` (following one level of `extends` for the common case where
682
+ * Walk parent directories until a `tsconfig.json` is found, then read its
683
+ * `baseUrl` and `paths` (following `extends` for the common case where
615
684
  * tsconfig.json extends a tsconfig.path.json sibling). Returns null when no
616
- * tsconfig is found or no `baseUrl` is configured.
685
+ * tsconfig is found, or when the one found configures neither.
617
686
  *
618
687
  * Memoized in the parse cache: cache hits AND misses, both keyed by the
619
688
  * directory we started searching from. Prevents re-stat'ing the same parent
620
689
  * chain on every import in a deeply nested tree.
621
690
  */
622
- function findTsconfigBaseUrl(fromDir, cache) {
623
- const cached = cache.tsconfigBaseUrl.get(fromDir);
691
+ function findTsconfig(fromDir, cache) {
692
+ const cached = cache.tsconfig.get(fromDir);
624
693
  if (cached !== undefined)
625
694
  return cached;
626
695
  let cur = fromDir;
627
696
  for (let i = 0; i < 12; i++) {
628
697
  const tsconfigPath = path.join(cur, "tsconfig.json");
629
698
  if (fileExistsSync(tsconfigPath)) {
630
- const baseUrl = readTsconfigBaseUrl(tsconfigPath);
631
- cache.tsconfigBaseUrl.set(fromDir, baseUrl);
632
- return baseUrl;
699
+ // Cap recursion through `extends` to avoid pathological loops.
700
+ const resolution = readTsconfig(tsconfigPath, 0);
701
+ cache.tsconfig.set(fromDir, resolution);
702
+ return resolution;
633
703
  }
634
704
  const parent = path.dirname(cur);
635
705
  if (parent === cur)
636
706
  break;
637
707
  cur = parent;
638
708
  }
639
- cache.tsconfigBaseUrl.set(fromDir, null);
709
+ cache.tsconfig.set(fromDir, null);
640
710
  return null;
641
711
  }
642
- function readTsconfigBaseUrl(tsconfigPath) {
643
- // Cap recursion through `extends` to avoid pathological loops.
644
- return readTsconfigBaseUrlInner(tsconfigPath, 0);
645
- }
646
- function readTsconfigBaseUrlInner(tsconfigPath, depth) {
712
+ function readTsconfig(tsconfigPath, depth) {
647
713
  if (depth > 4)
648
714
  return null;
649
715
  let parsed;
@@ -655,26 +721,86 @@ function readTsconfigBaseUrlInner(tsconfigPath, depth) {
655
721
  catch {
656
722
  return null;
657
723
  }
658
- const baseUrl = parsed?.compilerOptions?.baseUrl;
659
- if (typeof baseUrl === "string") {
660
- return path.resolve(path.dirname(tsconfigPath), baseUrl);
661
- }
662
- const extendsPath = parsed?.extends;
663
- if (typeof extendsPath === "string") {
664
- const tsconfigDir = path.dirname(tsconfigPath);
665
- let extendedPath;
666
- if (extendsPath.startsWith(".") || extendsPath.startsWith("/")) {
667
- extendedPath = path.resolve(tsconfigDir, extendsPath);
668
- }
669
- else {
670
- // Bare-extend (e.g. "@tsconfig/node20/tsconfig.json") — skip.
724
+ const tsconfigDir = path.dirname(tsconfigPath);
725
+ const rawBaseUrl = parsed?.compilerOptions?.baseUrl;
726
+ let baseUrl = null;
727
+ if (typeof rawBaseUrl === "string") {
728
+ // An absolute baseUrl makes every non-relative import in the repo resolve
729
+ // against a directory of the tsconfig's choosing, anywhere on the host. The
730
+ // same reason absolute import specifiers are skipped applies here, and a
731
+ // real project writes baseUrl relative to its own tsconfig.
732
+ if (path.isAbsolute(rawBaseUrl))
671
733
  return null;
734
+ baseUrl = path.resolve(tsconfigDir, rawBaseUrl);
735
+ }
736
+ // TypeScript resolves `paths` targets against baseUrl when the project sets
737
+ // one, and against the tsconfig's own directory otherwise.
738
+ const paths = readPathAliases(parsed?.compilerOptions?.paths, baseUrl ?? tsconfigDir);
739
+ if (baseUrl !== null && paths.length > 0)
740
+ return { baseUrl, paths };
741
+ const inherited = readExtendedTsconfig(parsed?.extends, tsconfigDir, depth);
742
+ const merged = {
743
+ baseUrl: baseUrl ?? inherited?.baseUrl ?? null,
744
+ paths: paths.length > 0 ? paths : (inherited?.paths ?? []),
745
+ };
746
+ if (merged.baseUrl === null && merged.paths.length === 0)
747
+ return null;
748
+ return merged;
749
+ }
750
+ /** Follow `extends`, refusing anything that is not a config file named relative
751
+ * to this one. */
752
+ function readExtendedTsconfig(extendsPath, tsconfigDir, depth) {
753
+ if (typeof extendsPath !== "string")
754
+ return null;
755
+ if (path.isAbsolute(extendsPath)) {
756
+ // `extends: "/etc/passwd"` would have this function read that file during
757
+ // a scan. Only a sibling config, named relative to this one, is followed.
758
+ return null;
759
+ }
760
+ // Bare-extend (e.g. "@tsconfig/node20/tsconfig.json") — skip.
761
+ if (!extendsPath.startsWith("."))
762
+ return null;
763
+ let extended = path.resolve(tsconfigDir, extendsPath);
764
+ if (!extended.endsWith(".json"))
765
+ extended += ".json";
766
+ return readTsconfig(extended, depth + 1);
767
+ }
768
+ /** Parse `compilerOptions.paths` into match-ready aliases. */
769
+ function readPathAliases(raw, resolveAgainst) {
770
+ if (!raw || typeof raw !== "object")
771
+ return [];
772
+ const aliases = [];
773
+ for (const [pattern, value] of Object.entries(raw)) {
774
+ if (!Array.isArray(value))
775
+ continue;
776
+ const star = pattern.indexOf("*");
777
+ // TypeScript accepts at most one `*` per pattern and per target.
778
+ if (star !== pattern.lastIndexOf("*"))
779
+ continue;
780
+ const targets = [];
781
+ for (const target of value) {
782
+ if (typeof target !== "string")
783
+ continue;
784
+ // An absolute target reaches outside the project, the same way an
785
+ // absolute baseUrl would.
786
+ if (path.isAbsolute(target))
787
+ continue;
788
+ if (target.indexOf("*") !== target.lastIndexOf("*"))
789
+ continue;
790
+ targets.push(path.resolve(resolveAgainst, target));
672
791
  }
673
- if (!extendedPath.endsWith(".json"))
674
- extendedPath += ".json";
675
- return readTsconfigBaseUrlInner(extendedPath, depth + 1);
792
+ if (targets.length === 0)
793
+ continue;
794
+ aliases.push({
795
+ prefix: star >= 0 ? pattern.slice(0, star) : pattern,
796
+ suffix: star >= 0 ? pattern.slice(star + 1) : "",
797
+ wildcard: star >= 0,
798
+ targets,
799
+ });
676
800
  }
677
- return null;
801
+ // Longest prefix first, so `@/components/*` is tried before `@/*`.
802
+ aliases.sort((a, b) => b.prefix.length - a.prefix.length);
803
+ return aliases;
678
804
  }
679
805
  /**
680
806
  * Strip line/block comments from JSON before parsing. tsconfig.json files
@@ -713,12 +839,55 @@ function vueRouterRoutesFromFile(sf, filePath, imports, localConsts, cache) {
713
839
  // heuristic anyway, so this guard only bites the rarer `component:` form.
714
840
  if (isInsideCreateRouter(node))
715
841
  return;
716
- const r = routeFromObjectLiteral(node, "", filePath, imports, localConsts, cache);
842
+ const prefix = enclosingObjectRoutePath(node, filePath, imports, localConsts, cache);
843
+ if (prefix === null)
844
+ return;
845
+ const r = routeFromObjectLiteral(node, prefix, filePath, imports, localConsts, cache);
717
846
  if (r)
718
847
  out.push(r);
719
848
  });
720
849
  return out;
721
850
  }
851
+ /**
852
+ * The path prefix a route object literal inherits from the route objects that
853
+ * hold it in a `children:` array, composed root-to-leaf. `""` for a top-level
854
+ * route. `null` when an ancestor states a path this scan cannot resolve: the
855
+ * descendant's URL would then be wrong, and a wrong URL sends the browser to
856
+ * the wrong page with the server's authority behind it.
857
+ */
858
+ function enclosingObjectRoutePath(node, filePath, imports, localConsts, cache) {
859
+ const segments = [];
860
+ for (let cur = node.parent; cur; cur = cur.parent) {
861
+ if (!ts.isPropertyAssignment(cur))
862
+ continue;
863
+ if (!ts.isIdentifier(cur.name) || cur.name.text !== "children")
864
+ continue;
865
+ if (!ts.isObjectLiteralExpression(cur.parent))
866
+ continue;
867
+ const value = objectLiteralPath(cur.parent, filePath, imports, localConsts, cache);
868
+ if (value === null)
869
+ return null;
870
+ // Nearest ancestor is seen first, so prepend for root→leaf order.
871
+ if (value !== undefined)
872
+ segments.unshift(value);
873
+ }
874
+ let prefix = "";
875
+ for (const segment of segments)
876
+ prefix = composeRoutePath(prefix, segment);
877
+ return prefix;
878
+ }
879
+ /** A route object's `path`: the string it states, `undefined` when it states
880
+ * none (a pathless layout), `null` when it states one that cannot be resolved. */
881
+ function objectLiteralPath(obj, filePath, imports, localConsts, cache) {
882
+ for (const prop of obj.properties) {
883
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
884
+ continue;
885
+ if (prop.name.text !== "path")
886
+ continue;
887
+ return resolveExpressionToString(prop.initializer, filePath, imports, localConsts, cache, 0);
888
+ }
889
+ return undefined;
890
+ }
722
891
  function isInsideDefineModule(node) {
723
892
  let cur = node.parent;
724
893
  while (cur) {
@@ -826,7 +995,38 @@ function createRouterRoutesFromCall(call, filePath, imports, localConsts, cache)
826
995
  const arg = call.arguments[0];
827
996
  if (!arg || !ts.isArrayLiteralExpression(arg))
828
997
  return null;
829
- return dataRouterRoutesFromArray(arg, "", filePath, imports, localConsts, cache);
998
+ const routes = dataRouterRoutesFromArray(arg, "", filePath, imports, localConsts, cache);
999
+ // A route path inside the array is written absolute, so the basename cannot be
1000
+ // threaded through as a prefix — composeRoutePath drops a prefix for an
1001
+ // absolute child. Apply it to the finished paths instead.
1002
+ const basename = basenameOfRouterCall(call);
1003
+ if (!basename)
1004
+ return routes;
1005
+ return routes.map((route) => ({
1006
+ ...route,
1007
+ path: composeRoutePath(basename, route.path.replace(/^\//, "")),
1008
+ }));
1009
+ }
1010
+ /** The `basename` a data-router factory mounts its tree under, from its options
1011
+ * argument: `createBrowserRouter(routes, { basename: "/app" })`. Every URL the
1012
+ * app serves carries it. */
1013
+ function basenameOfRouterCall(call) {
1014
+ const options = call.arguments[1];
1015
+ if (!options || !ts.isObjectLiteralExpression(options))
1016
+ return undefined;
1017
+ for (const prop of options.properties) {
1018
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
1019
+ continue;
1020
+ if (prop.name.text !== "basename")
1021
+ continue;
1022
+ if (!ts.isStringLiteral(prop.initializer))
1023
+ return undefined;
1024
+ const value = prop.initializer.text.replace(/\/+$/, "");
1025
+ if (value === "")
1026
+ return undefined;
1027
+ return value.startsWith("/") ? value : "/" + value;
1028
+ }
1029
+ return undefined;
830
1030
  }
831
1031
  function dataRouterRoutesFromArray(arr, prefix, filePath, imports, localConsts, cache) {
832
1032
  const out = [];
@@ -861,30 +1061,38 @@ function dataRouterRoutesFromArray(arr, prefix, filePath, imports, localConsts,
861
1061
  if (hasPath && pathValue === null)
862
1062
  continue;
863
1063
  let childPrefix = prefix;
1064
+ let ownPath;
864
1065
  if (hasPath) {
865
1066
  childPrefix = composeRoutePath(prefix, pathValue);
866
- if (componentName) {
867
- out.push({
868
- path: childPrefix,
869
- componentName,
870
- componentFile: imports.get(componentName)?.filePath,
871
- declaredIn: filePath,
872
- });
873
- }
1067
+ ownPath = childPrefix;
1068
+ }
1069
+ else if (isIndex) {
1070
+ ownPath = composeRoutePath(prefix, "");
874
1071
  }
875
- else if (isIndex && componentName) {
876
- out.push({
877
- path: composeRoutePath(prefix, ""),
878
- componentName,
879
- componentFile: imports.get(componentName)?.filePath,
1072
+ const childRoutes = childrenArray
1073
+ ? dataRouterRoutesFromArray(childrenArray, childPrefix, filePath, imports, localConsts, cache)
1074
+ : [];
1075
+ if (componentName) {
1076
+ const component = componentName;
1077
+ const mountedAt = (routePath) => ({
1078
+ path: routePath,
1079
+ componentName: component,
1080
+ componentFile: imports.get(component)?.filePath,
880
1081
  declaredIn: filePath,
881
1082
  });
1083
+ if (ownPath !== undefined) {
1084
+ out.push(mountedAt(ownPath));
1085
+ }
1086
+ else {
1087
+ // Pathless layout route: it contributes no URL segment and has none of
1088
+ // its own, so it renders on every route nested under it — reported the
1089
+ // same way a pathless TanStack layout is.
1090
+ for (const routePath of new Set(childRoutes.map((route) => route.path))) {
1091
+ out.push(mountedAt(routePath));
1092
+ }
1093
+ }
882
1094
  }
883
- // Pathless layout route (no path, not index): contributes no URL segment;
884
- // its children inherit the current prefix unchanged.
885
- if (childrenArray) {
886
- out.push(...dataRouterRoutesFromArray(childrenArray, childPrefix, filePath, imports, localConsts, cache));
887
- }
1095
+ out.push(...childRoutes);
888
1096
  }
889
1097
  return out;
890
1098
  }
@@ -2,10 +2,19 @@
2
2
  * The single definition of "discovery treats this path as a test file", shared by
3
3
  * discovery itself and by callers that must agree with what discovery classified as
4
4
  * external coverage. A narrower predicate silently disagrees: `tests/test_orders.py`
5
- * is external coverage here but is not a test file to `isTestFile` in
6
- * scopeAssessment.ts, which only knows the Skyramp and JS/TS spec spellings.
5
+ * is external coverage here but is not a test file to `isTestFile` below, which
6
+ * only knows the Skyramp and JS/TS spec spellings.
7
7
  *
8
8
  * Accepts absolute or repo-relative paths. The directory patterns need a leading
9
9
  * separator, which a repo-relative path has not got, so one is prepended.
10
10
  */
11
11
  export declare function isDiscoveredTestFile(filePath: string): boolean;
12
+ /** The NARROW predicate: a Skyramp-generated test or scenario file, a
13
+ * Vitest/Jest/Playwright spec, or anything under `__tests__/`. Deliberately not
14
+ * `isDiscoveredTestFile`'s rule, which also counts `tests/`, a page-object
15
+ * directory and `test_orders.py`. The maintenance tool wants this narrow one. */
16
+ export declare function isTestFile(filePath: string): boolean;
17
+ /** Whether a path is a test file, by directory or by name. Read by the route
18
+ * checks: a route declared in a mock server or a spec fixture is not a surface
19
+ * anyone serves. */
20
+ export declare function isTestPath(file: string): boolean;
@@ -1,4 +1,11 @@
1
1
  import * as path from "path";
2
+ // Four questions about a path, four predicates, one file. Each is answered by its
3
+ // own rule on purpose, and merging them has broken a caller before:
4
+ // isDiscoveredTestFile — does test DISCOVERY own this file?
5
+ // isTestFile — is it a Skyramp test or a JS/TS spec? (maintenance)
6
+ // isTestPath — can this file DECLARE a route? (route citations)
7
+ // Two more patterns still live elsewhere and are out of scope here:
8
+ // `pr-comment-parser.ts:45` and `test-recommendation-prompt.ts:45`.
2
9
  // Test file naming patterns — match against basename.
3
10
  // Includes Skyramp-generated suffixes so that files like orders_smoke.py placed
4
11
  // outside a recognized test directory are still discovered.
@@ -34,8 +41,8 @@ const TEST_DIR_PATTERNS = [
34
41
  * The single definition of "discovery treats this path as a test file", shared by
35
42
  * discovery itself and by callers that must agree with what discovery classified as
36
43
  * external coverage. A narrower predicate silently disagrees: `tests/test_orders.py`
37
- * is external coverage here but is not a test file to `isTestFile` in
38
- * scopeAssessment.ts, which only knows the Skyramp and JS/TS spec spellings.
44
+ * is external coverage here but is not a test file to `isTestFile` below, which
45
+ * only knows the Skyramp and JS/TS spec spellings.
39
46
  *
40
47
  * Accepts absolute or repo-relative paths. The directory patterns need a leading
41
48
  * separator, which a repo-relative path has not got, so one is prepended.
@@ -45,3 +52,38 @@ export function isDiscoveredTestFile(filePath) {
45
52
  return true;
46
53
  return TEST_DIR_PATTERNS.some((p) => p.test(`/${filePath}`));
47
54
  }
55
+ /** The NARROW predicate: a Skyramp-generated test or scenario file, a
56
+ * Vitest/Jest/Playwright spec, or anything under `__tests__/`. Deliberately not
57
+ * `isDiscoveredTestFile`'s rule, which also counts `tests/`, a page-object
58
+ * directory and `test_orders.py`. The maintenance tool wants this narrow one. */
59
+ export function isTestFile(filePath) {
60
+ return (/(?:_test|_smoke|_contract|_fuzz|_integration|_load|_e2e|_ui)\.[^/]+$/.test(filePath) ||
61
+ /scenario_[^/]+\.json$/.test(filePath) ||
62
+ // Include the `.e2e.` dot form so Playwright/Cypress specs discovery treats
63
+ // as UI tests are recognized here too — e.g. `login.e2e.ts`.
64
+ /\.(spec|test|e2e)\.(tsx?|jsx?)$/.test(filePath) ||
65
+ /(?:^|\/)__tests__\//.test(filePath));
66
+ }
67
+ // ── The route-citation rule ──────────────────────────────────────────────────
68
+ // Its three regexes stay separate from the discovery patterns above. Each one
69
+ // differs deliberately, and the differences are the point — see each comment.
70
+ /** A directory that is a test root whatever the file inside is called. `spec/`
71
+ * and `specs/` are deliberately absent, unlike `TEST_DIR_PATTERNS`: a spec-first
72
+ * service keeps its OpenAPI document there, and that document IS its route
73
+ * definition. */
74
+ const TEST_ROOT_DIR = /(^|[\\/])(tests?|__tests__|__mocks__|e2e|cypress|testing)([\\/]|$)/i;
75
+ /** A file whose own NAME says it is a test, wherever it sits. Wider than the
76
+ * `TEST_FILE_PATTERNS` rows it resembles — any extension, and `.`, `_` or `-`
77
+ * before `test` — so no row above can stand in for it. */
78
+ const TEST_FILE_NAME = /[._-](test|spec)\.[a-z]+$|(^|[\\/])test_[^\\/]*$/i;
79
+ /** The CamelCase spellings, which carry no separator before `Test`.
80
+ * Case-SENSITIVE and must stay so: with `i` it also matches `Latest.java` and
81
+ * `Manifest.cs`, ordinary source files. That is why the case-insensitive
82
+ * `*Test.java` / `*Tests.cs` rows above cannot serve here. */
83
+ const CAMEL_TEST_FILE_NAME = /[a-z0-9](Tests?|IT)\.[A-Za-z0-9]+$/;
84
+ /** Whether a path is a test file, by directory or by name. Read by the route
85
+ * checks: a route declared in a mock server or a spec fixture is not a surface
86
+ * anyone serves. */
87
+ export function isTestPath(file) {
88
+ return TEST_ROOT_DIR.test(file) || TEST_FILE_NAME.test(file) || CAMEL_TEST_FILE_NAME.test(file);
89
+ }