@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,489 +0,0 @@
1
- /**
2
- * Importer-hop endpoint classification — SKYR-3855 (backend half).
3
- *
4
- * `classifyEndpointsByChangedFiles` (routeParsers.ts) matches changed files to
5
- * scanned endpoints by exact `sourceFile` equality. A changed DTO/service/model
6
- * file — a non-route backend file that a route handler *imports* rather than
7
- * declares routes in — matches no endpoint and lands in `unmatchedFiles`. That
8
- * silently drops the change from `diffContext.modifiedEndpoints`, which makes
9
- * every drafted scenario classify as `novelty: existing` and the ranked plan
10
- * never mentions the actual change (see testbot run 27468308525 — immich PR
11
- * adding a derived `initials` field to `user.dto.ts`).
12
- *
13
- * This module "hops" one import edge: for each unmatched backend file, it
14
- * finds already-scanned endpoints whose source file textually imports the
15
- * changed file (via the same path-reference signature used by
16
- * `frontendIntegration.ts`), then narrows to endpoints that plausibly belong
17
- * to the same REST resource as the changed file (the "resource-affinity"
18
- * filter) — critical containment so a widely-imported DTO (e.g. immich's
19
- * `user.dto.ts`, imported by nearly every controller) doesn't flag the whole
20
- * endpoint catalog. If no candidate survives the affinity filter, the file is
21
- * left unmapped — conservative by design, matching the pre-existing
22
- * "unmatched" behavior rather than guessing.
23
- *
24
- * Every entry point here is fail-safe: any error for a given file is logged
25
- * and that file is simply skipped, never thrown to the caller.
26
- *
27
- * KNOWN LIMITATIONS (conscious first-cut scope — both no-op conservatively,
28
- * leaving classification exactly as it was pre-hop):
29
- * 1. Import edge is TS/JS path-quoted imports only. Python/Go/Java dotted or
30
- * package imports produce zero signature hits (e.g. FastAPI's
31
- * `schemas.actions.DeploymentCreate`). Language-aware signatures tracked
32
- * in SKYR-3921's lane. Symbol-based affinity (see point 2) widens which
33
- * candidates PASS the filter once an edge fires — it does not create new
34
- * import-edge candidates, so this limitation stands on its own for
35
- * Python/Go/Java until SKYR-3921/3929 lands.
36
- * 2. MITIGATED: affinity used to be FILENAME-only, so generic multi-DTO
37
- * modules defeated it even when the edge fired: `schemas/actions.py`
38
- * holding `DeploymentCreate` derived token "action", never matching
39
- * `/deployments`. `deriveResourceTokensForFile` now additionally derives
40
- * tokens from the changed SYMBOL names in the diff hunk
41
- * (`class DeploymentCreate` -> "deployment"), and the affinity filter
42
- * passes if an endpoint matches ANY derived token. Shared with the
43
- * maintenance path — SKYR-3924 consumes `deriveResourceTokensForFile`
44
- * directly for its own "changed DTO -> affected resource" signal, so the
45
- * derivation lives here once rather than being duplicated.
46
- *
47
- * SKYR-3857 consolidation: pathAffinityClassification.ts's fallback used to
48
- * carry its own, basename-only `deriveFileResourceToken` derivation. It now
49
- * consumes `deriveResourceTokensForFile` directly (gaining symbol-based
50
- * affinity for route files for free), so this is the ONLY filename/symbol to
51
- * resource-token derivation in the codebase. `deriveResourceToken` here
52
- * additionally strips the route-ish filename suffixes ("-router"/"-routes"/
53
- * ".controller"/etc.) that module used to handle on its own, and
54
- * `GENERIC_RESOURCE_TOKENS` absorbed its generic-token vocabulary.
55
- */
56
- import * as fs from "fs";
57
- import * as path from "path";
58
- import { logger } from "./logger.js";
59
- import { extractResourceFromPath, isParamSegment, isVersionLikeSegment, SKIP_PATH_SEGMENTS } from "./routeParsers.js";
60
- import { sliceAddedLinesByFile } from "./branchDiff.js";
61
- import { buildPathSignatures, importsIndexModule, isIndexModule } from "./pathSignatures.js";
62
- import { singularize } from "./pluralization.js";
63
- import { isFrontendFile, isTestFile } from "../prompts/test-recommendation/scopeAssessment.js";
64
- const MAX_FILE_SIZE_BYTES = 512 * 1024;
65
- // Mirrors the small `SOURCE_EXTS` copies already living in routeParsers.ts and
66
- // analyzeChangesTool.ts — kept local rather than shared so each domain stays
67
- // independently tunable (same precedent as frontendIntegration.ts's own
68
- // FRONTEND_SOURCE_EXT_PATTERN, which is deliberately not shared either).
69
- const SOURCE_EXTS = /\.(ts|tsx|js|jsx|py|java|kt|go|rb|php|rs|cs|ex|exs)$/;
70
- // Common backend "framework layer" suffixes stripped before deriving a resource
71
- // token from a source file name — e.g. "user.dto.ts" -> "user",
72
- // "order.service.ts" -> "order", "UsersRepository.ts" handled via extension only.
73
- const FRAMEWORK_SUFFIX_RE = /\.(dto|service|controller|repository|entity|module|schema|model|resolver|gateway|guard|middleware|interceptor|mapper|serializer)$/i;
74
- // Route-ish suffix forms, merged in from pathAffinityClassification.ts's former
75
- // `ROUTE_FILE_SUFFIX_RE` during the SKYR-3857 derivation consolidation — e.g.
76
- // "secret-router.ts" -> "secret", "orders-routes.js" -> "order". Hyphen, dot,
77
- // or underscore delimited, since route-file naming conventions vary by
78
- // framework (Fastify favors hyphens, Nest/Angular favor dots). Applied after
79
- // FRAMEWORK_SUFFIX_RE in `deriveResourceToken` below; both regexes are
80
- // end-anchored, so together they only ever strip within the final path
81
- // segment — directory names are never touched.
82
- const ROUTE_SUFFIX_RE = /[-_.](?:router|routes?|controllers?|handlers?|endpoints?|resource|resources|api)$/i;
83
- // Resource tokens too generic to safely anchor the affinity filter — matching on
84
- // these would defeat its purpose (e.g. a shared base.service.ts imported everywhere).
85
- const GENERIC_RESOURCE_TOKENS = new Set([
86
- "base", "common", "shared", "abstract", "index", "types", "utils", "helpers",
87
- "constants", "config", "settings", "app", "main", "server", "core",
88
- // Merged from pathAffinityClassification.ts's former GENERIC_RESOURCE_TOKENS
89
- // (SKYR-3857 derivation consolidation) — route-file vocabulary that is
90
- // structural, not resource-identifying, e.g. a bare "router.ts" or "api.ts".
91
- "route", "router", "routes", "controller", "controllers", "handler",
92
- "handlers", "endpoint", "endpoints", "api", "service", "gateway",
93
- "resolver", "middleware", "resource", "resources",
94
- ]);
95
- // Sibling to GENERIC_RESOURCE_TOKENS, for words split out of a declared SYMBOL
96
- // name rather than a filename — DTO/action/framework vocabulary that appears
97
- // inside class/type names constantly (e.g. "DeploymentCreate", "UserRequest")
98
- // but never identifies the resource itself. Checked in addition to
99
- // GENERIC_RESOURCE_TOKENS (a word generic enough to reject as a filename token
100
- // is equally generic as a symbol-word token).
101
- const GENERIC_SYMBOL_TOKENS = new Set([
102
- "create", "update", "delete", "request", "response", "dto", "schema",
103
- "input", "output", "type", "base", "params", "options", "config", "error",
104
- ]);
105
- // Caps the token list `deriveResourceTokensForFile` returns so a single huge
106
- // schema/module file (many declared symbols) can't blow up the affinity
107
- // filter into matching nearly everything.
108
- const MAX_RESOURCE_TOKENS = 8;
109
- // Declared-symbol forms recognized on an ADDED diff line (leading "+"), in
110
- // the languages importer-hop already targets. Best-effort regex, not a
111
- // parser — mirrors the precedent set by TS_FIELD_RE/PY_FIELD_RE below. Method
112
- // and function declarations never match any of these (no "class"/"interface"/
113
- // "type"/"enum" keyword), so `getUsers(): User[] {` is correctly ignored.
114
- const TS_SYMBOL_DECL_RES = [
115
- /^\+\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z_]\w*)/,
116
- /^\+\s*(?:export\s+)?interface\s+([A-Za-z_]\w*)/,
117
- /^\+\s*(?:export\s+)?type\s+([A-Za-z_]\w*)\s*=/,
118
- /^\+\s*(?:export\s+)?(?:const\s+)?enum\s+([A-Za-z_]\w*)/,
119
- // "schema-ish" export const — e.g. a zod DTO: `export const UserSchema = z.object({...})`.
120
- /^\+\s*export\s+const\s+([A-Za-z_]\w*)\s*=\s*z\./,
121
- ];
122
- const PY_SYMBOL_DECL_RE = /^\+\s*class\s+([A-Za-z_]\w*)/;
123
- /**
124
- * Extracts declared class/interface/type/enum/schema symbol names from an
125
- * array of raw added diff lines (each still prefixed with the unified-diff
126
- * `+` marker). Sibling to `extractFieldNamesFromAddedLines` — same
127
- * best-effort, non-parser contract — but looks for type-level declarations
128
- * instead of field/property assignments.
129
- */
130
- function extractDeclaredSymbolsFromAddedLines(lines) {
131
- const symbols = new Set();
132
- for (const line of lines) {
133
- if (line.startsWith("++"))
134
- continue; // "+++ b/file" diff header line
135
- let name;
136
- for (const re of TS_SYMBOL_DECL_RES) {
137
- const match = re.exec(line);
138
- if (match) {
139
- name = match[1];
140
- break;
141
- }
142
- }
143
- if (!name)
144
- name = PY_SYMBOL_DECL_RE.exec(line)?.[1];
145
- if (name)
146
- symbols.add(name);
147
- }
148
- return [...symbols];
149
- }
150
- /** Splits a CamelCase/PascalCase/snake_case symbol name into lowercase words —
151
- * "DeploymentCreate" -> ["Deployment", "Create"], "secret_version" -> ["secret", "version"],
152
- * "HTTPServer" -> ["HTTP", "Server"] (acronym-aware boundary). */
153
- function splitSymbolIntoWords(symbol) {
154
- const withBoundaries = symbol
155
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
156
- .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
157
- .replace(/_/g, " ");
158
- return withBoundaries.split(/\s+/).filter(Boolean);
159
- }
160
- /** Splits a declared symbol into filtered, singularized resource-token
161
- * candidates — drops generic/framework words and tokens too short (<=2
162
- * chars) to safely anchor affinity. */
163
- function tokensFromSymbol(symbol) {
164
- return splitSymbolIntoWords(symbol)
165
- .map((word) => singularize(word.toLowerCase()))
166
- .filter((token) => token.length > 2 &&
167
- !GENERIC_SYMBOL_TOKENS.has(token) &&
168
- !GENERIC_RESOURCE_TOKENS.has(token));
169
- }
170
- /**
171
- * Derives a singular, lowercase "resource token" from a backend source file
172
- * path — e.g. "src/dtos/user.dto.ts" -> "user",
173
- * "modules/orders/order.service.ts" -> "order". Strips the file extension and
174
- * a trailing framework-layer suffix, then reuses `extractResourceFromPath`'s
175
- * "last meaningful path segment" rule by treating the remaining slash-joined
176
- * path as if it were a URL path.
177
- */
178
- export function deriveResourceToken(file) {
179
- const normalized = file.replace(/\\/g, "/");
180
- const ext = path.extname(normalized);
181
- const withoutExt = ext ? normalized.slice(0, -ext.length) : normalized;
182
- const withoutSuffix = withoutExt.replace(FRAMEWORK_SUFFIX_RE, "").replace(ROUTE_SUFFIX_RE, "");
183
- return singularize(extractResourceFromPath(withoutSuffix).toLowerCase());
184
- }
185
- /**
186
- * Derives ALL plausible resource-affinity tokens for a changed file: the
187
- * filename-derived token from `deriveResourceToken` (when it isn't generic),
188
- * plus tokens split out of any class/interface/type/enum/schema symbols
189
- * declared on the file's ADDED diff lines (when `diffHunk` is supplied).
190
- *
191
- * This is the sharper anchor requested on PR #588: a generic multi-DTO
192
- * module (`schemas/actions.py` holding `class DeploymentCreate`, `class
193
- * FlowCreate`, ...) has no single filename->resource mapping, but the
194
- * declared symbol name IS the resource — `DeploymentCreate` -> "deployment".
195
- * Same problem exists for TS (`types/common.ts` holding many interfaces).
196
- *
197
- * `diffHunk` is expected to be the file's own added-line slice (e.g. one
198
- * entry's lines from `sliceAddedLinesByFile`, joined with "\n") — this
199
- * function does not re-slice a full multi-file diff itself, to avoid
200
- * duplicating that walking logic in two places.
201
- *
202
- * Deduped; capped at `MAX_RESOURCE_TOKENS` so a huge schema file with many
203
- * declared symbols can't explode the affinity filter into a near-match-all.
204
- * With no `diffHunk` (or no symbols found), the result is exactly
205
- * `[deriveResourceToken(file)]` (or `[]` if that's generic/unknown) —
206
- * identical to today's filename-only behavior.
207
- */
208
- /**
209
- * Declared symbol names changed in a unified diff — from added-line declarations
210
- * AND from the hunk-header enclosing type (so a field-only edit to an existing class,
211
- * e.g. `DeploymentCreate.work_pool_name` → required, still yields `DeploymentCreate`).
212
- * Returns raw names, used both for resource-token derivation (via `tokensFromSymbol`)
213
- * and for content-grep matching against test bodies (SKYR-3924).
214
- */
215
- export function extractChangedSymbols(diffHunk) {
216
- const lines = diffHunk.split("\n");
217
- const symbols = new Set(extractDeclaredSymbolsFromAddedLines(lines.filter((line) => line.startsWith("+"))));
218
- const HUNK_CTX_DECL = /^@@.*@@.*\b(?:class|struct|interface|type|enum)\s+([A-Za-z_]\w*)/;
219
- for (const line of lines) {
220
- if (!line.startsWith("@@"))
221
- continue;
222
- const m = HUNK_CTX_DECL.exec(line);
223
- if (m)
224
- symbols.add(m[1]);
225
- }
226
- return [...symbols];
227
- }
228
- export function deriveResourceTokensForFile(file, diffHunk) {
229
- const tokens = [];
230
- const seen = new Set();
231
- const addToken = (token) => {
232
- if (!token || seen.has(token))
233
- return;
234
- seen.add(token);
235
- tokens.push(token);
236
- };
237
- const filenameToken = deriveResourceToken(file);
238
- if (filenameToken && filenameToken !== "unknown" && !GENERIC_RESOURCE_TOKENS.has(filenameToken)) {
239
- addToken(filenameToken);
240
- }
241
- if (diffHunk) {
242
- for (const symbol of extractChangedSymbols(diffHunk)) {
243
- for (const token of tokensFromSymbol(symbol))
244
- addToken(token);
245
- }
246
- }
247
- return tokens.slice(0, MAX_RESOURCE_TOKENS);
248
- }
249
- /**
250
- * All singular resource tokens implied by an endpoint path, used for affinity
251
- * matching against a `deriveResourceToken` result. Shared by the importer-hop
252
- * and path-affinity fallbacks (SKYR-3857) — the ONLY endpoint-path to
253
- * resource-token derivation in the codebase, mirroring how
254
- * `deriveResourceTokensForFile` is the only file-side one:
255
- * - the primary (first non-skip, non-param, non-version) path segment —
256
- * catches literal action-style segments like "/users/me", which
257
- * `extractResourceFromPath`'s "last segment" rule would otherwise resolve
258
- * to "me" instead of "users". Version-like segments are excluded via
259
- * `isVersionLikeSegment` — `SKIP_PATH_SEGMENTS` covers "api"/"public" but
260
- * only v1-v3, so without it any endpoint versioned beyond v3 would derive
261
- * a nonsense token ("v4") as its resource.
262
- * - `extractResourceFromPath`'s own result, split on "_" for the sub-resource
263
- * composite form ("orders_items" -> "order", "item"). That function only
264
- * recognizes OpenAPI-style "{param}" segments (its dedup-key callers see
265
- * normalized paths), so ":id"/"[id]"-style params can flow through as its
266
- * result — filtered here rather than widening a function whose other
267
- * callers don't need it.
268
- */
269
- export function endpointResourceTokens(endpointPath) {
270
- const tokens = new Set();
271
- const segments = endpointPath.split("/").filter(Boolean);
272
- const primary = segments.find((s) => !isParamSegment(s) && !SKIP_PATH_SEGMENTS.has(s.toLowerCase()) && !isVersionLikeSegment(s));
273
- if (primary)
274
- tokens.add(singularize(primary.toLowerCase()));
275
- const composite = extractResourceFromPath(endpointPath).toLowerCase();
276
- for (const part of composite.split("_")) {
277
- if (part && !isParamSegment(part))
278
- tokens.add(singularize(part));
279
- }
280
- return tokens;
281
- }
282
- /**
283
- * Maps unmatched changed files (from `classifyEndpointsByChangedFiles`) to
284
- * endpoints that import them, when the import target and the endpoint share
285
- * the same REST resource.
286
- *
287
- * `alreadyClassifiedEndpoints` (typically `changedEndpoints` + `newEndpoints`
288
- * from the same classification pass) is used purely for dedup — an endpoint
289
- * already classified directly is not duplicated into the returned `endpoints`
290
- * list, though the mapping is still established (harmless to skip re-adding
291
- * something already present).
292
- *
293
- * `diffContent`, when supplied, is the full unified diff — used to derive
294
- * per-file added-line hunks for `deriveResourceTokensForFile`'s symbol-based
295
- * affinity anchors (see module doc, limitation 2). Without it, affinity falls
296
- * back to the filename-only token, identical to pre-symbol-affinity behavior.
297
- */
298
- export function mapUnmatchedFilesToImporterEndpoints(repositoryPath, unmatchedFiles, scannedEndpoints, alreadyClassifiedEndpoints = [], diffContent) {
299
- const endpoints = [];
300
- const byFile = {};
301
- const alreadyKeys = new Set(alreadyClassifiedEndpoints.map((ep) => `${ep.sourceFile}::${ep.path}`));
302
- const addedKeys = new Set();
303
- const addedLinesByFile = diffContent ? sliceAddedLinesByFile(diffContent) : undefined;
304
- // Cache file contents across unmatched files — multiple changed files may be
305
- // imported by the same controller/service, and controllers can be large.
306
- const contentCache = new Map();
307
- const readCached = (sourceFile) => {
308
- if (contentCache.has(sourceFile))
309
- return contentCache.get(sourceFile);
310
- let content = null;
311
- try {
312
- const abs = path.isAbsolute(sourceFile) ? sourceFile : path.join(repositoryPath, sourceFile);
313
- const stat = fs.statSync(abs);
314
- if (stat.size <= MAX_FILE_SIZE_BYTES) {
315
- content = fs.readFileSync(abs, "utf-8");
316
- }
317
- }
318
- catch {
319
- content = null; // unreadable / too large — tolerate, treat as no content to match against
320
- }
321
- contentCache.set(sourceFile, content);
322
- return content;
323
- };
324
- for (const file of unmatchedFiles) {
325
- try {
326
- if (isFrontendFile(file) || isTestFile(file) || !SOURCE_EXTS.test(file))
327
- continue;
328
- const fileAddedLines = addedLinesByFile?.get(file);
329
- const diffHunk = fileAddedLines && fileAddedLines.length > 0 ? fileAddedLines.join("\n") : undefined;
330
- const resourceTokens = deriveResourceTokensForFile(file, diffHunk);
331
- if (resourceTokens.length === 0)
332
- continue;
333
- const signatures = buildPathSignatures(file);
334
- // For an index module the directory stem is a plain string match, so also
335
- // require the specifier to resolve to this module's directory.
336
- const indexModule = isIndexModule(file);
337
- const rawCandidates = scannedEndpoints.filter((ep) => {
338
- const content = readCached(ep.sourceFile);
339
- if (content === null || !signatures.some((sig) => content.includes(sig)))
340
- return false;
341
- return !indexModule || importsIndexModule(ep.sourceFile, content, file);
342
- });
343
- if (rawCandidates.length === 0)
344
- continue;
345
- // Critical containment: only keep candidates whose own resource affinity
346
- // matches ANY of this file's resource tokens (filename OR symbol-derived
347
- // — see deriveResourceTokensForFile). If nothing survives, flag NOTHING
348
- // for this file — conservative, preserves today's "unmatched" behavior
349
- // rather than guessing.
350
- const affineCandidates = rawCandidates.filter((ep) => {
351
- const epTokens = endpointResourceTokens(ep.path);
352
- return resourceTokens.some((token) => epTokens.has(token));
353
- });
354
- if (affineCandidates.length === 0)
355
- continue;
356
- const mappedPaths = [];
357
- for (const ep of affineCandidates) {
358
- const key = `${ep.sourceFile}::${ep.path}`;
359
- mappedPaths.push(ep.path);
360
- if (alreadyKeys.has(key) || addedKeys.has(key))
361
- continue;
362
- addedKeys.add(key);
363
- endpoints.push(ep);
364
- }
365
- if (mappedPaths.length > 0) {
366
- byFile[file] = [...new Set(mappedPaths)];
367
- }
368
- }
369
- catch (err) {
370
- logger.warning("Importer-hop classification failed for file — skipping", {
371
- file,
372
- error: err instanceof Error ? err.message : String(err),
373
- });
374
- }
375
- }
376
- return { endpoints, byFile };
377
- }
378
- /**
379
- * Builds the inputs `draftResponseShapeScenarios` needs from an
380
- * {@link ImporterHopResult}, keyed off `byFile` rather than `endpoints`.
381
- *
382
- * `hopResult.endpoints` is deduped against endpoints already present in
383
- * `changedEndpoints`/`newEndpoints` (see `mapUnmatchedFilesToImporterEndpoints`),
384
- * so it can be empty — or missing entries — even when `byFile` holds valid
385
- * path mappings (e.g. a controller and the DTO it imports both changed in the
386
- * same PR: the controller's endpoint is already classified directly, so the
387
- * hop drops it from `endpoints`, but `byFile` still records the mapping).
388
- * Response-shape drafting wants every hop-mapped endpoint regardless of
389
- * whether it was newly added to the change set, so this re-derives the
390
- * endpoint list from `scannedEndpoints` filtered to the paths `byFile` maps
391
- * to, instead of relying on the deduped `endpoints` array.
392
- */
393
- export function buildHopResponseShapeInputs(hopResult, scannedEndpoints, diffContent) {
394
- const mappedPaths = new Set(Object.values(hopResult.byFile).flat());
395
- if (mappedPaths.size === 0)
396
- return { endpoints: [], addedFieldsByFile: {} };
397
- return {
398
- endpoints: scannedEndpoints.filter((ep) => mappedPaths.has(ep.path)),
399
- addedFieldsByFile: diffContent
400
- ? computeAddedFieldsByFile(diffContent, Object.keys(hopResult.byFile))
401
- : {},
402
- };
403
- }
404
- // ── Added-field extraction from diff hunks ──────────────────────────────────
405
- const TS_FIELD_RE = /^\+\s*(?:public |readonly |private )?([A-Za-z_]\w*)\??!?\s*[:=]/;
406
- const PY_FIELD_RE = /^\+\s*([a-z_]\w*)\s*:\s*\w+/;
407
- // Words that can precede a bare colon/equals in real code without being a
408
- // field/property declaration (control-flow keywords, declarations, labels).
409
- const NON_FIELD_KEYWORDS = new Set([
410
- "if", "for", "while", "return", "const", "let", "var",
411
- "import", "export", "function", "class", "case", "default",
412
- "else", "switch", "do", "try", "catch", "finally",
413
- ]);
414
- /**
415
- * Extracts likely property/field names from an array of raw added diff lines
416
- * (each still prefixed with the unified-diff `+` marker). Best-effort regex,
417
- * not a parser — intended to give a scenario a field name to name-drop, not
418
- * to be exhaustive.
419
- */
420
- export function extractFieldNamesFromAddedLines(lines) {
421
- const fields = new Set();
422
- for (const line of lines) {
423
- // A genuine content line never starts with a second "+" — that's the
424
- // "+++ b/file" diff header line.
425
- if (line.startsWith("++"))
426
- continue;
427
- const match = TS_FIELD_RE.exec(line) ?? PY_FIELD_RE.exec(line);
428
- if (!match)
429
- continue;
430
- const name = match[1];
431
- if (NON_FIELD_KEYWORDS.has(name.toLowerCase()))
432
- continue;
433
- fields.add(name);
434
- }
435
- return [...fields];
436
- }
437
- // `sliceAddedLinesByFile` now lives in `branchDiff.ts` (imported above) — moved
438
- // there so modules this one already imports can reuse it without an import
439
- // cycle. Re-exported here so existing importers of it from this module
440
- // (pathAffinityClassification.ts) don't need to change their import path.
441
- export { sliceAddedLinesByFile };
442
- /**
443
- * Splits a unified diff into per-file raw sections — every line after a file's
444
- * `diff --git` header up to the next one — keyed by the `b/` (post-change) path.
445
- * Unlike `sliceAddedLinesByFile` (added lines only), this preserves the `@@`
446
- * hunk headers, so `extractChangedSymbols`' enclosing-type extraction (which
447
- * reads `@@ ... class Foo`) still works when a single file's section is passed
448
- * to `deriveResourceTokensForFile`. Use this for per-file token derivation that
449
- * must not mix symbols across files in a multi-file diff (SKYR-3924).
450
- */
451
- export function sliceDiffByFile(diffContent) {
452
- const sections = new Map();
453
- let currentFile = null;
454
- for (const line of diffContent.split("\n")) {
455
- const headerMatch = /^diff --git a\/.+ b\/(.+)$/.exec(line);
456
- if (headerMatch) {
457
- currentFile = headerMatch[1];
458
- sections.set(currentFile, []);
459
- continue;
460
- }
461
- if (currentFile)
462
- sections.get(currentFile).push(line);
463
- }
464
- const joined = new Map();
465
- for (const [file, lines] of sections)
466
- joined.set(file, lines.join("\n"));
467
- return joined;
468
- }
469
- /**
470
- * Computes added-field names per file for the given files, from a full
471
- * unified diff. Files with no added-line matches are omitted from the result
472
- * (an empty field list is a valid, expected outcome — the caller still drafts
473
- * a scenario, just without field hints).
474
- */
475
- export function computeAddedFieldsByFile(diffContent, files) {
476
- const result = {};
477
- if (!diffContent || files.length === 0)
478
- return result;
479
- const sections = sliceAddedLinesByFile(diffContent);
480
- for (const file of files) {
481
- const addedLines = sections.get(file);
482
- if (!addedLines || addedLines.length === 0)
483
- continue;
484
- const names = extractFieldNamesFromAddedLines(addedLines);
485
- if (names.length > 0)
486
- result[file] = names;
487
- }
488
- return result;
489
- }
@@ -1,49 +0,0 @@
1
- import type { ScannedEndpoint } from "./repoScanner.js";
2
- /** Version/prefix directory segments ("v4", "api", ...) present in a changed
3
- * file's own path — required, as a conjunction, in any matching endpoint path.
4
- * Prevents a `routes/v4/secret-router.ts` change from matching a `/api/v1/*`
5
- * sibling endpoint on resource-token alone. */
6
- export declare function deriveVersionSegments(file: string): Set<string>;
7
- export interface PathAffinityFallbackResult {
8
- /** Endpoints newly classified via path affinity, deduped against `alreadyClassifiedEndpoints`. */
9
- endpoints: ScannedEndpoint[];
10
- /** Unmatched files that mapped to at least one endpoint (for unmatched-state cleanup). */
11
- mappedFiles: string[];
12
- }
13
- /**
14
- * Maps changed files that `classifyEndpointsByChangedFiles` could not match by
15
- * `sourceFile` (its `unmatchedFiles`) to catalog endpoints that plausibly belong
16
- * to the same REST resource, purely from the file's own path and the endpoint's
17
- * own path — no `sourceFile` needed on either side. Intended to run against the
18
- * *final* endpoint catalog (after any OpenAPI spec merge), since a catalog with
19
- * no source-scan coverage for a file is, by definition, only useful for this
20
- * fallback once spec-derived entries are present to match against.
21
- *
22
- * `alreadyClassifiedEndpoints` (typically `changedEndpoints` + `newEndpoints`
23
- * from the same classification pass) is used only for dedup — never duplicates
24
- * an endpoint already classified directly.
25
- *
26
- * `diffContent`, when supplied, is the full unified diff — sliced per-file
27
- * (via `sliceAddedLinesByFile`, shared with `mapUnmatchedFilesToImporterEndpoints`)
28
- * and passed to `deriveResourceTokensForFile` so a route file whose filename
29
- * alone is too generic to anchor a match (e.g. `routes/v4/index-router.ts`)
30
- * can still match via a symbol declared on its added diff lines. Without it,
31
- * matching falls back to the filename-only token, identical to pre-symbol-
32
- * affinity behavior.
33
- *
34
- * `removedEndpoints` (the classifier's removal result, recovered from the base
35
- * tree) is excluded from the candidate set BY PATH+METHOD, not via the
36
- * sourceFile-keyed dedup: removal is classified at Step 2.5 against the
37
- * pre-spec-merge catalog, but this fallback matches the post-spec-merge
38
- * catalog — where a stale checked-in OpenAPI spec can still list a deleted
39
- * endpoint as a `sourceFile: ""` entry. Without this guard, a sibling
40
- * unmatched file sharing the resource token would resurrect the removed
41
- * endpoint as "modified", contradicting the removal classification. A `MULTI`
42
- * removed method covers every method on that path; a candidate survives if
43
- * any of its methods is not removed.
44
- *
45
- * Matched endpoints are always attributed as MODIFIED (never new/removed) —
46
- * path affinity says an endpoint is *related* to the change, not that the
47
- * relationship is an addition or removal.
48
- */
49
- export declare function classifyUnmatchedFilesByPathAffinity(unmatchedFiles: string[], scannedEndpoints: ScannedEndpoint[], alreadyClassifiedEndpoints?: ScannedEndpoint[], maxEndpointsPerFile?: number, diffContent?: string, removedEndpoints?: ScannedEndpoint[]): PathAffinityFallbackResult;