@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
@@ -1,180 +0,0 @@
1
- /**
2
- * Path-affinity classification fallback — SKYR-3857.
3
- *
4
- * `classifyEndpointsByChangedFiles` (routeParsers.ts) matches changed files to
5
- * scanned endpoints by exact `sourceFile` equality. That match structurally fails
6
- * for an entire repository whenever the endpoint catalog carries no usable
7
- * `sourceFile` linkage for its route files — either because the framework isn't
8
- * (yet) handled by `parseFileEndpoints` (the repo scan contributes nothing for
9
- * those files), or because the catalog is predominantly spec-derived
10
- * (`filterEndpointsBySpec` merges in OpenAPI-only entries with `sourceFile: ""`).
11
- * In both cases every changed route file lands in `unmatchedFiles`, `changed`/
12
- * `new` counts stay at 0 for every PR touching that repo, and downstream
13
- * `hasApiChanges` derivation misclassifies genuinely mixed PRs as UI-only (see
14
- * testbot run 26968884237, koljahe/infisical PR #16 — Fastify backend, 1,972
15
- * spec-merged catalog entries, all with `sourceFile: ""`).
16
- *
17
- * This module recovers those files by matching on PATH instead of `sourceFile`:
18
- * for each unmatched, route-like changed file, it derives a resource token from
19
- * the file's own name and looks for catalog endpoints whose path implies the
20
- * same resource — narrowed by a version/prefix conjunction (see
21
- * `deriveVersionSegments`) so a file under `routes/v4/` cannot match a `/v1/`
22
- * or `/v2/` sibling endpoint just because the resource name lines up.
23
- *
24
- * Deliberately conservative: only fired for files `classifyEndpointsByChangedFiles`
25
- * already found nothing for (never overrides a real sourceFile match), and a file
26
- * with no surviving affinity match is left exactly as unmatched as it is today.
27
- * Every entry point here is fail-safe — a per-file error is logged and that file
28
- * is skipped, never thrown to the caller.
29
- *
30
- * KNOWN LIMITATION (conscious first-cut scope): matching is resource-token +
31
- * version/prefix only. It does not corroborate against literal relative-URL
32
- * string evidence in the changed file's content (route files quote their own
33
- * URL suffixes, e.g. `"/:secretName"`) — the token+version conjunction already
34
- * narrows precisely enough for the motivating case, and content-based
35
- * corroboration adds real complexity (multi-language quoting, template-literal
36
- * interpolation) for a marginal precision gain. Left as a follow-up if
37
- * real-world false positives surface.
38
- *
39
- * SKYR-3857 consolidation: both halves of the token matching are shared with
40
- * `importerHop.ts` rather than reimplemented here — `deriveResourceTokensForFile`
41
- * for the file side (which also gives this fallback symbol-based affinity: a
42
- * declared class/interface/schema name on an added diff line can anchor a match
43
- * for route files whose filename alone is too generic, e.g.
44
- * `routes/v4/index-router.ts`) and `endpointResourceTokens` for the endpoint
45
- * side. The version vocabulary comes from `isVersionLikeSegment` in
46
- * `routeParsers.ts`, next to the `SKIP_PATH_SEGMENTS` set it extends.
47
- */
48
- import { logger } from "./logger.js";
49
- import { isRouteLikeFile, isVersionLikeSegment } from "./routeParsers.js";
50
- import { isFrontendFile, isTestFile } from "../prompts/test-recommendation/scopeAssessment.js";
51
- import { deriveResourceTokensForFile, endpointResourceTokens, sliceAddedLinesByFile } from "./importerHop.js";
52
- /** Default cap on endpoints classified per unmatched file — keeps a broad/wrong
53
- * resource-token match from flooding the change set. */
54
- const DEFAULT_MAX_ENDPOINTS_PER_FILE = 15;
55
- /** Version/prefix directory segments ("v4", "api", ...) present in a changed
56
- * file's own path — required, as a conjunction, in any matching endpoint path.
57
- * Prevents a `routes/v4/secret-router.ts` change from matching a `/api/v1/*`
58
- * sibling endpoint on resource-token alone. */
59
- export function deriveVersionSegments(file) {
60
- const normalized = file.replace(/\\/g, "/");
61
- const segments = normalized.split("/").filter(Boolean);
62
- // Exclude the basename — the resource token is derived from it separately,
63
- // and a hyphenated basename (e.g. "secret-v4-router.ts") isn't a directory.
64
- const dirSegments = segments.slice(0, -1);
65
- return new Set(dirSegments.filter(isVersionLikeSegment).map((s) => s.toLowerCase()));
66
- }
67
- /** Segments of an endpoint path, lowercased, for the version-conjunction check. */
68
- function endpointPathSegments(endpointPath) {
69
- return new Set(endpointPath.split("/").filter(Boolean).map((s) => s.toLowerCase()));
70
- }
71
- /**
72
- * Maps changed files that `classifyEndpointsByChangedFiles` could not match by
73
- * `sourceFile` (its `unmatchedFiles`) to catalog endpoints that plausibly belong
74
- * to the same REST resource, purely from the file's own path and the endpoint's
75
- * own path — no `sourceFile` needed on either side. Intended to run against the
76
- * *final* endpoint catalog (after any OpenAPI spec merge), since a catalog with
77
- * no source-scan coverage for a file is, by definition, only useful for this
78
- * fallback once spec-derived entries are present to match against.
79
- *
80
- * `alreadyClassifiedEndpoints` (typically `changedEndpoints` + `newEndpoints`
81
- * from the same classification pass) is used only for dedup — never duplicates
82
- * an endpoint already classified directly.
83
- *
84
- * `diffContent`, when supplied, is the full unified diff — sliced per-file
85
- * (via `sliceAddedLinesByFile`, shared with `mapUnmatchedFilesToImporterEndpoints`)
86
- * and passed to `deriveResourceTokensForFile` so a route file whose filename
87
- * alone is too generic to anchor a match (e.g. `routes/v4/index-router.ts`)
88
- * can still match via a symbol declared on its added diff lines. Without it,
89
- * matching falls back to the filename-only token, identical to pre-symbol-
90
- * affinity behavior.
91
- *
92
- * `removedEndpoints` (the classifier's removal result, recovered from the base
93
- * tree) is excluded from the candidate set BY PATH+METHOD, not via the
94
- * sourceFile-keyed dedup: removal is classified at Step 2.5 against the
95
- * pre-spec-merge catalog, but this fallback matches the post-spec-merge
96
- * catalog — where a stale checked-in OpenAPI spec can still list a deleted
97
- * endpoint as a `sourceFile: ""` entry. Without this guard, a sibling
98
- * unmatched file sharing the resource token would resurrect the removed
99
- * endpoint as "modified", contradicting the removal classification. A `MULTI`
100
- * removed method covers every method on that path; a candidate survives if
101
- * any of its methods is not removed.
102
- *
103
- * Matched endpoints are always attributed as MODIFIED (never new/removed) —
104
- * path affinity says an endpoint is *related* to the change, not that the
105
- * relationship is an addition or removal.
106
- */
107
- export function classifyUnmatchedFilesByPathAffinity(unmatchedFiles, scannedEndpoints, alreadyClassifiedEndpoints = [], maxEndpointsPerFile = DEFAULT_MAX_ENDPOINTS_PER_FILE, diffContent, removedEndpoints = []) {
108
- const endpoints = [];
109
- const mappedFiles = [];
110
- const eligibleFiles = unmatchedFiles.filter((file) => !isFrontendFile(file) && !isTestFile(file) && isRouteLikeFile(file));
111
- if (eligibleFiles.length === 0)
112
- return { endpoints, mappedFiles };
113
- const alreadyKeys = new Set(alreadyClassifiedEndpoints.map((ep) => `${ep.sourceFile}::${ep.path}`));
114
- const addedKeys = new Set();
115
- const addedLinesByFile = diffContent ? sliceAddedLinesByFile(diffContent) : undefined;
116
- const removedMethodKeys = new Set();
117
- const removedMultiPaths = new Set();
118
- for (const ep of removedEndpoints) {
119
- for (const method of ep.methods) {
120
- if (method === "MULTI")
121
- removedMultiPaths.add(ep.path);
122
- else
123
- removedMethodKeys.add(`${method}::${ep.path}`);
124
- }
125
- }
126
- const isRemoved = (ep) => removedMultiPaths.has(ep.path) ||
127
- (ep.methods.length > 0 && ep.methods.every((m) => removedMethodKeys.has(`${m}::${ep.path}`)));
128
- // Endpoint-side tokens/segments depend only on the endpoint's own path —
129
- // derive them once up front rather than per (file, endpoint) pair (the
130
- // motivating spec-merged catalogs run to thousands of entries).
131
- const endpointIndex = scannedEndpoints
132
- .filter((ep) => !isRemoved(ep))
133
- .map((ep) => ({
134
- ep,
135
- tokens: endpointResourceTokens(ep.path),
136
- segments: endpointPathSegments(ep.path),
137
- }));
138
- for (const file of eligibleFiles) {
139
- try {
140
- const fileAddedLines = addedLinesByFile?.get(file);
141
- const diffHunk = fileAddedLines && fileAddedLines.length > 0 ? fileAddedLines.join("\n") : undefined;
142
- const resourceTokens = deriveResourceTokensForFile(file, diffHunk);
143
- if (resourceTokens.length === 0)
144
- continue;
145
- const requiredVersionSegments = deriveVersionSegments(file);
146
- const candidates = endpointIndex.filter(({ tokens, segments }) => {
147
- if (!resourceTokens.some((token) => tokens.has(token)))
148
- return false;
149
- if (requiredVersionSegments.size === 0)
150
- return true;
151
- return [...requiredVersionSegments].every((seg) => segments.has(seg));
152
- });
153
- if (candidates.length === 0)
154
- continue;
155
- const capped = candidates.slice(0, maxEndpointsPerFile);
156
- for (const { ep } of capped) {
157
- const key = `${ep.sourceFile}::${ep.path}`;
158
- if (alreadyKeys.has(key) || addedKeys.has(key))
159
- continue;
160
- addedKeys.add(key);
161
- endpoints.push(ep);
162
- }
163
- mappedFiles.push(file);
164
- logger.info("Path-affinity fallback classified endpoints for sourceFile-less file", {
165
- file,
166
- resourceTokens,
167
- versionSegments: [...requiredVersionSegments],
168
- matched: capped.length,
169
- truncated: candidates.length > capped.length,
170
- });
171
- }
172
- catch (err) {
173
- logger.warning("Path-affinity classification failed for file — skipping", {
174
- file,
175
- error: err instanceof Error ? err.message : String(err),
176
- });
177
- }
178
- }
179
- return { endpoints, mappedFiles };
180
- }
@@ -1,25 +0,0 @@
1
- /** Repo-relative POSIX path of a Python source file. */
2
- export type RelFile = string;
3
- /** A router variable name as written in source (e.g. "router"). */
4
- export type RouterVar = string;
5
- /** The ancestor mount prefix a router inherits from its include_router chain (e.g. "/api"). */
6
- export type MountPrefix = string;
7
- /**
8
- * `sourceFile → (routerVar → ancestor mount prefix)`, produced by
9
- * `resolvePythonMountPrefixes`. Only routers that inherit a non-empty ancestor
10
- * prefix appear; everything else is absent (single-file parsing is unchanged).
11
- */
12
- export type MountPrefixesByFile = Map<RelFile, Map<RouterVar, MountPrefix>>;
13
- /**
14
- * Join a prefix and a child path segment with exactly one slash, tolerating
15
- * leading/trailing slashes on either side. Empty child → the prefix unchanged;
16
- * empty prefix → the child made absolute.
17
- */
18
- export declare function joinRoutePrefix(prefix: string, child: string): string;
19
- /**
20
- * Build `sourceFile → (routerVar → ancestorMountPrefix)` for every FastAPI
21
- * router reachable through an `include_router` chain. Only non-empty ancestor
22
- * prefixes are returned, so files whose routers sit at the root are absent and
23
- * fall back to unchanged single-file parsing.
24
- */
25
- export declare function resolvePythonMountPrefixes(repositoryPath: string): MountPrefixesByFile;
@@ -1,347 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import { walkSourceFiles } from "./fileWalk.js";
4
- import { logger } from "./logger.js";
5
- /**
6
- * Cross-file FastAPI mount-prefix resolution.
7
- *
8
- * FastAPI composes a route's full URL from prefixes applied at three levels:
9
- * 1. the router's own `APIRouter(prefix=…)` (captured per-file by
10
- * `parseFileEndpoints`), and
11
- * 2. every `include_router(child, prefix=…)` / `app.include_router(child, …)`
12
- * mount point the router passes through — which routinely live in *other*
13
- * files (an `app.py` / `routes/__init__.py` aggregator).
14
- *
15
- * The single-file route parser can only see (1), so a leaf route declared as
16
- * `@router.get("")` on a `router = APIRouter(prefix="/recipes")` that an
17
- * aggregator mounts under `APIRouter(prefix="/api")` was emitted as `/recipes`
18
- * instead of `/api/recipes` — generated tests then hit the un-prefixed URL and
19
- * 405. (Seen on Mealie's endpoint catalog.)
20
- *
21
- * This module does a repo-level pre-pass: it parses every Python file for router
22
- * declarations, `include_router` mount edges, and the intra-repo imports that
23
- * connect them, then walks the mount graph to compute, for each router, the
24
- * **ancestor** prefix it inherits from its mount chain (NOT including the
25
- * router's own prefix, which `parseFileEndpoints` still applies). The result is
26
- * keyed by `sourceFile → routerVarName → ancestorPrefix`.
27
- *
28
- * Best-effort by design (mirrors the rest of the extractor): an import that
29
- * can't be resolved simply leaves that router as a graph root with an empty
30
- * ancestor prefix — i.e. it falls back to today's single-file behavior rather
31
- * than emitting a wrong path.
32
- */
33
- /** Skip these directory names when walking for Python sources. */
34
- const SKIP_DIRS = new Set([
35
- "node_modules", ".git", "dist", "build", "out",
36
- "venv", ".venv", "env", ".env", "__pycache__", ".tox",
37
- "site-packages", ".mypy_cache", ".pytest_cache", "coverage",
38
- ]);
39
- /** Cap on Python files parsed — a backstop against pathological repos. */
40
- const MAX_PY_FILES = 5000;
41
- /** Guard against cycles / runaway mount chains. */
42
- const MAX_MOUNT_DEPTH = 32;
43
- function nodeId(relFile, varName) {
44
- return `${relFile}\n${varName}`;
45
- }
46
- /**
47
- * Join a prefix and a child path segment with exactly one slash, tolerating
48
- * leading/trailing slashes on either side. Empty child → the prefix unchanged;
49
- * empty prefix → the child made absolute.
50
- */
51
- export function joinRoutePrefix(prefix, child) {
52
- const left = prefix.replace(/\/+$/, "");
53
- const right = child.replace(/^\/+/, "");
54
- if (!right)
55
- return left;
56
- if (!left)
57
- return "/" + right;
58
- return `${left}/${right}`;
59
- }
60
- function normalizeRel(p) {
61
- return p.replace(/\\/g, "/");
62
- }
63
- /** Recursively collect `.py` files, returning repo-relative (posix) paths. */
64
- function collectPyFiles(repositoryPath) {
65
- const abs = walkSourceFiles(repositoryPath, {
66
- extensions: [".py"],
67
- skipDirs: SKIP_DIRS,
68
- limit: MAX_PY_FILES,
69
- });
70
- if (abs.length >= MAX_PY_FILES) {
71
- logger.warning("resolvePythonMountPrefixes: hit MAX_PY_FILES cap — some Python files were not scanned, so cross-file mount prefixes may be under-resolved on this repo", { cap: MAX_PY_FILES });
72
- }
73
- return abs.map((full) => normalizeRel(full.startsWith(repositoryPath) ? full.slice(repositoryPath.length + 1) : full));
74
- }
75
- /**
76
- * Merge physical lines into logical lines, joining any line whose parentheses
77
- * are left open (multi-line `APIRouter(`, `include_router(`, and
78
- * `from x import ( … )`). String contents are not specially handled — a stray
79
- * paren inside a string literal is a rare edge that only degrades to a missed
80
- * prefix (fail-safe).
81
- */
82
- function toLogicalLines(content) {
83
- const logical = [];
84
- let buf = "";
85
- let depth = 0;
86
- for (const raw of content.split("\n")) {
87
- const line = raw.replace(/#.*$/, ""); // drop trailing comments
88
- buf = buf ? `${buf} ${line.trim()}` : line;
89
- for (const ch of line) {
90
- if (ch === "(" || ch === "[" || ch === "{")
91
- depth++;
92
- else if (ch === ")" || ch === "]" || ch === "}")
93
- depth = Math.max(0, depth - 1);
94
- }
95
- if (depth === 0) {
96
- logical.push(buf);
97
- buf = "";
98
- }
99
- }
100
- if (buf)
101
- logical.push(buf);
102
- return logical;
103
- }
104
- /**
105
- * Resolve an intra-repo Python module reference to a source file in `fileSet`.
106
- * `dots` is the leading-dot count (0 = absolute), `modulePath` the dotted
107
- * remainder (e.g. `mealie.routes` or `recipe`). Returns the repo-relative file
108
- * path or null when it can't be resolved to a known file.
109
- */
110
- function resolveModuleFile(fromRel, dots, modulePath, fileSet) {
111
- const segments = modulePath ? modulePath.split(".").filter(Boolean) : [];
112
- const candidates = [];
113
- if (dots > 0) {
114
- // Relative import: walk up `dots` directories from the current file's dir.
115
- let base = path.posix.dirname(fromRel);
116
- for (let i = 1; i < dots; i++)
117
- base = path.posix.dirname(base);
118
- const joined = [base === "." ? "" : base, ...segments].filter(Boolean).join("/");
119
- candidates.push(`${joined}.py`, `${joined}/__init__.py`);
120
- }
121
- else {
122
- // Absolute import: resolve from repo root, but also suffix-match so `src/`
123
- // (or other package-root) layouts resolve too.
124
- const joined = segments.join("/");
125
- candidates.push(`${joined}.py`, `${joined}/__init__.py`);
126
- }
127
- for (const c of candidates) {
128
- if (fileSet.has(c))
129
- return c;
130
- }
131
- // Suffix fallback for absolute imports under a package root (e.g. src/).
132
- if (dots === 0) {
133
- const joined = segments.join("/");
134
- const suffixes = [`/${joined}.py`, `/${joined}/__init__.py`];
135
- for (const f of fileSet) {
136
- if (suffixes.some((s) => f.endsWith(s)))
137
- return f;
138
- }
139
- }
140
- return null;
141
- }
142
- const ROUTER_DECL_RE = /^(\w+)\s*=\s*\w*APIRouter\s*\(/;
143
- const PREFIX_KW_RE = /(?:^|[,(\s])prefix\s*=\s*["']([^"']*)["']/;
144
- const CONST_RE = /^(\w+)\s*=\s*["']([^"']*)["']\s*$/;
145
- const IMPORT_FROM_RE = /^from\s+(\.*)([\w.]*)\s+import\s+(.+)$/;
146
- const IMPORT_RE = /^import\s+([\w.]+)(?:\s+as\s+(\w+))?/;
147
- const INCLUDE_ROUTER_RE = /(\w+)\.include_router\s*\(\s*([\w.]+)\s*(?:,(.*))?\)/;
148
- function parseImportedNames(blob) {
149
- return blob
150
- .replace(/[()]/g, "")
151
- .split(",")
152
- .map((s) => s.trim())
153
- .filter(Boolean)
154
- .map((tok) => {
155
- const m = tok.match(/^(\w+)(?:\s+as\s+(\w+))?$/);
156
- if (!m)
157
- return null;
158
- return { name: m[1], alias: m[2] ?? m[1] };
159
- })
160
- .filter((x) => x !== null);
161
- }
162
- function collectFileFacts(relFile, content, fileSet) {
163
- const facts = {
164
- ownPrefixes: new Map(),
165
- stringConsts: new Map(),
166
- moduleAlias: new Map(),
167
- symbolAlias: new Map(),
168
- mounts: [],
169
- };
170
- for (const line of toLogicalLines(content)) {
171
- const trimmed = line.trim();
172
- if (!trimmed)
173
- continue;
174
- // Router declaration + its own prefix.
175
- const decl = trimmed.match(ROUTER_DECL_RE);
176
- if (decl) {
177
- const pm = trimmed.match(PREFIX_KW_RE);
178
- facts.ownPrefixes.set(decl[1], pm ? pm[1] : "");
179
- // fall through: a declaration line has no include_router / import.
180
- }
181
- // Module-level string const (for prefix=CONST resolution).
182
- const cm = trimmed.match(CONST_RE);
183
- if (cm)
184
- facts.stringConsts.set(cm[1], cm[2]);
185
- // `from <module> import <names>`
186
- const fromM = trimmed.match(IMPORT_FROM_RE);
187
- if (fromM) {
188
- const dots = fromM[1].length;
189
- const modulePath = fromM[2];
190
- const names = parseImportedNames(fromM[3]);
191
- if (modulePath) {
192
- // Symbols imported FROM a specific module (e.g. `from .x import router`).
193
- const modFile = resolveModuleFile(relFile, dots, modulePath, fileSet);
194
- if (modFile) {
195
- for (const { name, alias } of names) {
196
- facts.symbolAlias.set(alias, { file: modFile, varName: name });
197
- }
198
- }
199
- }
200
- else if (dots > 0) {
201
- // `from . import a, b` — each name is a submodule.
202
- for (const { name, alias } of names) {
203
- const modFile = resolveModuleFile(relFile, dots, name, fileSet);
204
- if (modFile)
205
- facts.moduleAlias.set(alias, modFile);
206
- }
207
- }
208
- continue;
209
- }
210
- // `import a.b.c as d`
211
- const impM = trimmed.match(IMPORT_RE);
212
- if (impM) {
213
- const modFile = resolveModuleFile(relFile, 0, impM[1], fileSet);
214
- if (modFile) {
215
- const alias = impM[2] ?? impM[1].split(".")[0];
216
- facts.moduleAlias.set(alias, modFile);
217
- }
218
- continue;
219
- }
220
- // include_router mount edges (a line may contain several).
221
- let m;
222
- const re = new RegExp(INCLUDE_ROUTER_RE, "g");
223
- while ((m = re.exec(trimmed)) !== null) {
224
- const prefixExpr = extractMountPrefixExpr(m[3] ?? "");
225
- facts.mounts.push({ parentVar: m[1], arg: m[2], prefixExpr });
226
- }
227
- }
228
- return facts;
229
- }
230
- /** Pull the `prefix=` argument (literal or bare identifier) out of the call tail. */
231
- function extractMountPrefixExpr(argsTail) {
232
- const lit = argsTail.match(/prefix\s*=\s*["']([^"']*)["']/);
233
- if (lit)
234
- return JSON.stringify(lit[1]); // quote to mark it a literal
235
- const ident = argsTail.match(/prefix\s*=\s*(\w+)/);
236
- if (ident)
237
- return ident[1]; // bare identifier — resolve against stringConsts
238
- return null;
239
- }
240
- /** Resolve an `include_router` first-argument expression to a router NodeId. */
241
- function resolveMountArg(arg, relFile, facts) {
242
- const parts = arg.split(".");
243
- if (parts.length === 1) {
244
- // Bare name: an imported symbol, else a router declared in this file.
245
- const sym = facts.symbolAlias.get(parts[0]);
246
- if (sym)
247
- return nodeId(sym.file, sym.varName);
248
- return nodeId(relFile, parts[0]);
249
- }
250
- // `mod.router` — module alias + attribute.
251
- const modFile = facts.moduleAlias.get(parts[0]);
252
- if (modFile)
253
- return nodeId(modFile, parts[parts.length - 1]);
254
- return null;
255
- }
256
- /**
257
- * Build `sourceFile → (routerVar → ancestorMountPrefix)` for every FastAPI
258
- * router reachable through an `include_router` chain. Only non-empty ancestor
259
- * prefixes are returned, so files whose routers sit at the root are absent and
260
- * fall back to unchanged single-file parsing.
261
- */
262
- export function resolvePythonMountPrefixes(repositoryPath) {
263
- const pyFiles = collectPyFiles(repositoryPath);
264
- const result = new Map();
265
- if (pyFiles.length === 0)
266
- return result;
267
- const fileSet = new Set(pyFiles);
268
- const factsByFile = new Map();
269
- const ownPrefixByNode = new Map();
270
- const edges = [];
271
- for (const rel of pyFiles) {
272
- let content;
273
- try {
274
- content = fs.readFileSync(path.join(repositoryPath, rel), "utf-8");
275
- }
276
- catch {
277
- continue;
278
- }
279
- // Cheap gate: only parse files that actually wire routers together or
280
- // declare one — avoids tokenizing the whole repo's Python.
281
- if (!/include_router|APIRouter/.test(content))
282
- continue;
283
- const facts = collectFileFacts(rel, content, fileSet);
284
- factsByFile.set(rel, facts);
285
- for (const [varName, prefix] of facts.ownPrefixes) {
286
- ownPrefixByNode.set(nodeId(rel, varName), prefix);
287
- }
288
- }
289
- // Second pass: resolve mount edges now that every file's facts (imports,
290
- // consts, router vars) are known.
291
- for (const [rel, facts] of factsByFile) {
292
- for (const mount of facts.mounts) {
293
- const child = resolveMountArg(mount.arg, rel, facts);
294
- if (!child)
295
- continue;
296
- let mountPrefix = "";
297
- if (mount.prefixExpr !== null) {
298
- if (mount.prefixExpr.startsWith('"')) {
299
- mountPrefix = JSON.parse(mount.prefixExpr);
300
- }
301
- else {
302
- mountPrefix = facts.stringConsts.get(mount.prefixExpr) ?? "";
303
- }
304
- }
305
- edges.push({ parent: nodeId(rel, mount.parentVar), child, mountPrefix });
306
- }
307
- }
308
- // First incoming edge wins (a router mounted in two places is rare; pick one
309
- // deterministically rather than guessing a "primary" mount).
310
- const incoming = new Map();
311
- for (const e of edges) {
312
- if (!incoming.has(e.child))
313
- incoming.set(e.child, e);
314
- }
315
- const memo = new Map();
316
- function ancestorPrefix(node, depth) {
317
- if (depth > MAX_MOUNT_DEPTH)
318
- return "";
319
- const cached = memo.get(node);
320
- if (cached !== undefined)
321
- return cached;
322
- memo.set(node, ""); // cycle guard: treat as root until resolved
323
- const inc = incoming.get(node);
324
- if (!inc)
325
- return ""; // root — no inherited prefix
326
- const parentAncestor = ancestorPrefix(inc.parent, depth + 1);
327
- const parentOwn = ownPrefixByNode.get(inc.parent) ?? "";
328
- const full = joinRoutePrefix(joinRoutePrefix(parentAncestor, parentOwn), inc.mountPrefix);
329
- memo.set(node, full);
330
- return full;
331
- }
332
- for (const node of ownPrefixByNode.keys()) {
333
- const prefix = ancestorPrefix(node, 0);
334
- if (!prefix)
335
- continue; // root router — leave to single-file parsing
336
- const sep = node.indexOf("\n");
337
- const relFile = node.slice(0, sep);
338
- const varName = node.slice(sep + 1);
339
- let perFile = result.get(relFile);
340
- if (!perFile) {
341
- perFile = new Map();
342
- result.set(relFile, perFile);
343
- }
344
- perFile.set(varName, prefix);
345
- }
346
- return result;
347
- }
@@ -1,34 +0,0 @@
1
- export interface ScannedEndpoint {
2
- path: string;
3
- methods: string[];
4
- sourceFile: string;
5
- /**
6
- * 1-based line number of the route's declaration in `sourceFile`, when known.
7
- * Used by hunk-aware diff scoping to attribute a changed line to the single
8
- * route that owns it (rather than flagging every route in a touched file).
9
- * Optional — absence disables scoping and falls back to file-level matching.
10
- */
11
- line?: number;
12
- }
13
- /**
14
- * Returns the relative paths of entry-point files that define the routing
15
- * hierarchy for this repository. The LLM reads these files in Step 1.5 to
16
- * trace router/module nesting and build an authoritative path resolution table.
17
- *
18
- * Intentionally returns only file paths — no extracted lines. Static regex
19
- * extraction is fragile and framework-specific; the LLM reads the files directly
20
- * and understands any framework's routing syntax.
21
- */
22
- export declare function grepRouterMountingContext(repositoryPath: string): string[];
23
- /**
24
- * Return relative paths of all source files that are likely to define API
25
- * routes. Two-pass strategy:
26
- * 1. Fast: filename matches ROUTE_FILE_PATTERN (no file I/O needed).
27
- * 2. Content: files not matched by name are read and checked for both a
28
- * URL-path literal AND an HTTP method registration. This covers any
29
- * framework the filename filter misses — Axum, Ktor, .NET, Hono, etc.
30
- * Result is capped at MAX_CANDIDATE_FILES to keep the LLM prompt bounded.
31
- */
32
- export declare function findCandidateRouteFiles(repositoryPath: string): string[];
33
- export declare function scanAllRepoEndpoints(repositoryPath: string): ScannedEndpoint[];
34
- export declare function scanRelatedEndpoints(repositoryPath: string, changedFiles: string[]): ScannedEndpoint[];