@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
@@ -0,0 +1,75 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ScreenRouteCandidate } from "../../utils/screenRoutes.js";
3
+ /** Where a bound route parameter came from. `unresolved` never carries a value. */
4
+ export type ParamSource = "currentUrl" | "blueprint" | "unresolved";
5
+ export interface ResolvedParam {
6
+ value: string;
7
+ from: ParamSource;
8
+ }
9
+ export interface ScreenCandidate {
10
+ /** The route with its known parameters filled in, prefixed by `baseUrl` when one
11
+ * is known. Any parameter still in `needs` is left in its pattern spelling. */
12
+ url: string;
13
+ routePattern: string;
14
+ params: Record<string, ResolvedParam>;
15
+ /** Route entry file first, changed file last — the evidence for `routePattern`. */
16
+ mountChain: string[];
17
+ }
18
+ export interface ResolveScreenResult {
19
+ status: "resolved" | "unmounted" | "unknown";
20
+ candidates: ScreenCandidate[];
21
+ /** Route patterns ruled out. When the browser's page is known this is that page's
22
+ * own route: the result then reads as a correction of where the browser is. */
23
+ notOn: string[];
24
+ /** Route parameters nothing could fill. */
25
+ needs: string[];
26
+ summary: string;
27
+ }
28
+ /** Where the browser is, as this server saw it. */
29
+ export interface BrowserLocation {
30
+ url: string;
31
+ source: "currentUrl" | "blueprint";
32
+ }
33
+ /** The path half of a url, or the value itself when it is already a path. */
34
+ export declare function urlPath(raw: string): string;
35
+ /**
36
+ * The parameters the browser's current page supplies, and the route it is on.
37
+ *
38
+ * The binding runs against the CURRENT page's own pattern, and the values carry
39
+ * across by NAME. In the motivating run the browser sat on
40
+ * `/organizations/8f2e6b9f/projects/secret-management/1eb91367/overview`, whose
41
+ * pattern names `$orgId` and `$projectId`; the target route names those two and
42
+ * `$envSlug`, so two are known and one is not.
43
+ *
44
+ * A url matching several patterns is ambiguous, and the most literal one wins: a
45
+ * static segment is a stronger claim about the page than a parameter that accepts
46
+ * anything.
47
+ */
48
+ export declare function bindFromLocation(location: BrowserLocation | undefined, patterns: string[]): {
49
+ pattern: string;
50
+ values: Record<string, string>;
51
+ source: ParamSource;
52
+ } | undefined;
53
+ /**
54
+ * Turn the resolver's three-state answer into the tool's, with parameters bound
55
+ * from wherever the browser is. Pure: every input is passed in, so the shape of the
56
+ * answer is testable without a repository or a browser.
57
+ */
58
+ export declare function buildResolveScreenResult(input: {
59
+ changedFile: string;
60
+ candidates: ScreenRouteCandidate[] | undefined;
61
+ routePatterns: string[];
62
+ location?: BrowserLocation;
63
+ baseUrl?: string;
64
+ }): ResolveScreenResult;
65
+ /**
66
+ * The origin the UI is served from, as `.skyramp/workspace.yml` records it.
67
+ *
68
+ * There is no explicit "this one is the UI" marker in the workspace schema, so the
69
+ * service that runs Playwright is the proxy for it; failing that, an unambiguous
70
+ * single service with a baseUrl. Ambiguity yields nothing rather than a guess —
71
+ * the caller can pass `baseUrl`, and a route path without an origin is still a
72
+ * correct answer.
73
+ */
74
+ export declare function workspaceUiBaseUrl(repositoryPath: string): Promise<string | undefined>;
75
+ export declare function registerResolveScreenTool(server: McpServer): void;
@@ -0,0 +1,289 @@
1
+ /**
2
+ * `skyramp_resolve_screen` — which URL renders this changed file, and how to get
3
+ * there from where the browser already is (SKYR-4341).
4
+ *
5
+ * The dead end this tool ends: an agent recording a UI trace finds the element it
6
+ * is looking for is not in the snapshot, and has nowhere to go. In run 33999747382
7
+ * it went to `browser_evaluate`, twenty-nine times, on a page that never rendered
8
+ * the changed component, and produced no test. The page was wrong, not the
9
+ * selector, and nothing in the run could say so.
10
+ *
11
+ * THE TOOL TAKES NO `currentUrl`. This server forwards every `browser_navigate`
12
+ * and records every blueprint capture, so it knows which page the browser is on;
13
+ * asking the agent instead would take the answer from the party this tool exists
14
+ * to correct. An agent-supplied url can be stale or invented, and the correction
15
+ * would then be built on the mistake.
16
+ *
17
+ * It reports what the resolver knows and never more: `resolved` with routes,
18
+ * `unmounted` when nothing renders the file, `unknown` when the scan cannot speak.
19
+ * A route parameter nothing could fill goes in `needs` — never a guessed value,
20
+ * because a plausible wrong id sends the browser to an empty page that looks real.
21
+ */
22
+ import * as path from "path";
23
+ import { z } from "zod";
24
+ import { TOOL_RESOLVE_SCREEN } from "../../toolNames.js";
25
+ import { logger } from "../../utils/logger.js";
26
+ import { lastKnownBrowserUrl } from "../../playwright/blueprintDigestStore.js";
27
+ import { readWorkspaceServices } from "../../prompts/prompt-utils.js";
28
+ import { createScreenRouteLookup } from "../../utils/screenRoutes.js";
29
+ // ── Route patterns ──────────────────────────────────────────────────────────
30
+ /** A pattern segment, in the four spellings the frameworks this run meets use:
31
+ * TanStack `$id`, React/Vue `:id`, Next `[id]`, OpenAPI-ish `{id}`. Mirrors the
32
+ * `screenRoute` verifier — the two answer the same question about the same
33
+ * patterns, and disagreeing about what a parameter looks like would let one pass
34
+ * what the other objects to. */
35
+ function parameterName(segment) {
36
+ if (segment.startsWith("$"))
37
+ return segment.slice(1) || undefined;
38
+ if (segment.startsWith(":"))
39
+ return segment.slice(1) || undefined;
40
+ if (segment.startsWith("[") && segment.endsWith("]"))
41
+ return segment.slice(1, -1).replace(/^\.\.\./, "") || undefined;
42
+ if (segment.startsWith("{") && segment.endsWith("}"))
43
+ return segment.slice(1, -1) || undefined;
44
+ return undefined;
45
+ }
46
+ function segments(value) {
47
+ return value.split("/").filter(Boolean);
48
+ }
49
+ /** The path half of a url, or the value itself when it is already a path. */
50
+ export function urlPath(raw) {
51
+ const value = (raw ?? "").trim();
52
+ if (!value)
53
+ return "";
54
+ try {
55
+ return new URL(value).pathname.replace(/\/+$/, "") || "/";
56
+ }
57
+ catch {
58
+ return value.startsWith("/") ? value.split(/[?#]/)[0].replace(/\/+$/, "") || "/" : "";
59
+ }
60
+ }
61
+ function origin(raw) {
62
+ try {
63
+ return new URL(raw).origin;
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
69
+ /** Bind a pattern's parameters from a concrete path, or nothing when the path is
70
+ * not that pattern filled in. Depth agrees or nothing does: a parameter matches
71
+ * one segment and never spans two. */
72
+ function bind(pattern, concretePath) {
73
+ const wanted = segments(pattern);
74
+ const actual = segments(concretePath);
75
+ if (wanted.length !== actual.length)
76
+ return undefined;
77
+ const bound = {};
78
+ for (let i = 0; i < wanted.length; i++) {
79
+ const name = parameterName(wanted[i]);
80
+ if (name)
81
+ bound[name] = actual[i];
82
+ else if (wanted[i] !== actual[i])
83
+ return undefined;
84
+ }
85
+ return bound;
86
+ }
87
+ /**
88
+ * The parameters the browser's current page supplies, and the route it is on.
89
+ *
90
+ * The binding runs against the CURRENT page's own pattern, and the values carry
91
+ * across by NAME. In the motivating run the browser sat on
92
+ * `/organizations/8f2e6b9f/projects/secret-management/1eb91367/overview`, whose
93
+ * pattern names `$orgId` and `$projectId`; the target route names those two and
94
+ * `$envSlug`, so two are known and one is not.
95
+ *
96
+ * A url matching several patterns is ambiguous, and the most literal one wins: a
97
+ * static segment is a stronger claim about the page than a parameter that accepts
98
+ * anything.
99
+ */
100
+ export function bindFromLocation(location, patterns) {
101
+ if (!location)
102
+ return undefined;
103
+ const concretePath = urlPath(location.url);
104
+ if (!concretePath)
105
+ return undefined;
106
+ let best;
107
+ for (const pattern of patterns) {
108
+ const values = bind(pattern, concretePath);
109
+ if (!values)
110
+ continue;
111
+ const literals = segments(pattern).filter((segment) => !parameterName(segment)).length;
112
+ if (!best || literals > best.literals)
113
+ best = { pattern, values, literals };
114
+ }
115
+ return best ? { pattern: best.pattern, values: best.values, source: location.source } : undefined;
116
+ }
117
+ /** The pattern with its bound parameters substituted, prefixed by the origin when
118
+ * one is known. An unbound parameter is left in its own spelling rather than
119
+ * dropped: a url missing a segment silently names a different page. */
120
+ function buildUrl(pattern, params, baseUrl) {
121
+ const filled = segments(pattern)
122
+ .map((segment) => {
123
+ const name = parameterName(segment);
124
+ const bound = name ? params[name] : undefined;
125
+ return bound && bound.from !== "unresolved" ? bound.value : segment;
126
+ })
127
+ .join("/");
128
+ const pathPart = "/" + filled;
129
+ const base = baseUrl ? baseUrl.replace(/\/+$/, "") : "";
130
+ return base ? base + pathPart : pathPart;
131
+ }
132
+ // ── The answer ──────────────────────────────────────────────────────────────
133
+ /**
134
+ * Turn the resolver's three-state answer into the tool's, with parameters bound
135
+ * from wherever the browser is. Pure: every input is passed in, so the shape of the
136
+ * answer is testable without a repository or a browser.
137
+ */
138
+ export function buildResolveScreenResult(input) {
139
+ const { changedFile, candidates, routePatterns, location, baseUrl } = input;
140
+ const here = bindFromLocation(location, routePatterns);
141
+ if (candidates === undefined) {
142
+ return {
143
+ status: "unknown",
144
+ candidates: [],
145
+ notOn: [],
146
+ needs: [],
147
+ summary: `No route table this server can read covers ${changedFile}, so it cannot say which page renders it. ` +
148
+ "Find the page yourself; do not treat this as evidence that nothing renders the file.",
149
+ };
150
+ }
151
+ if (candidates.length === 0) {
152
+ return {
153
+ status: "unmounted",
154
+ candidates: [],
155
+ // The current page is ruled out along with every other: nothing mounts it.
156
+ notOn: here ? [here.pattern] : [],
157
+ needs: [],
158
+ summary: `No route in this repository renders ${changedFile}. It is not reachable in the running app, ` +
159
+ "so a browser recording cannot exercise it. Say so in one line and keep the planned test out of the browser.",
160
+ };
161
+ }
162
+ const needs = new Set();
163
+ const resolved = candidates.map((candidate) => {
164
+ const params = {};
165
+ for (const segment of segments(candidate.routePattern)) {
166
+ const name = parameterName(segment);
167
+ if (!name)
168
+ continue;
169
+ const value = here?.values[name];
170
+ if (value !== undefined)
171
+ params[name] = { value, from: here?.source ?? "unresolved" };
172
+ else {
173
+ params[name] = { value: "", from: "unresolved" };
174
+ needs.add(name);
175
+ }
176
+ }
177
+ return {
178
+ url: buildUrl(candidate.routePattern, params, baseUrl),
179
+ routePattern: candidate.routePattern,
180
+ params,
181
+ mountChain: candidate.mountChain,
182
+ };
183
+ });
184
+ const mounts = new Set(resolved.map((candidate) => candidate.routePattern));
185
+ const notOn = here && !mounts.has(here.pattern) ? [here.pattern] : [];
186
+ const lines = [];
187
+ if (notOn.length > 0) {
188
+ lines.push(`The browser is on ${location?.url}, which is ${notOn[0]} — that page does not render ${changedFile}.`);
189
+ lines.push(`Navigate to ${resolved[0].url} instead.`);
190
+ }
191
+ else {
192
+ lines.push(`${changedFile} is rendered on ${resolved.map((c) => c.routePattern).join("; ")}.`);
193
+ }
194
+ if (needs.size > 0) {
195
+ lines.push(`No value is known for ${[...needs].join(", ")}. Read ${needs.size === 1 ? "it" : "them"} off the app with ` +
196
+ "`browser_snapshot`: the value shows up as a link or a list entry in the ARIA tree, so open the parent " +
197
+ "page and snapshot it if the current one does not hold it. Do not dig for it with `browser_evaluate`, " +
198
+ "and do not invent one — a plausible wrong id renders an empty page that looks real.");
199
+ }
200
+ lines.push("`mountChain` is the import path from the route file to the changed file: check it rather than trust it.");
201
+ return { status: "resolved", candidates: resolved, notOn, needs: [...needs], summary: lines.join(" ") };
202
+ }
203
+ // ── Workspace default for baseUrl ───────────────────────────────────────────
204
+ /**
205
+ * The origin the UI is served from, as `.skyramp/workspace.yml` records it.
206
+ *
207
+ * There is no explicit "this one is the UI" marker in the workspace schema, so the
208
+ * service that runs Playwright is the proxy for it; failing that, an unambiguous
209
+ * single service with a baseUrl. Ambiguity yields nothing rather than a guess —
210
+ * the caller can pass `baseUrl`, and a route path without an origin is still a
211
+ * correct answer.
212
+ */
213
+ export async function workspaceUiBaseUrl(repositoryPath) {
214
+ try {
215
+ const services = (await readWorkspaceServices(repositoryPath));
216
+ const withUrl = services.filter((service) => typeof service?.api?.baseUrl === "string" && service.api.baseUrl);
217
+ const playwright = withUrl.filter((service) => service.framework === "playwright");
218
+ const chosen = playwright.length === 1 ? playwright[0] : withUrl.length === 1 ? withUrl[0] : undefined;
219
+ return chosen ? origin(chosen.api?.baseUrl) : undefined;
220
+ }
221
+ catch {
222
+ return undefined;
223
+ }
224
+ }
225
+ // ── Registration ────────────────────────────────────────────────────────────
226
+ const inputSchema = {
227
+ repositoryPath: z.string().describe("Absolute path to the repository checkout that holds the frontend file."),
228
+ changedFile: z
229
+ .string()
230
+ .describe("Repository-relative path of the frontend file the test is about — the file the diff changed, " +
231
+ "not the page you think renders it."),
232
+ baseUrl: z
233
+ .string()
234
+ .optional()
235
+ .describe("Origin the UI is served from, e.g. 'http://localhost:3000'. Defaults to the UI service's " +
236
+ "`api.baseUrl` in `.skyramp/workspace.yml`; when neither is known the returned urls are route paths."),
237
+ };
238
+ const outputSchema = {
239
+ status: z
240
+ .enum(["resolved", "unmounted", "unknown"])
241
+ .describe("resolved — the routes in `candidates` render the file. unmounted — no route renders it, so no browser " +
242
+ "recording can reach it. unknown — no route table could be read; this says nothing about the file."),
243
+ candidates: z
244
+ .array(z.object({
245
+ url: z.string(),
246
+ routePattern: z.string(),
247
+ params: z.record(z.object({ value: z.string(), from: z.enum(["currentUrl", "blueprint", "unresolved"]) })),
248
+ mountChain: z.array(z.string()),
249
+ }))
250
+ .describe("Each route that renders the file, with the import chain from its route file down to the file."),
251
+ notOn: z
252
+ .array(z.string())
253
+ .describe("The route the browser is currently on, when it is not one that renders the file."),
254
+ needs: z.array(z.string()).describe("Route parameters no known value fills. Read them off the app; never invent one."),
255
+ summary: z.string().describe("The same answer in one paragraph."),
256
+ };
257
+ const DESCRIPTION = "Answer which page of the running app renders a changed frontend file, and give the url to open. " +
258
+ "Call it BEFORE hunting for an element that is not in the snapshot: the usual cause is that the browser is " +
259
+ "on the wrong page, and no amount of browser_evaluate finds an element the page never rendered. " +
260
+ "Returns status 'resolved' with candidate urls, 'unmounted' when no route renders the file at all " +
261
+ "(skip the browser and say so), or 'unknown' when no route table could be read (find the page yourself). " +
262
+ "The current page is read from this server's own browser session — do not pass it.";
263
+ export function registerResolveScreenTool(server) {
264
+ server.registerTool(TOOL_RESOLVE_SCREEN, { description: DESCRIPTION, inputSchema, outputSchema }, async (params) => {
265
+ const repositoryPath = String(params.repositoryPath ?? "").trim();
266
+ const changedFile = String(params.changedFile ?? "").trim();
267
+ const location = lastKnownBrowserUrl();
268
+ const baseUrl = (typeof params.baseUrl === "string" && params.baseUrl.trim()
269
+ ? origin(params.baseUrl.trim()) || params.baseUrl.trim()
270
+ : undefined) ??
271
+ (await workspaceUiBaseUrl(repositoryPath)) ??
272
+ (location ? origin(location.url) : undefined);
273
+ const lookup = createScreenRouteLookup(path.resolve(repositoryPath));
274
+ const result = buildResolveScreenResult({
275
+ changedFile,
276
+ candidates: lookup.candidatesFor(changedFile),
277
+ routePatterns: lookup.routePatterns(),
278
+ location,
279
+ baseUrl,
280
+ });
281
+ logger.info("skyramp_resolve_screen answered", {
282
+ changedFile,
283
+ status: result.status,
284
+ routes: result.candidates.length,
285
+ needs: result.needs.length,
286
+ });
287
+ return { structuredContent: { ...result }, content: [{ type: "text", text: result.summary }] };
288
+ });
289
+ }
@@ -0,0 +1,34 @@
1
+ /** What is kept from one `browser_blueprint` capture so a plan-time check can ask
2
+ * whether an element the agent named was really on the page. A DIGEST, never the
3
+ * capture: the largest real one is 26,542 bytes and a run takes many, so only the
4
+ * fields an element can be recognised by are kept. */
5
+ export interface BlueprintElementDigest {
6
+ /** ARIA role, as the capture spells it. */
7
+ role: string;
8
+ /** The element's accessible name. For a REPEATING element the capture carries
9
+ * `accessibleNameTemplate` instead — one entry is kept for the template and one
10
+ * per row with `{param}` filled in, so either spelling is recognised. */
11
+ accessibleName: string;
12
+ /** `data-testid` value. Absent when the element has none — SKYR-4208 removed
13
+ * null-valued keys from the capture, so an absence is what arrives. */
14
+ testId?: string;
15
+ /** Unique `id` attribute value, absent when the element has none. */
16
+ stableId?: string;
17
+ /** Surrounding row text for an element inside a repeating section. It is the
18
+ * only thing that separates two rows whose rendered names collide. */
19
+ contextText?: string[];
20
+ }
21
+ /** One capture, keyed by the page it was taken from. */
22
+ export interface BlueprintCaptureDigest {
23
+ /** The page URL the capture reports. A repeat capture at the same URL carries
24
+ * no URL of its own, so it inherits the one before it. */
25
+ url: string;
26
+ /** The capture's own page hash. Stored so a stale citation can be recognised
27
+ * later; v0 matching does not narrow by it — see `uiElementGrounded`. */
28
+ pageHash?: string;
29
+ elements: BlueprintElementDigest[];
30
+ /** Whether the element cap cut this capture. An ungrounded objection says so,
31
+ * because "no capture holds that element" is not a fact about a page whose
32
+ * elements were only partly kept. */
33
+ elementsTruncated?: boolean;
34
+ }
@@ -0,0 +1 @@
1
+ export {};