@skyramp/mcp 0.3.4 → 0.3.6-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 (159) hide show
  1. package/build/playwright/registerPlaywrightTools.js +92 -30
  2. package/build/playwright/traceRecordingPrompt.d.ts +6 -0
  3. package/build/playwright/traceRecordingPrompt.js +6 -2
  4. package/build/prompts/code-reuse.d.ts +1 -2
  5. package/build/prompts/code-reuse.js +182 -77
  6. package/build/prompts/modularization/integration-test-modularization.d.ts +2 -0
  7. package/build/prompts/modularization/integration-test-modularization.js +83 -41
  8. package/build/prompts/modularization/render.d.ts +18 -0
  9. package/build/prompts/modularization/render.js +12 -0
  10. package/build/prompts/modularization/ui-test-modularization.d.ts +3 -1
  11. package/build/prompts/modularization/ui-test-modularization.js +89 -47
  12. package/build/prompts/pom-aware-code-reuse.js +3 -1
  13. package/build/prompts/shared-helper-policy.d.ts +57 -0
  14. package/build/prompts/shared-helper-policy.js +135 -0
  15. package/build/prompts/test-recommendation/diffExecutionPlan.js +62 -56
  16. package/build/prompts/test-recommendation/fullRepoCatalog.js +19 -8
  17. package/build/prompts/test-recommendation/recommendationShared.d.ts +28 -6
  18. package/build/prompts/test-recommendation/recommendationShared.js +90 -16
  19. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +22 -0
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -2
  21. package/build/prompts/test-recommendation/test-recommendation-prompt.js +3 -3
  22. package/build/prompts/testbot/testbot-prompts.js +88 -33
  23. package/build/recommendation/budgeters/shared.js +105 -27
  24. package/build/recommendation/discriminators.js +13 -2
  25. package/build/recommendation/planRanker.d.ts +6 -6
  26. package/build/recommendation/planRanker.js +6 -61
  27. package/build/services/AnalyticsService.d.ts +7 -0
  28. package/build/services/AnalyticsService.js +7 -1
  29. package/build/services/ModularizationService.js +1 -3
  30. package/build/services/TestDiscoveryService.d.ts +0 -2
  31. package/build/services/TestDiscoveryService.js +2 -37
  32. package/build/services/TestGenerationService.d.ts +16 -0
  33. package/build/services/TestGenerationService.js +86 -10
  34. package/build/services/containerEnv.js +13 -12
  35. package/build/tools/code-refactor/codeReuseTool.js +279 -93
  36. package/build/tools/code-refactor/enhance-state.d.ts +49 -0
  37. package/build/tools/code-refactor/enhance-state.js +109 -0
  38. package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -1
  39. package/build/tools/code-refactor/modularizationTool.js +9 -2
  40. package/build/tools/code-refactor/reuse-outcome.d.ts +23 -1
  41. package/build/tools/code-refactor/reuse-outcome.js +14 -4
  42. package/build/tools/code-refactor/reuse-state.d.ts +127 -5
  43. package/build/tools/code-refactor/reuse-state.js +628 -16
  44. package/build/tools/code-refactor/utils-verify-gates.d.ts +26 -0
  45. package/build/tools/code-refactor/utils-verify-gates.js +100 -0
  46. package/build/tools/code-refactor/verify-gates.d.ts +2 -1
  47. package/build/tools/code-refactor/verify-gates.js +90 -25
  48. package/build/tools/executeSkyrampTestTool.d.ts +19 -0
  49. package/build/tools/executeSkyrampTestTool.js +158 -8
  50. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -2
  51. package/build/tools/generate-tests/generateE2ERestTool.js +16 -0
  52. package/build/tools/generate-tests/generateUIRestTool.d.ts +1 -0
  53. package/build/tools/generate-tests/generateUIRestTool.js +22 -0
  54. package/build/tools/generate-tests/scenarioLint.d.ts +2 -0
  55. package/build/tools/generate-tests/scenarioLint.js +127 -19
  56. package/build/tools/generate-tests/trace-reuse-guard.d.ts +20 -0
  57. package/build/tools/generate-tests/trace-reuse-guard.js +93 -0
  58. package/build/tools/submitReportTool.d.ts +38 -38
  59. package/build/tools/submitReportTool.js +487 -104
  60. package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
  61. package/build/tools/test-management/analyzeChangesTool.js +75 -12
  62. package/build/tools/test-management/analyzeTestHealthTool.js +7 -7
  63. package/build/tools/test-management/registerTestPlanTool.d.ts +203 -0
  64. package/build/tools/test-management/registerTestPlanTool.js +149 -23
  65. package/build/types/Recommendation.d.ts +34 -5
  66. package/build/types/RepositoryAnalysis.d.ts +133 -114
  67. package/build/types/RepositoryAnalysis.js +1 -1
  68. package/build/types/ReuseOutcome.d.ts +102 -6
  69. package/build/types/ReuseOutcome.js +16 -2
  70. package/build/types/TestRecommendation.js +21 -3
  71. package/build/types/TestTypes.js +14 -8
  72. package/build/types/TestbotReport.d.ts +25 -3
  73. package/build/types/index.d.ts +2 -2
  74. package/build/types/index.js +1 -1
  75. package/build/utils/AnalysisStateManager.d.ts +69 -1
  76. package/build/utils/AnalysisStateManager.js +69 -5
  77. package/build/utils/branchDiff.d.ts +10 -0
  78. package/build/utils/branchDiff.js +28 -0
  79. package/build/utils/changedRoutes.d.ts +29 -0
  80. package/build/utils/changedRoutes.js +87 -0
  81. package/build/utils/featureFlags.d.ts +21 -0
  82. package/build/utils/featureFlags.js +23 -0
  83. package/build/utils/frontendIntegration.js +34 -4
  84. package/build/utils/importerHop.d.ts +2 -8
  85. package/build/utils/importerHop.js +15 -53
  86. package/build/utils/pathMatching.d.ts +38 -0
  87. package/build/utils/pathMatching.js +71 -0
  88. package/build/utils/pathSignatures.d.ts +22 -0
  89. package/build/utils/pathSignatures.js +57 -0
  90. package/build/utils/planMatchKeys.d.ts +16 -3
  91. package/build/utils/planMatchKeys.js +26 -10
  92. package/build/utils/pluralization.d.ts +10 -0
  93. package/build/utils/pluralization.js +18 -0
  94. package/build/utils/pom-catalog-parse.d.ts +52 -0
  95. package/build/utils/pom-catalog-parse.js +141 -0
  96. package/build/utils/pom-scope/selector-extractor.d.ts +12 -0
  97. package/build/utils/pom-scope/selector-extractor.js +34 -8
  98. package/build/utils/pom-verify/verify.d.ts +6 -5
  99. package/build/utils/pom-verify/verify.js +8 -6
  100. package/build/utils/reportLanguage.d.ts +43 -0
  101. package/build/utils/reportLanguage.js +125 -0
  102. package/build/utils/reportVerification.d.ts +74 -4
  103. package/build/utils/reportVerification.js +259 -3
  104. package/build/utils/reuseRouting.d.ts +3 -0
  105. package/build/utils/reuseRouting.js +50 -0
  106. package/build/utils/routeParsers.d.ts +2 -0
  107. package/build/utils/routeParsers.js +65 -8
  108. package/build/utils/scenarioDrafting.d.ts +1 -1
  109. package/build/utils/scenarioDrafting.js +57 -45
  110. package/build/utils/subjectEndpoints.d.ts +19 -0
  111. package/build/utils/subjectEndpoints.js +98 -0
  112. package/build/utils/testFileClassification.d.ts +11 -0
  113. package/build/utils/testFileClassification.js +47 -0
  114. package/build/utils/uiPageEnumerator.d.ts +45 -19
  115. package/build/utils/uiPageEnumerator.js +95 -51
  116. package/build/utils/utils-verify/allow.d.ts +16 -0
  117. package/build/utils/utils-verify/allow.js +68 -0
  118. package/build/utils/utils-verify/call-sites.d.ts +34 -0
  119. package/build/utils/utils-verify/call-sites.js +154 -0
  120. package/build/utils/utils-verify/index.d.ts +7 -0
  121. package/build/utils/utils-verify/index.js +7 -0
  122. package/build/utils/utils-verify/language-spec.d.ts +91 -0
  123. package/build/utils/utils-verify/language-spec.js +210 -0
  124. package/build/utils/utils-verify/locate.d.ts +39 -0
  125. package/build/utils/utils-verify/locate.js +199 -0
  126. package/build/utils/utils-verify/parse.d.ts +34 -0
  127. package/build/utils/utils-verify/parse.js +177 -0
  128. package/build/utils/utils-verify/stage.d.ts +24 -0
  129. package/build/utils/utils-verify/stage.js +107 -0
  130. package/build/utils/utils-verify/verify.d.ts +63 -0
  131. package/build/utils/utils-verify/verify.js +168 -0
  132. package/build/utils/utils.d.ts +3 -1
  133. package/build/utils/utils.js +3 -1
  134. package/build/workspace/workspace.d.ts +32 -32
  135. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +9 -5
  136. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +16 -0
  137. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -0
  138. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +115 -14
  139. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +13 -1
  140. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
  141. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -0
  142. package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +27 -253
  143. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-DtudTj_v.js → codeMirrorModule-DJMC4zNo.js} +1 -1
  144. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BW82eAUI.js +196 -0
  145. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
  146. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-FNMuBzX1.js → codeMirrorModule-CZfp96qZ.js} +1 -1
  147. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-gpLo02E0.js +809 -0
  148. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.Bq1r1URj.js +2 -0
  149. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  150. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.VEfqi1qN.js +5 -0
  151. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  152. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  153. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  154. package/node_modules/playwright/package.json +1 -1
  155. package/package.json +2 -2
  156. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +0 -422
  157. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +0 -1035
  158. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +0 -2
  159. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +0 -5
@@ -4,6 +4,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { CandidateUiPage } from "../../utils/uiPageEnumerator.js";
5
5
  import type { FrontendFileIntegration } from "../../types/FrontendIntegration.js";
6
6
  import { TraceFile } from "../../types/RepositoryAnalysis.js";
7
+ import { BranchDiffData } from "../../utils/branchDiff.js";
7
8
  import { ScannedEndpoint } from "../../utils/repoScanner.js";
8
9
  import { TraceParseResult } from "../../utils/trace-parser.js";
9
10
  /** Exported for testing: maps a parsed trace result to a TraceFile. */
@@ -25,7 +26,7 @@ export declare const analyzeChangesInputSchema: {
25
26
  includeUncommitted: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
26
27
  };
27
28
  export declare const NO_UI_INSTRUCTIONS = "No UI changes detected \u2014 no blueprint capture needed.";
28
- export declare const NO_RESOLVABLE_URLS_INSTRUCTIONS = "Frontend changes detected but no candidate URLs could be resolved (workspace baseUrl missing or no router files matched). UI recommendations will be source-grounded only.";
29
+ export declare const NO_RESOLVABLE_URLS_INSTRUCTIONS = "Frontend changes detected but no candidate URLs could be resolved (no route files matched the changed files or their importers, and no frontend baseUrl to fall back to). UI recommendations will be source-grounded only.";
29
30
  export declare function buildCaptureInstructions(pages: CandidateUiPage[]): string;
30
31
  /**
31
32
  * Instruction block for changed frontend files the server determined have no
@@ -45,4 +46,26 @@ export declare function buildAnalyzeChangesResult(parts: {
45
46
  outputText: string;
46
47
  recommendationPrompt: string;
47
48
  }): CallToolResult;
49
+ /**
50
+ * Coverage is discovered from the working tree, so a test file the PR itself adds looks
51
+ * identical to pre-existing coverage. This is what the budgeter reads to decide whether
52
+ * an empty plan is a dedup artifact or the author's own coverage.
53
+ *
54
+ * No diff means no answer, so it reports `true` and the reserve stays shut. `diffData` is
55
+ * absent in full-repo scope — where fullRepoCatalog deliberately drops externally covered
56
+ * scenarios, and promoting them back would contradict the prompt the agent reads — and
57
+ * after a branch-diff failure, where nothing is known about the changed files at all.
58
+ *
59
+ * Classification comes from discovery, not from `isTestFile`: the flag has to agree with
60
+ * whatever built the coverage keys it guards.
61
+ *
62
+ * `userChangedFiles` is the bot-filtered list. The bot commits its generated tests onto
63
+ * the branch it analyzed, and those filenames are exactly what the discovery patterns
64
+ * exist to match — so reading the raw base..HEAD list shut the reserve on every re-run
65
+ * of a branch: run 1 generates the tests, run 2 sees them. When the filter cannot answer
66
+ * (no bot commit yet, or a git failure) the raw list stands. That is safe in the same
67
+ * direction as the `!diffData` case: the raw list can only hold MORE files, so the
68
+ * fallback shuts the reserve rather than opening it.
69
+ */
70
+ export declare function computeDiffChangesTestFiles(diffData: BranchDiffData | undefined, userChangedFiles?: string[] | null): boolean;
48
71
  export declare function registerAnalyzeChangesTool(server: McpServer): void;
@@ -10,17 +10,20 @@ import { dualChannelResult } from "../../utils/utils.js";
10
10
  import { parseWorkspaceAuthType, getDefaultAuthHeader, WorkspaceAuthType, readWorkspaceConfigRaw } from "../../utils/workspaceAuth.js";
11
11
  import { AnalyticsService } from "../../services/AnalyticsService.js";
12
12
  import { makeProgressReporter } from "../../utils/progress.js";
13
- import { StateManager, registerSession, storeSessionData, setTestsRepoDir, getActiveRunStatePath, setActiveRunStatePath, } from "../../utils/AnalysisStateManager.js";
13
+ import { StateManager, registerSession, storeSessionData, rememberTestsRepoDir, getActiveRunStatePath, setActiveRunStatePath, } from "../../utils/AnalysisStateManager.js";
14
14
  import { buildRecommendationPrompt, computeScoredCandidates } from "../../prompts/test-recommendation/test-recommendation-prompt.js";
15
15
  import { hasFlutterSdkDep, isFrontendFile, isTestFile } from "../../prompts/test-recommendation/scopeAssessment.js";
16
16
  import { buildExternalCoverageSet } from "../../prompts/test-recommendation/recommendationShared.js";
17
+ import { resolveSubjectEndpoints } from "../../utils/subjectEndpoints.js";
18
+ import { collectChangedRouteLines } from "../../utils/changedRoutes.js";
17
19
  import { CandidateSource, computeCandidateId } from "../../types/Recommendation.js";
18
20
  import { selectPlan } from "../../recommendation/planRanker.js";
19
21
  import { buildApprovedPlanItem } from "../../utils/planMatchKeys.js";
20
- import { enumerateCandidateUiPages } from "../../utils/uiPageEnumerator.js";
22
+ import { enumerateCandidateUiPages, MAX_CANDIDATE_PAGES } from "../../utils/uiPageEnumerator.js";
21
23
  import { checkFrontendFileIntegration } from "../../utils/frontendIntegration.js";
22
24
  import { MAX_RECOMMENDATIONS, MAX_TESTS_TO_GENERATE } from "../../prompts/test-recommendation/recommendationSections.js";
23
25
  import { TestDiscoveryService } from "../../services/TestDiscoveryService.js";
26
+ import { isDiscoveredTestFile } from "../../utils/testFileClassification.js";
24
27
  import { ScenarioSource, AnalysisScope } from "../../types/RepositoryAnalysis.js";
25
28
  import { computeBranchDiff } from "../../utils/branchDiff.js";
26
29
  import { classifyEndpointsByChangedFiles, selectRemovalCandidateFiles, recoverRemovedEndpointsFromBase, } from "../../utils/routeParsers.js";
@@ -324,23 +327,35 @@ export const analyzeChangesInputSchema = {
324
327
  // for UI recommendation reasoning (enforced at submit time by the Blueprint
325
328
  // Citation Invariant in the testbot prompt).
326
329
  export const NO_UI_INSTRUCTIONS = `No UI changes detected — no blueprint capture needed.`;
327
- export const NO_RESOLVABLE_URLS_INSTRUCTIONS = `Frontend changes detected but no candidate URLs could be resolved (workspace baseUrl missing or no router files matched). UI recommendations will be source-grounded only.`;
330
+ export const NO_RESOLVABLE_URLS_INSTRUCTIONS = `Frontend changes detected but no candidate URLs could be resolved (no route files matched the changed files or their importers, and no frontend baseUrl to fall back to). UI recommendations will be source-grounded only.`;
328
331
  export function buildCaptureInstructions(pages) {
332
+ const pathOnly = pages.some((p) => p.baseUrlResolved === false);
329
333
  const pagesYaml = pages
330
- .map((p, i) => ` ${i + 1}. ${p.url} (sourcedFrom: ${p.sourcedFrom.join(", ") || "(none)"}, strategy: ${p.strategy})`)
334
+ .map((p, i) => ` ${i + 1}. ${p.url} (sourcedFrom: ${p.sourcedFrom.join(", ") || "(none)"}${p.via ? `, via: ${p.via.join(", ")}` : ""}, strategy: ${p.strategy})`)
331
335
  .join("\n");
336
+ const capNote = pages.length >= MAX_CANDIDATE_PAGES
337
+ ? `\n(The list is capped at ${MAX_CANDIDATE_PAGES} pages — direct route matches first, then the pages that render the most changed files.)`
338
+ : "";
339
+ // The server could not resolve a frontend baseUrl (no frontend service with
340
+ // `api.baseUrl`, no SKYRAMP_TEST_BASE_URL). The paths are still exact; the
341
+ // agent supplies the host.
342
+ const baseUrlStep = pathOnly
343
+ ? `
344
+ **The entries above are URL paths, not full URLs** — the workspace declares no frontend service with \`api.baseUrl\`. Determine the frontend base URL once before capturing — the testbot workflow's \`targetReadyCheckCommand\` or the dev-server port in \`package.json\` scripts usually names it — then prefix every path with it. Log an \`issuesFound\` info entry recommending that \`api.baseUrl\` be set on the frontend service in \`.skyramp/workspace.yml\`.
345
+ `
346
+ : "";
332
347
  return `Frontend changes detected. **Before writing any UI recommendation \`reasoning\`, capture blueprints on the candidate UI pages below.** Those captures stay in your tool-result history and serve as element vocabulary — the recommendation catalog further down gives you the authoring rules; you bring the observed elements.
333
348
 
334
- **Candidate URLs:**
335
- ${pagesYaml}
336
-
349
+ **Candidate ${pathOnly ? "URL paths" : "URLs"}:**
350
+ ${pagesYaml}${capNote}
351
+ ${baseUrlStep}
337
352
  **For each candidate URL:**
338
353
  - \`browser_navigate\` to the URL
339
354
  - \`browser_blueprint\` to capture the page
340
355
 
341
356
  You don't need to thread the blueprints back into a tool call — they're in your context once captured.
342
357
 
343
- If a candidate URL 404s or redirects unexpectedly, navigate from the workspace baseUrl and explore (admin apps mount routes under base prefixes the source extraction can't see). If the page rendered but lacks the changed feature (gated UI: modal, dropdown, accordion), do NOT iterate further during this step — UI recs will fall back to source-grounded prose for those, and the agent's later trace recording (Task 2) will navigate into the gate via capture-act-capture.
358
+ If a candidate URL 404s or redirects unexpectedly, navigate from the frontend base URL and explore (admin apps mount routes under base prefixes the source extraction can't see). If the page rendered but lacks the changed feature (gated UI: modal, dropdown, accordion), do NOT iterate further during this step — UI recs will fall back to source-grounded prose for those, and the agent's later trace recording (Task 2) will navigate into the gate via capture-act-capture.
344
359
 
345
360
  If \`browser_blueprint\` fails on every candidate URL (app unreachable, all 404s), proceed and log an \`issuesFound\` info entry. Recommendations will be source-grounded; non-UI work is unaffected.`;
346
361
  }
@@ -376,6 +391,32 @@ export function buildAnalyzeChangesResult(parts) {
376
391
  const executionPlan = `\`\`\`json\n${parts.structuredSummary}\n\`\`\`\n\n## UI Blueprint Capture — do this BEFORE writing UI recommendation reasoning\n${parts.uiInstructions}\n\n${parts.outputText}\n\n---\n\n## Pre-built Test Catalog — Fill in placeholders from source code, then display verbatim\n⚠️ Do NOT reformat, rename sections, or generate a new catalog. Replace \`<…from source>\` values, then show this output exactly as-is, grouped by test type.\n\n${parts.recommendationPrompt}`;
377
392
  return dualChannelResult({ executionPlan });
378
393
  }
394
+ /**
395
+ * Coverage is discovered from the working tree, so a test file the PR itself adds looks
396
+ * identical to pre-existing coverage. This is what the budgeter reads to decide whether
397
+ * an empty plan is a dedup artifact or the author's own coverage.
398
+ *
399
+ * No diff means no answer, so it reports `true` and the reserve stays shut. `diffData` is
400
+ * absent in full-repo scope — where fullRepoCatalog deliberately drops externally covered
401
+ * scenarios, and promoting them back would contradict the prompt the agent reads — and
402
+ * after a branch-diff failure, where nothing is known about the changed files at all.
403
+ *
404
+ * Classification comes from discovery, not from `isTestFile`: the flag has to agree with
405
+ * whatever built the coverage keys it guards.
406
+ *
407
+ * `userChangedFiles` is the bot-filtered list. The bot commits its generated tests onto
408
+ * the branch it analyzed, and those filenames are exactly what the discovery patterns
409
+ * exist to match — so reading the raw base..HEAD list shut the reserve on every re-run
410
+ * of a branch: run 1 generates the tests, run 2 sees them. When the filter cannot answer
411
+ * (no bot commit yet, or a git failure) the raw list stands. That is safe in the same
412
+ * direction as the `!diffData` case: the raw list can only hold MORE files, so the
413
+ * fallback shuts the reserve rather than opening it.
414
+ */
415
+ export function computeDiffChangesTestFiles(diffData, userChangedFiles) {
416
+ if (!diffData)
417
+ return true;
418
+ return (userChangedFiles ?? diffData.changedFiles).some(isDiscoveredTestFile);
419
+ }
379
420
  export function registerAnalyzeChangesTool(server) {
380
421
  server.registerTool(TOOL_NAME, {
381
422
  annotations: {
@@ -736,7 +777,9 @@ export function registerAnalyzeChangesTool(server) {
736
777
  let discoveredRelevantExternalPaths = [];
737
778
  try {
738
779
  const testDiscoveryService = new TestDiscoveryService();
739
- setTestsRepoDir(params.testsRepoDir);
780
+ // Run-scoped: only the primary call carries testsRepoDir, so never
781
+ // let a related repo's call clear it (SKYR-4204).
782
+ rememberTestsRepoDir(params.testsRepoDir);
740
783
  const discoveryResult = await testDiscoveryService.discoverTests(testDir ?? params.repositoryPath, { changedResources, changedSymbols, preciseResources: (changedSymbols?.length ?? 0) > 0, changedFrontendFiles, changedSelectors });
741
784
  existingTests = discoveryResult.tests.map((test) => ({
742
785
  testFile: test.testFile,
@@ -1321,19 +1364,21 @@ export function registerAnalyzeChangesTool(server) {
1321
1364
  // same classification without re-deriving it. Absent on backend-only PRs.
1322
1365
  //
1323
1366
  // candidateUiPages is enumerated programmatically via the strategy
1324
- // ladder in uiPageEnumerator (framework route grep, source-grounded
1325
- // routes, root fallback). The agent uses these to capture
1367
+ // ladder in uiPageEnumerator (framework route grep + import graph,
1368
+ // source-grounded routes, root fallback). The agent uses these to capture
1326
1369
  // browser_blueprints — see uiInstructions below, which this tool returns
1327
1370
  // so the agent captures element vocabulary for UI rec reasoning.
1328
1371
  const uiContext = await (async () => {
1329
1372
  // changedFrontendFiles computed above (before discoverTests) — reuse here.
1330
1373
  if (changedFrontendFiles.length === 0)
1331
1374
  return undefined;
1332
- const candidateUiPages = await enumerateCandidateUiPages(params.repositoryPath, changedFrontendFiles);
1333
1375
  // SKYR-3855: deterministic production-importer check, computed here so
1334
1376
  // downstream consumers (testbot prompt) can skip UI generation for
1335
1377
  // unintegrated components on a server fact instead of a mid-run grep.
1378
+ // Computed first: the enumerator resolves changed non-route files to
1379
+ // the pages that import them.
1336
1380
  const frontendFileIntegration = checkFrontendFileIntegration(params.repositoryPath, changedFrontendFiles);
1381
+ const candidateUiPages = await enumerateCandidateUiPages(params.repositoryPath, changedFrontendFiles, frontendFileIntegration);
1337
1382
  return {
1338
1383
  changedFrontendFiles,
1339
1384
  candidateUiPages,
@@ -1370,13 +1415,29 @@ export function registerAnalyzeChangesTool(server) {
1370
1415
  // computeScoredCandidates is the single source of truth for both.
1371
1416
  const topN = params.topN ?? MAX_RECOMMENDATIONS;
1372
1417
  const scoredResult = computeScoredCandidates(fullAnalysis, analysisScope, topN, params.maxGenerate);
1418
+ // Resolve each scenario's subject endpoints once, here — this is the only
1419
+ // point that has the scenarios, the diff, and runs before BOTH consumers
1420
+ // (selectPlan below, and the two prompt renderers). `scored.scenario` is
1421
+ // the SAME object as the entry in `allDraftedScenarios` (assigned by
1422
+ // reference into businessContext.draftedScenarios, then into
1423
+ // repositoryAnalysis.scenarios below), so this mutation also fills what
1424
+ // gets persisted. Filling later would leave one consumer keying off a
1425
+ // different subject (SKYR-4214).
1426
+ const changedRoutesForSubjects = collectChangedRouteLines(diffText ?? "");
1427
+ for (const scored of scoredResult.scored) {
1428
+ scored.scenario.subjectEndpoints = resolveSubjectEndpoints(scored.scenario, {
1429
+ changedRoutes: changedRoutesForSubjects,
1430
+ });
1431
+ }
1373
1432
  const externalCoverage = buildExternalCoverageSet(testLocationsByType);
1433
+ const diffChangesTestFiles = computeDiffChangesTestFiles(diffData, await getUserChangedFiles(params.repositoryPath));
1374
1434
  const planBudgetContext = {
1375
1435
  maxGenerate: scoredResult.maxGen,
1376
1436
  maxTotal: topN,
1377
1437
  isUIOnlyPR: scoredResult.isUIOnlyPR,
1378
1438
  hasFrontendChanges: scoredResult.hasFrontendChanges,
1379
1439
  externalCoverageKeys: [...externalCoverage],
1440
+ diffChangesTestFiles,
1380
1441
  };
1381
1442
  let approvedPlan;
1382
1443
  if (scoredResult.scored.length > 0) {
@@ -1386,6 +1447,7 @@ export function registerAnalyzeChangesTool(server) {
1386
1447
  isUIOnlyPR: planBudgetContext.isUIOnlyPR,
1387
1448
  hasFrontendChanges: planBudgetContext.hasFrontendChanges,
1388
1449
  externalCoverage,
1450
+ diffChangesTestFiles,
1389
1451
  };
1390
1452
  const serverCandidates = scoredResult.scored.map(({ scenario, priority, novelty }) => ({
1391
1453
  scenario,
@@ -1401,6 +1463,7 @@ export function registerAnalyzeChangesTool(server) {
1401
1463
  generate: plan.generate.map(buildApprovedPlanItem),
1402
1464
  additional: plan.additional.map(buildApprovedPlanItem),
1403
1465
  demotions: [],
1466
+ dropped: plan.dropped,
1404
1467
  };
1405
1468
  }
1406
1469
  const unifiedState = {
@@ -175,13 +175,13 @@ export function registerAnalyzeTestHealthTool(server) {
175
175
  // every repo in a multi-repo run (endpoints in Repo A, tests in Repo B).
176
176
  // TODO(multi-repo): existingTests is scoped to the current repo only — related
177
177
  // repo tests are not pre-loaded, discoverable only via the grep instruction.
178
- // Skip the async lookups when there are no tests — symbol discovery is a no-op.
179
- let allRepoPaths = [repositoryPath];
180
- if (existingTests.length > 0) {
181
- const relatedRepoKeys = await stateManager.listRelatedRepos();
182
- const relatedRepoPaths = (await Promise.all(relatedRepoKeys.map(r => stateManager.getRepoRepositoryPath(r)))).filter((p) => typeof p === "string");
183
- allRepoPaths = [repositoryPath, ...relatedRepoPaths];
184
- }
178
+ // Every root in the RUN, not the requested repo plus the related ones: on a
179
+ // related-repo call the latter omits the primary and repeats the requested
180
+ // repo (SKYR-4246). Dedupe by root because a legacy untagged primary can also
181
+ // appear as a related section holding the same path.
182
+ const allRepoPaths = [
183
+ ...new Set((await stateManager.listRepoCheckouts()).map(c => c.root)),
184
+ ];
185
185
  const promptText = buildDriftAnalysisPrompt(stateManager.getStatePath(), apiTests.map((t) => ({ testFile: t.testFile, source: t.source })), uiDriftParams, allRepoPaths, stateData?.externalTestResults);
186
186
  return {
187
187
  structuredContent: { prompt: promptText },
@@ -1,2 +1,205 @@
1
+ import { z } from "zod";
1
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { HttpMethod, TestType } from "../../types/TestTypes.js";
4
+ import { Candidate, DiscriminatorKind } from "../../types/Recommendation.js";
5
+ import { ChangedRoute } from "../../utils/changedRoutes.js";
6
+ declare const registerCandidateSchema: z.ZodObject<{
7
+ scenarioName: z.ZodString;
8
+ description: z.ZodString;
9
+ category: z.ZodEnum<["new_endpoint", "bug_caught", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
10
+ priority: z.ZodEnum<["high", "medium", "low"]>;
11
+ testType: z.ZodEffects<z.ZodNativeEnum<typeof TestType>, TestType, TestType>;
12
+ steps: z.ZodArray<z.ZodObject<{
13
+ order: z.ZodNumber;
14
+ method: z.ZodNativeEnum<typeof HttpMethod>;
15
+ path: z.ZodString;
16
+ description: z.ZodString;
17
+ interactionType: z.ZodEnum<["success", "error", "edge-case"]>;
18
+ requestBody: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
19
+ queryParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
20
+ responseBody: z.ZodOptional<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodArray<z.ZodAny, "many">]>>;
21
+ expectedStatusCode: z.ZodNumber;
22
+ expectedResponseFields: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
23
+ bodyMustInclude: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
24
+ chainsFrom: z.ZodOptional<z.ZodUnion<[z.ZodObject<{
25
+ sourceStep: z.ZodNumber;
26
+ sourceField: z.ZodString;
27
+ sourceLocation: z.ZodEnum<["body", "header", "cookie"]>;
28
+ targetParam: z.ZodString;
29
+ targetLocation: z.ZodEnum<["path", "body", "query", "header", "cookie"]>;
30
+ }, "strip", z.ZodTypeAny, {
31
+ sourceStep: number;
32
+ sourceField: string;
33
+ sourceLocation: "body" | "header" | "cookie";
34
+ targetParam: string;
35
+ targetLocation: "path" | "body" | "header" | "cookie" | "query";
36
+ }, {
37
+ sourceStep: number;
38
+ sourceField: string;
39
+ sourceLocation: "body" | "header" | "cookie";
40
+ targetParam: string;
41
+ targetLocation: "path" | "body" | "header" | "cookie" | "query";
42
+ }>, z.ZodArray<z.ZodObject<{
43
+ sourceStep: z.ZodNumber;
44
+ sourceField: z.ZodString;
45
+ sourceLocation: z.ZodEnum<["body", "header", "cookie"]>;
46
+ targetParam: z.ZodString;
47
+ targetLocation: z.ZodEnum<["path", "body", "query", "header", "cookie"]>;
48
+ }, "strip", z.ZodTypeAny, {
49
+ sourceStep: number;
50
+ sourceField: string;
51
+ sourceLocation: "body" | "header" | "cookie";
52
+ targetParam: string;
53
+ targetLocation: "path" | "body" | "header" | "cookie" | "query";
54
+ }, {
55
+ sourceStep: number;
56
+ sourceField: string;
57
+ sourceLocation: "body" | "header" | "cookie";
58
+ targetParam: string;
59
+ targetLocation: "path" | "body" | "header" | "cookie" | "query";
60
+ }>, "many">]>>;
61
+ }, "strip", z.ZodTypeAny, {
62
+ path: string;
63
+ method: HttpMethod;
64
+ description: string;
65
+ order: number;
66
+ interactionType: "error" | "success" | "edge-case";
67
+ expectedStatusCode: number;
68
+ queryParams?: Record<string, any> | undefined;
69
+ requestBody?: Record<string, any> | undefined;
70
+ responseBody?: any[] | Record<string, any> | undefined;
71
+ expectedResponseFields?: string[] | undefined;
72
+ chainsFrom?: {
73
+ sourceStep: number;
74
+ sourceField: string;
75
+ sourceLocation: "body" | "header" | "cookie";
76
+ targetParam: string;
77
+ targetLocation: "path" | "body" | "header" | "cookie" | "query";
78
+ } | {
79
+ sourceStep: number;
80
+ sourceField: string;
81
+ sourceLocation: "body" | "header" | "cookie";
82
+ targetParam: string;
83
+ targetLocation: "path" | "body" | "header" | "cookie" | "query";
84
+ }[] | undefined;
85
+ bodyMustInclude?: string[] | undefined;
86
+ }, {
87
+ path: string;
88
+ method: HttpMethod;
89
+ description: string;
90
+ order: number;
91
+ interactionType: "error" | "success" | "edge-case";
92
+ expectedStatusCode: number;
93
+ queryParams?: Record<string, any> | undefined;
94
+ requestBody?: Record<string, any> | undefined;
95
+ responseBody?: any[] | Record<string, any> | undefined;
96
+ expectedResponseFields?: string[] | undefined;
97
+ chainsFrom?: {
98
+ sourceStep: number;
99
+ sourceField: string;
100
+ sourceLocation: "body" | "header" | "cookie";
101
+ targetParam: string;
102
+ targetLocation: "path" | "body" | "header" | "cookie" | "query";
103
+ } | {
104
+ sourceStep: number;
105
+ sourceField: string;
106
+ sourceLocation: "body" | "header" | "cookie";
107
+ targetParam: string;
108
+ targetLocation: "path" | "body" | "header" | "cookie" | "query";
109
+ }[] | undefined;
110
+ bodyMustInclude?: string[] | undefined;
111
+ }>, "many">;
112
+ discriminator: z.ZodOptional<z.ZodObject<{
113
+ kind: z.ZodNativeEnum<typeof DiscriminatorKind>;
114
+ changedCodeAnchor: z.ZodString;
115
+ }, "strip", z.ZodTypeAny, {
116
+ kind: DiscriminatorKind;
117
+ changedCodeAnchor: string;
118
+ }, {
119
+ kind: DiscriminatorKind;
120
+ changedCodeAnchor: string;
121
+ }>>;
122
+ }, "strip", z.ZodTypeAny, {
123
+ description: string;
124
+ priority: "high" | "medium" | "low";
125
+ testType: TestType;
126
+ scenarioName: string;
127
+ category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
128
+ steps: {
129
+ path: string;
130
+ method: HttpMethod;
131
+ description: string;
132
+ order: number;
133
+ interactionType: "error" | "success" | "edge-case";
134
+ expectedStatusCode: number;
135
+ queryParams?: Record<string, any> | undefined;
136
+ requestBody?: Record<string, any> | undefined;
137
+ responseBody?: any[] | Record<string, any> | undefined;
138
+ expectedResponseFields?: string[] | undefined;
139
+ chainsFrom?: {
140
+ sourceStep: number;
141
+ sourceField: string;
142
+ sourceLocation: "body" | "header" | "cookie";
143
+ targetParam: string;
144
+ targetLocation: "path" | "body" | "header" | "cookie" | "query";
145
+ } | {
146
+ sourceStep: number;
147
+ sourceField: string;
148
+ sourceLocation: "body" | "header" | "cookie";
149
+ targetParam: string;
150
+ targetLocation: "path" | "body" | "header" | "cookie" | "query";
151
+ }[] | undefined;
152
+ bodyMustInclude?: string[] | undefined;
153
+ }[];
154
+ discriminator?: {
155
+ kind: DiscriminatorKind;
156
+ changedCodeAnchor: string;
157
+ } | undefined;
158
+ }, {
159
+ description: string;
160
+ priority: "high" | "medium" | "low";
161
+ testType: TestType;
162
+ scenarioName: string;
163
+ category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
164
+ steps: {
165
+ path: string;
166
+ method: HttpMethod;
167
+ description: string;
168
+ order: number;
169
+ interactionType: "error" | "success" | "edge-case";
170
+ expectedStatusCode: number;
171
+ queryParams?: Record<string, any> | undefined;
172
+ requestBody?: Record<string, any> | undefined;
173
+ responseBody?: any[] | Record<string, any> | undefined;
174
+ expectedResponseFields?: string[] | undefined;
175
+ chainsFrom?: {
176
+ sourceStep: number;
177
+ sourceField: string;
178
+ sourceLocation: "body" | "header" | "cookie";
179
+ targetParam: string;
180
+ targetLocation: "path" | "body" | "header" | "cookie" | "query";
181
+ } | {
182
+ sourceStep: number;
183
+ sourceField: string;
184
+ sourceLocation: "body" | "header" | "cookie";
185
+ targetParam: string;
186
+ targetLocation: "path" | "body" | "header" | "cookie" | "query";
187
+ }[] | undefined;
188
+ bodyMustInclude?: string[] | undefined;
189
+ }[];
190
+ discriminator?: {
191
+ kind: DiscriminatorKind;
192
+ changedCodeAnchor: string;
193
+ } | undefined;
194
+ }>;
195
+ type RegisterCandidateInput = z.infer<typeof registerCandidateSchema>;
196
+ /** Build the agent-submitted candidates. Discriminator claims are verified
197
+ * later, by {@link applyDiscriminatorClaims}, once the merge has settled which
198
+ * scenario each name resolves to. Exported for the direct unit test in
199
+ * registerTestPlanTool.test.ts — a candidate that does not survive selection
200
+ * never reaches the persisted plan, so its subjectEndpoints fill is not
201
+ * observable through the tool's output, and testing here avoids coupling the
202
+ * fill to what selection happens to keep. */
203
+ export declare function buildAgentCandidates(candidates: RegisterCandidateInput[], changedRoutes: ChangedRoute[]): Candidate[];
2
204
  export declare function registerRegisterTestPlanTool(server: McpServer): void;
205
+ export {};