@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
@@ -14,6 +14,12 @@
14
14
  * Next.js (app/ and pages/), Nuxt / Vue Router file-based, and SvelteKit.
15
15
  * Gated on detection of a framework config file in the repo.
16
16
  *
17
+ * Strategy 1b — Import graph
18
+ * For a changed file that is not itself a route (a component, a constants
19
+ * module), map the production files that import it through the same
20
+ * filesystem-route mapper. Uses the importer list the production-importer integration
21
+ * check already computed. Merged with strategy 1 rather than tried after it.
22
+ *
17
23
  * Strategy 2 — Source-grounded routes
18
24
  * Walks router/App files with the TS Compiler API for code-defined
19
25
  * route declarations like `<Route path="/cart" element={<Cart/>}>`.
@@ -26,6 +32,10 @@
26
32
  * Strategy 3 — Root fallback
27
33
  * Read the workspace's frontend baseUrl and treat it as the single
28
34
  * candidate page. Always available when a frontend service is configured.
35
+ *
36
+ * When no frontend baseUrl can be resolved, strategies 1/1b still run and their
37
+ * results are returned as bare URL paths (`baseUrlResolved: false`) so the agent
38
+ * can prefix the base URL it discovers; strategies 2–3 need a host and are skipped.
29
39
  */
30
40
  import * as fs from "fs";
31
41
  import * as path from "path";
@@ -33,6 +43,13 @@ import { extractDartRoutes } from "./dartRouteExtractor.js";
33
43
  import { extractSourceRoutes } from "./sourceRouteExtractor.js";
34
44
  import { hasFlutterSdkDep } from "../prompts/test-recommendation/scopeAssessment.js";
35
45
  import { readWorkspaceConfigRaw } from "./workspaceAuth.js";
46
+ /**
47
+ * Upper bound on candidate pages. Strategy 1b is bounded by changed files ×
48
+ * importers, and every candidate costs the agent a navigate + a capture, so the
49
+ * list is cut here — direct route matches first, then import-graph pages that
50
+ * render the most changed files.
51
+ */
52
+ export const MAX_CANDIDATE_PAGES = 10;
36
53
  // ── Strategy 1: framework route grep ────────────────────────────────────────
37
54
  /**
38
55
  * File-system → URL mapping for filesystem-routed frameworks.
@@ -61,8 +78,9 @@ export function frameworkFileToUrlPath(filePath) {
61
78
  return "/" + stripRouteGroups(nextAppMatch[1]).replace(/\[(\.\.\.)?(\w+)\]/g, ":$2");
62
79
  }
63
80
  // Next.js Pages Router: pages/foo.tsx → /foo, pages/index.tsx → /
64
- // Excludes pages/api/* (those are API handlers, not UI pages).
65
- const nextPagesMatch = normalized.match(/(?:^|\/)pages\/(?!api\/)(.+?)\.(tsx|jsx|ts|js)$/);
81
+ // Excludes pages/api/* (API handlers) and the framework files _app, _document,
82
+ // _error (they wrap every page and are not routes of their own).
83
+ const nextPagesMatch = normalized.match(/(?:^|\/)pages\/(?!api\/)(?!_(?:app|document|error)\.)(.+?)\.(tsx|jsx|ts|js)$/);
66
84
  if (nextPagesMatch) {
67
85
  let urlPath = nextPagesMatch[1];
68
86
  // /index → /, /foo/index → /foo
@@ -140,43 +158,64 @@ export function detectsFilesystemRouting(repositoryPath) {
140
158
  return false;
141
159
  }
142
160
  /**
143
- * Strategy 1 implementation. Filters frontend files through the framework
144
- * route mapper; collects the URL paths that mapped successfully.
161
+ * Strategies 1 + 1b: map changed route files (1) and the route files that import
162
+ * changed non-route files (1b) to URL paths. Deduped by path; when both name the
163
+ * same path the strategy-1 entry wins and the extra source is appended.
145
164
  *
146
165
  * Suppressed when the repo doesn't have a recognized filesystem-routing
147
166
  * framework config — see `detectsFilesystemRouting`. Without that guard,
148
167
  * repos with conventional `pages/` directories but code-defined routes
149
168
  * (e.g. React with React Router) produce false-positive URLs.
150
169
  */
151
- export function findCandidatePagesByFrameworkRoute(frontendFiles, baseUrl, repositoryPath) {
170
+ function collectRoutePaths(frontendFiles, integration, repositoryPath) {
152
171
  // When repositoryPath is provided, gate on framework detection. When
153
172
  // omitted (e.g. older callers, unit tests), preserve original behavior.
154
173
  if (repositoryPath !== undefined && !detectsFilesystemRouting(repositoryPath)) {
155
174
  return [];
156
175
  }
157
- const byUrl = new Map();
158
- for (const file of frontendFiles) {
159
- const urlPath = frameworkFileToUrlPath(file);
160
- if (urlPath === null)
161
- continue;
162
- const normalizedBase = baseUrl.replace(/\/$/, "");
163
- const fullUrl = normalizedBase + urlPath;
164
- const existing = byUrl.get(fullUrl);
176
+ const byPath = new Map();
177
+ const add = (urlPath, sourceFile, strategy, via) => {
178
+ const existing = byPath.get(urlPath);
165
179
  if (existing) {
166
- // Same URL surfaced by multiple files — track all sources.
167
- if (!existing.sourcedFrom.includes(file)) {
168
- existing.sourcedFrom.push(file);
169
- }
180
+ // Same path surfaced by multiple files — track all sources.
181
+ if (!existing.sourcedFrom.includes(sourceFile))
182
+ existing.sourcedFrom.push(sourceFile);
183
+ if (via && existing.via && !existing.via.includes(via))
184
+ existing.via.push(via);
170
185
  }
171
186
  else {
172
- byUrl.set(fullUrl, {
173
- url: fullUrl,
174
- sourcedFrom: [file],
175
- strategy: "framework-route-grep",
176
- });
187
+ byPath.set(urlPath, { path: urlPath, sourcedFrom: [sourceFile], strategy, ...(via ? { via: [via] } : {}) });
188
+ }
189
+ };
190
+ for (const file of frontendFiles) {
191
+ const urlPath = frameworkFileToUrlPath(file);
192
+ if (urlPath !== null)
193
+ add(urlPath, file, "framework-route-grep");
194
+ }
195
+ for (const entry of integration ?? []) {
196
+ if (!entry.integrated)
197
+ continue;
198
+ for (const importer of entry.importers) {
199
+ const urlPath = frameworkFileToUrlPath(importer);
200
+ if (urlPath !== null)
201
+ add(urlPath, entry.file, "import-graph", importer);
177
202
  }
178
203
  }
179
- return Array.from(byUrl.values());
204
+ const all = Array.from(byPath.values());
205
+ const rank = (c) => (c.strategy === "framework-route-grep" ? 1 : 0) * 1000 + c.sourcedFrom.length;
206
+ return all.sort((a, b) => rank(b) - rank(a)).slice(0, MAX_CANDIDATE_PAGES);
207
+ }
208
+ function joinBaseUrl(baseUrl, candidate) {
209
+ return {
210
+ url: baseUrl.replace(/\/$/, "") + candidate.path,
211
+ sourcedFrom: candidate.sourcedFrom,
212
+ strategy: candidate.strategy,
213
+ ...(candidate.via ? { via: candidate.via } : {}),
214
+ };
215
+ }
216
+ /** Strategy 1 only (no import graph), joined to `baseUrl`. */
217
+ export function findCandidatePagesByFrameworkRoute(frontendFiles, baseUrl, repositoryPath) {
218
+ return collectRoutePaths(frontendFiles, undefined, repositoryPath).map((c) => joinBaseUrl(baseUrl, c));
180
219
  }
181
220
  // ── Strategy 2: source-grounded routes ──────────────────────────────────────
182
221
  /**
@@ -302,13 +341,10 @@ export function findCandidatePagesByDartRoute(repositoryPath, baseUrl, frontendF
302
341
  */
303
342
  export async function findRootFallbackPage(repositoryPath) {
304
343
  const baseUrl = await pickFrontendBaseUrl(repositoryPath);
305
- if (!baseUrl)
306
- return null;
307
- return {
308
- url: baseUrl,
309
- sourcedFrom: [],
310
- strategy: "root-fallback",
311
- };
344
+ return baseUrl ? rootFallbackPage(baseUrl) : null;
345
+ }
346
+ function rootFallbackPage(baseUrl) {
347
+ return { url: baseUrl, sourcedFrom: [], strategy: "root-fallback" };
312
348
  }
313
349
  /**
314
350
  * Mirror of testbot.git src/services.ts exportServiceBaseUrlEnvVars sanitize().
@@ -369,19 +405,21 @@ function pickFrontendService(services) {
369
405
  * is the picked frontend service's serviceName, sanitized via
370
406
  * `sanitizeServiceName`)
371
407
  * 2. Global override: `SKYRAMP_TEST_BASE_URL` (single service or all
372
- * services share one URL)
408
+ * services share one URL). Also honored when the workspace declares no
409
+ * services at all — the action can know the URL without a workspace entry.
373
410
  * 3. Static workspace.yml value: `api.baseUrl` from the picked frontend
374
411
  * service
375
412
  *
376
- * Returns null when no service has a usable URL.
413
+ * Returns null when neither the environment nor any service has a usable URL.
377
414
  *
378
415
  * Exported (vs private) so analyze_changes and future tools can reuse the
379
416
  * same selection without re-implementing it.
380
417
  */
381
418
  export async function pickFrontendBaseUrl(repositoryPath) {
382
419
  const config = await readWorkspaceConfigRaw(repositoryPath);
420
+ const global = process.env.SKYRAMP_TEST_BASE_URL?.trim();
383
421
  if (!config?.services || !Array.isArray(config.services))
384
- return null;
422
+ return global || null;
385
423
  const picked = pickFrontendService(config.services);
386
424
  // 1. Per-service override (multi-service, distinct URLs).
387
425
  // readWorkspaceConfigRaw may fall back to unvalidated YAML, so serviceName
@@ -392,7 +430,6 @@ export async function pickFrontendBaseUrl(repositoryPath) {
392
430
  return perSvc;
393
431
  }
394
432
  // 2. Global override (single service / all services share a URL).
395
- const global = process.env.SKYRAMP_TEST_BASE_URL?.trim();
396
433
  if (global)
397
434
  return global;
398
435
  // 3. Static workspace.yml value.
@@ -400,31 +437,38 @@ export async function pickFrontendBaseUrl(repositoryPath) {
400
437
  }
401
438
  // ── Composer ────────────────────────────────────────────────────────────────
402
439
  /**
403
- * Run the strategy ladder in order: framework route grep (1) → source-grounded
404
- * routes (2) → root fallback (3). Each strategy is consulted only if the
405
- * previous one produced no candidates. Returns whatever the ladder produces
406
- * (possibly empty if the workspace has no frontend baseUrl AND none of the
407
- * earlier strategies matched).
440
+ * Run the strategy ladder: framework route grep + import graph (1/1b) →
441
+ * source-grounded routes (2) → Dart GoRouter (2.5) → root fallback (3). Each
442
+ * later strategy is consulted only if the earlier ones produced no candidates.
443
+ *
444
+ * `integration` is the production-importer check for the same files; when given,
445
+ * changed non-route files resolve to the pages that import them.
446
+ *
447
+ * Without a resolvable frontend baseUrl only strategies 1/1b can run; their
448
+ * paths are returned with `baseUrlResolved: false` for the agent to prefix.
449
+ * Returns empty only when nothing resolves at all.
408
450
  */
409
- export async function enumerateCandidateUiPages(repositoryPath, frontendFiles) {
451
+ export async function enumerateCandidateUiPages(repositoryPath, frontendFiles, integration) {
410
452
  if (frontendFiles.length === 0)
411
453
  return [];
412
454
  const baseUrl = await pickFrontendBaseUrl(repositoryPath);
413
- // Strategy 1 needs a baseUrl to construct full URLs. Without one, the
414
- // fallback path in strategy 3 also won't fire — return empty and let the
415
- // caller decide what to do (typically: persist nothing, agent does its
416
- // own enumeration via the existing prompt fallback).
417
- if (!baseUrl)
418
- return [];
419
- const fromRoutes = findCandidatePagesByFrameworkRoute(frontendFiles, baseUrl, repositoryPath);
420
- if (fromRoutes.length > 0)
421
- return fromRoutes;
455
+ const routePaths = collectRoutePaths(frontendFiles, integration, repositoryPath);
456
+ if (!baseUrl) {
457
+ return routePaths.map((c) => ({
458
+ url: c.path,
459
+ sourcedFrom: c.sourcedFrom,
460
+ strategy: c.strategy,
461
+ baseUrlResolved: false,
462
+ ...(c.via ? { via: c.via } : {}),
463
+ }));
464
+ }
465
+ if (routePaths.length > 0)
466
+ return routePaths.map((c) => joinBaseUrl(baseUrl, c));
422
467
  const fromSource = findCandidatePagesBySourceRoute(frontendFiles, baseUrl, repositoryPath);
423
468
  if (fromSource.length > 0)
424
469
  return fromSource;
425
470
  const fromDart = findCandidatePagesByDartRoute(repositoryPath, baseUrl, frontendFiles);
426
471
  if (fromDart.length > 0)
427
472
  return fromDart;
428
- const fallback = await findRootFallbackPage(repositoryPath);
429
- return fallback ? [fallback] : [];
473
+ return [rootFallbackPage(baseUrl)];
430
474
  }
@@ -0,0 +1,16 @@
1
+ import type { UtilsLanguageSpec } from "./language-spec.js";
2
+ export type UtilsViolationKind = "duplicate-helper" | "body-assertion" | "scenario-name";
3
+ export declare const UTILS_VIOLATION_KINDS: readonly UtilsViolationKind[];
4
+ export interface UtilsAllow {
5
+ kind: UtilsViolationKind;
6
+ helper: string;
7
+ reason: string;
8
+ }
9
+ /** The exact line to paste for a violation — what the failure text hands the agent. */
10
+ export declare function allowMarkerLine(spec: UtilsLanguageSpec, kind: UtilsViolationKind, helper: string): string;
11
+ export declare function parseUtilsAllows(content: string, spec: UtilsLanguageSpec): {
12
+ allows: UtilsAllow[];
13
+ /** Marker lines the loose pattern recognises but the grammar cannot read — a decline
14
+ * the gate would otherwise drop silently. Reported back so the agent fixes the line. */
15
+ malformed: string[];
16
+ };
@@ -0,0 +1,68 @@
1
+ import { blankStrings } from "./parse.js";
2
+ export const UTILS_VIOLATION_KINDS = [
3
+ "duplicate-helper",
4
+ "body-assertion",
5
+ "scenario-name",
6
+ ];
7
+ /**
8
+ * The documented-decline marker for a utils-file invariant, written in the utils file:
9
+ *
10
+ * # reuse-verify: allow <kind> <helper> — <reason> (Python)
11
+ * // reuse-verify: allow <kind> <helper> — <reason> (TS/JS)
12
+ *
13
+ * ONE grammar, used by both the gate and the report. The POM path's `// kept inline:`
14
+ * marker had two — the gate's regex and the report parser disagreed on the separator
15
+ * and on the file extension — and the gap was a run where the gate blocked execution
16
+ * on declines the report listed correctly (mcp#716, run 30965653402). Here the gate
17
+ * and the report call this one function.
18
+ *
19
+ * The separator is an em/en dash or a hyphen with whitespace on BOTH sides, so a
20
+ * hyphen inside a helper name cannot be mistaken for it. `(?!<reason)` rejects the
21
+ * paste-ready line from the failure text when it was pasted unedited: publishing the
22
+ * placeholder as the stated reason is worse than no marker.
23
+ */
24
+ const ALLOW_RE = /reuse-verify:\s*allow\s+(duplicate-helper|body-assertion|scenario-name)\s+([A-Za-z_$][\w$]*)\s*(?:[—–]+|\s+-+\s+)\s*(?!<reason)(\S.*?)\s*$/;
25
+ const ALLOW_LOOSE_RE = /reuse-verify:\s*allow\b/;
26
+ /** The exact line to paste for a violation — what the failure text hands the agent. */
27
+ export function allowMarkerLine(spec, kind, helper) {
28
+ return `${spec.commentPrefix} reuse-verify: allow ${kind} ${helper} — <reason this helper must stay as it is>`;
29
+ }
30
+ export function parseUtilsAllows(content, spec) {
31
+ const allows = [];
32
+ const malformed = [];
33
+ // String contents are blanked first, so a marker (or a comment token) quoted in
34
+ // code can neither be read as a decline nor make one look like a comment.
35
+ const rawLines = content.split("\n");
36
+ const codeLines = blankStrings(content, spec).split("\n");
37
+ for (let n = 0; n < rawLines.length; n++) {
38
+ const raw = rawLines[n];
39
+ const code = codeLines[n] ?? "";
40
+ // The marker may sit in a trailing comment, a block comment or a JSDoc line — any
41
+ // comment form. Match from the marker itself, so the comment syntax around it does
42
+ // not decide whether a decline is read.
43
+ const at = code.search(ALLOW_LOOSE_RE);
44
+ if (at === -1)
45
+ continue;
46
+ const before = code.slice(0, at);
47
+ const inComment = spec.commentPrefix === "#"
48
+ ? /#/.test(before)
49
+ : /\/\/|\/\*|^\s*\*/.test(before);
50
+ if (!inComment)
51
+ continue;
52
+ const line = raw
53
+ .slice(at)
54
+ .replace(/\s*\*\/\s*$/, "")
55
+ .trim();
56
+ const m = ALLOW_RE.exec(line);
57
+ if (!m) {
58
+ malformed.push(raw.trim());
59
+ continue;
60
+ }
61
+ allows.push({
62
+ kind: m[1],
63
+ helper: m[2],
64
+ reason: m[3],
65
+ });
66
+ }
67
+ return { allows, malformed };
68
+ }
@@ -0,0 +1,34 @@
1
+ import { type UtilsHelper } from "./parse.js";
2
+ import { type UtilsLanguageSpec } from "./language-spec.js";
3
+ /** The codegen marker every Skyramp-GENERATED TEST carries on line 1 (`Generated by
4
+ * Skyramp v<version>`), as opposed to the utils header (`Generated by Skyramp on …`).
5
+ * The same discriminator STEP 4 of the reuse prompt hands the agent. */
6
+ export declare const CODEGEN_MARKER_RE: RegExp;
7
+ export interface InlineCallSite {
8
+ /** Absolute path of the sibling test. */
9
+ file: string;
10
+ /** 1-based line of the request call. */
11
+ line: number;
12
+ method: string;
13
+ path: string;
14
+ /** The utils helper that wraps the same method+path. */
15
+ helper: string;
16
+ }
17
+ /**
18
+ * Inline request calls in OTHER Skyramp-generated tests beside `testFile` that a
19
+ * utils helper already wraps — the reuse the prompt asks for and the agent does not
20
+ * perform (measured 0/2 seeded eval runs with the rule as its own mandatory step).
21
+ *
22
+ * A call is INLINE when it sits in a test function, not in a module-level helper of
23
+ * the sibling's own: a sibling that defines `create_order` locally is STEP 4b's
24
+ * helper-vs-helper case, not a call site. Equivalence is `c3b9157b`'s: same method
25
+ * and same normalised path, nothing else — literals are what the call's arguments
26
+ * are for.
27
+ *
28
+ * Advisory by design: it names a change to a PRE-EXISTING test, which the maintenance
29
+ * flow owns, so it informs rather than gates. Never throws.
30
+ */
31
+ export declare function findSiblingInlineCallSites(testFile: string, utilsFiles: string[], _helpers: UtilsHelper[], spec: UtilsLanguageSpec): Promise<InlineCallSite[]>;
32
+ /** The keyword names of a request call (`path=`, `method=`, `query_params=` … / TS
33
+ * object keys), less the liftable ones — a cheap normalised call shape. */
34
+ export declare function shapeKeys(call: string): Set<string>;
@@ -0,0 +1,154 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { SKYRAMP_UTILS_HEADER } from "../utils.js";
4
+ import { parseUtilsFile } from "./parse.js";
5
+ import { normalizePath } from "./language-spec.js";
6
+ import { realpath } from "./locate.js";
7
+ /** The codegen marker every Skyramp-GENERATED TEST carries on line 1 (`Generated by
8
+ * Skyramp v<version>`), as opposed to the utils header (`Generated by Skyramp on …`).
9
+ * The same discriminator STEP 4 of the reuse prompt hands the agent. */
10
+ export const CODEGEN_MARKER_RE = new RegExp(`${SKYRAMP_UTILS_HEADER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+v\\d`);
11
+ const HEADER_SCAN_LINES = 5;
12
+ const REQUEST_CALL_RE = /\b(?:send_request|sendRequest)\s*\(/g;
13
+ /**
14
+ * Inline request calls in OTHER Skyramp-generated tests beside `testFile` that a
15
+ * utils helper already wraps — the reuse the prompt asks for and the agent does not
16
+ * perform (measured 0/2 seeded eval runs with the rule as its own mandatory step).
17
+ *
18
+ * A call is INLINE when it sits in a test function, not in a module-level helper of
19
+ * the sibling's own: a sibling that defines `create_order` locally is STEP 4b's
20
+ * helper-vs-helper case, not a call site. Equivalence is `c3b9157b`'s: same method
21
+ * and same normalised path, nothing else — literals are what the call's arguments
22
+ * are for.
23
+ *
24
+ * Advisory by design: it names a change to a PRE-EXISTING test, which the maintenance
25
+ * flow owns, so it informs rather than gates. Never throws.
26
+ */
27
+ export async function findSiblingInlineCallSites(testFile, utilsFiles, _helpers, spec) {
28
+ // route → helper name + the keyword set of the helper's own request call, so a
29
+ // sibling call is reported only when its SHAPE matches too (same keywords once the
30
+ // liftable ones are set aside) — criterion 2 of the API near-duplicate definition.
31
+ const byRoute = new Map();
32
+ for (const utilsFile of utilsFiles) {
33
+ let content;
34
+ try {
35
+ content = await fs.promises.readFile(utilsFile, "utf8");
36
+ }
37
+ catch {
38
+ continue;
39
+ }
40
+ // Owners come from THIS file's own parse: `helpers` spans every utils file, and
41
+ // a line range alone would attribute file B's request to file A's helper.
42
+ const fileHelpers = parseUtilsFile(content, spec);
43
+ for (const m of content.matchAll(REQUEST_CALL_RE)) {
44
+ const start = m.index ?? 0;
45
+ const line = content.slice(0, start).split("\n").length;
46
+ const owner = fileHelpers.find((h) => h.line <= line && line <= h.endLine);
47
+ if (!owner?.method || !owner.normalizedPath)
48
+ continue;
49
+ const call = callText(content, start + m[0].length - 1);
50
+ byRoute.set(`${owner.method} ${owner.normalizedPath}`, {
51
+ helper: owner.name,
52
+ keys: shapeKeys(call),
53
+ });
54
+ }
55
+ }
56
+ if (byRoute.size === 0)
57
+ return [];
58
+ // Canonical, like the utils paths: otherwise a tool-supplied /tmp path and a
59
+ // realpath'd /private/tmp utils file look like different directories to the renderer.
60
+ const dir = path.dirname(await realpath(testFile));
61
+ // Canonical like `dir`, or a spec reached through a symlink is reported as its
62
+ // own sibling.
63
+ const exclude = new Set(await Promise.all([testFile, ...utilsFiles].map((f) => realpath(f))));
64
+ let entries;
65
+ try {
66
+ entries = await fs.promises.readdir(dir);
67
+ }
68
+ catch {
69
+ return [];
70
+ }
71
+ const out = [];
72
+ for (const name of entries.sort()) {
73
+ const file = path.join(dir, name);
74
+ if (exclude.has(path.resolve(file)))
75
+ continue;
76
+ if (!spec.extensions.some((e) => name.toLowerCase().endsWith(e)))
77
+ continue;
78
+ let content;
79
+ try {
80
+ content = await fs.promises.readFile(file, "utf8");
81
+ }
82
+ catch {
83
+ continue;
84
+ }
85
+ if (!CODEGEN_MARKER_RE.test(content.split("\n").slice(0, HEADER_SCAN_LINES).join("\n")))
86
+ continue;
87
+ // Line ranges of the sibling's own non-test helpers: calls inside them are not inline.
88
+ const localHelpers = parseUtilsFile(content, spec).filter((h) => !/^test/i.test(h.name));
89
+ const inLocalHelper = (line) => localHelpers.some((h) => line >= h.line && line <= h.endLine);
90
+ for (const m of content.matchAll(REQUEST_CALL_RE)) {
91
+ const start = m.index ?? 0;
92
+ const line = content.slice(0, start).split("\n").length;
93
+ if (inLocalHelper(line))
94
+ continue;
95
+ const call = callText(content, start + m[0].length - 1);
96
+ const method = spec.methodRe.exec(call)?.[1]?.toUpperCase();
97
+ const p = spec.pathRe.exec(call)?.[1];
98
+ if (!method || !p)
99
+ continue;
100
+ const match = byRoute.get(`${method} ${normalizePath(p)}`);
101
+ if (!match)
102
+ continue;
103
+ if (!sameShape(shapeKeys(call), match.keys))
104
+ continue;
105
+ out.push({ file, line, method, path: p, helper: match.helper });
106
+ }
107
+ }
108
+ return out;
109
+ }
110
+ /** Keywords whose presence differs legitimately between two versions of one request:
111
+ * the literals the merge rule lifts to parameters. Everything else is call shape. */
112
+ const LIFTABLE_KEYS = new Set([
113
+ "body",
114
+ "data",
115
+ "data_override",
116
+ "dataOverride",
117
+ "headers",
118
+ "url",
119
+ "path_params",
120
+ "pathParams",
121
+ ]);
122
+ /** The keyword names of a request call (`path=`, `method=`, `query_params=` … / TS
123
+ * object keys), less the liftable ones — a cheap normalised call shape. */
124
+ export function shapeKeys(call) {
125
+ const keys = new Set();
126
+ for (const m of call.matchAll(/\b([A-Za-z_]\w*)\s*[=:](?![=])/g)) {
127
+ if (!LIFTABLE_KEYS.has(m[1]))
128
+ keys.add(m[1]);
129
+ }
130
+ return keys;
131
+ }
132
+ function sameShape(a, b) {
133
+ if (a.size !== b.size)
134
+ return false;
135
+ for (const k of a)
136
+ if (!b.has(k))
137
+ return false;
138
+ return true;
139
+ }
140
+ /** Text of the parenthesised argument list starting at the `(` at `open`. */
141
+ function callText(content, open) {
142
+ let depth = 0;
143
+ for (let i = open; i < content.length; i++) {
144
+ const ch = content[i];
145
+ if (ch === "(")
146
+ depth++;
147
+ else if (ch === ")") {
148
+ depth--;
149
+ if (depth === 0)
150
+ return content.slice(open, i + 1);
151
+ }
152
+ }
153
+ return content.slice(open);
154
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./language-spec.js";
2
+ export * from "./parse.js";
3
+ export * from "./allow.js";
4
+ export * from "./locate.js";
5
+ export * from "./verify.js";
6
+ export * from "./stage.js";
7
+ export * from "./call-sites.js";
@@ -0,0 +1,7 @@
1
+ export * from "./language-spec.js";
2
+ export * from "./parse.js";
3
+ export * from "./allow.js";
4
+ export * from "./locate.js";
5
+ export * from "./verify.js";
6
+ export * from "./stage.js";
7
+ export * from "./call-sites.js";
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Language-parametric description of a shared SkyrampUtils file, and the invariants
3
+ * every helper in it must hold. Encoded as DATA rather than as per-language checks so
4
+ * that adding a language (or a helper family with a different assertion rule, e.g.
5
+ * browser helpers) is a table entry, not a second verifier.
6
+ *
7
+ * The invariants come from the prompts that author these files — the HELPER SHAPE
8
+ * block in the integration modularization prompt and the API near-duplicate rules in
9
+ * the reuse prompt — and exist here because prompt rules are LLM-enforced only:
10
+ * SKYR-4196 Part B runs 5 and 11 leaked a scenario-literal body assertion into a shared
11
+ * helper despite both rules being in the prompt.
12
+ */
13
+ export type UtilsLanguage = "python" | "typescript" | "javascript";
14
+ export interface UtilsLanguageSpec {
15
+ language: UtilsLanguage;
16
+ /** File extensions (with dot) a utils file in this language can carry. */
17
+ extensions: string[];
18
+ commentPrefix: "#" | "//";
19
+ /** Matches the FIRST line of a module-level helper definition; group 1 is the name. */
20
+ helperStart: RegExp;
21
+ /** `method="POST"` / `method: "POST"` inside a helper body; group 1 is the verb. */
22
+ methodRe: RegExp;
23
+ /** `path="/api/…"` / `path: "/…"` inside a helper body; group 1 is the path. */
24
+ pathRe: RegExp;
25
+ /** A line that is an assertion at all. */
26
+ assertionRe: RegExp;
27
+ /** The one assertion shape an API helper may carry: a status-code comparison. Any
28
+ * assertion line not matching this is a body assertion — the scenario-specific
29
+ * claim that fails the next scenario calling the helper. */
30
+ allowedAssertion: RegExp;
31
+ /** The assertions a BROWSER helper may carry: structural waits that an element
32
+ * rendered / is interactable (`toBeVisible`, `toBeEnabled`, …). Value assertions
33
+ * (`toHaveText`, `toHaveValue`, `toHaveCount`, `toHaveURL`, any `expect` on a
34
+ * literal) are the browser analogue of a body assertion. Mirrors the
35
+ * BROWSER_HELPERS policy the modularization and reuse prompts state. */
36
+ allowedBrowserAssertion: RegExp;
37
+ /** Names `specContent` imports from the utils module whose basename stem is `stem`. */
38
+ importedNames(specContent: string, stem: string): string[];
39
+ }
40
+ /** Path parameters spelled `{id}`, `${id}`, `:id` all normalise to `{}` so two helpers
41
+ * for `/orders/{order_id}` and `/orders/{id}` compare equal. */
42
+ export declare function normalizePath(p: string): string;
43
+ export declare const UTILS_LANGUAGE_SPECS: Record<UtilsLanguage, UtilsLanguageSpec>;
44
+ /**
45
+ * The two kinds of shared helper, keyed by the test type's substrate — the same
46
+ * split the modularization and reuse prompts make (API_HELPERS / BROWSER_HELPERS):
47
+ * - `api` (integration, contract, load …): one SDK request; equivalence is
48
+ * method+path; the only assertion is the status code.
49
+ * - `browser` (ui, e2e): a Playwright action sequence; no route to key duplicates
50
+ * on, so the duplicate check is inert; the only assertions are structural waits.
51
+ */
52
+ export type HelperFamily = "api" | "browser";
53
+ export interface HelperFamilySpec {
54
+ family: HelperFamily;
55
+ /** Whether two helpers wrapping the same method+path are a violation. */
56
+ routeDuplicates: boolean;
57
+ /** Picks the permitted-assertion pattern out of a language spec. */
58
+ allowedAssertion(spec: UtilsLanguageSpec): RegExp;
59
+ /** Whether one complete assertion expression is permitted for this family. */
60
+ isAllowed(spec: UtilsLanguageSpec, expr: string): boolean;
61
+ /** One clause for the failure text: what a helper of this family may assert. */
62
+ assertionRule: string;
63
+ }
64
+ export declare const HELPER_FAMILIES: Record<HelperFamily, HelperFamilySpec>;
65
+ /** The helper family a test type's shared helpers follow. Unlike `isBrowserTestType`,
66
+ * an ABSENT test type resolves to `api`: this runs against a utils file that already
67
+ * exists, and the browser rules are the more permissive of the two on duplicates, so
68
+ * guessing "browser" would silently switch a check off. */
69
+ export declare function helperFamilyFor(testType: string | undefined): HelperFamilySpec;
70
+ /** The spec for a language string as the tools receive it, or `undefined` for a
71
+ * language this verifier does not cover (Java: utils reuse is disabled for it). */
72
+ export declare function utilsSpecFor(language: string | undefined): UtilsLanguageSpec | undefined;
73
+ /** The spec whose extensions cover `file`, for callers that know a path but not a language. */
74
+ export declare function utilsSpecForFile(file: string): UtilsLanguageSpec | undefined;
75
+ /**
76
+ * Helper names are `<verb>_<resource>` only (`create_order`, `update_order`); scenario
77
+ * data belongs in arguments. A name carrying a scenario adjective or a literal is the
78
+ * signature of a helper that will not merge with the same step from the next test
79
+ * (`update_order_50_discount` beside `update_order_discount`, SKYR-4196 run 4).
80
+ *
81
+ * Tokens are matched after splitting on `_` and camelCase boundaries, so the rule
82
+ * applies unchanged to Python and TS/JS names.
83
+ */
84
+ export declare const SCENARIO_NAME_RULE: {
85
+ forbiddenTokens: Set<string>;
86
+ /** A bare number as its OWN token — `update_order_50_discount` — never a digit inside
87
+ * an identifier (`s3`, `oauth2`, `ipv4`, `sha256`, `base64`, `iso8601`, `2fa`). */
88
+ digitToken: RegExp;
89
+ };
90
+ /** The reason a name is scenario-flavoured, or `undefined` when it is canonical. */
91
+ export declare function scenarioNameViolation(name: string): string | undefined;