@skyramp/mcp 0.3.5 → 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 (157) 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 +80 -34
  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 +411 -114
  60. package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
  61. package/build/tools/test-management/analyzeChangesTool.js +71 -10
  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 +70 -12
  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 +10 -1
  73. package/build/types/index.d.ts +2 -2
  74. package/build/types/index.js +1 -1
  75. package/build/utils/AnalysisStateManager.d.ts +57 -1
  76. package/build/utils/AnalysisStateManager.js +54 -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/reportVerification.d.ts +64 -4
  101. package/build/utils/reportVerification.js +228 -3
  102. package/build/utils/reuseRouting.d.ts +3 -0
  103. package/build/utils/reuseRouting.js +50 -0
  104. package/build/utils/routeParsers.d.ts +2 -0
  105. package/build/utils/routeParsers.js +65 -8
  106. package/build/utils/scenarioDrafting.d.ts +1 -1
  107. package/build/utils/scenarioDrafting.js +57 -45
  108. package/build/utils/subjectEndpoints.d.ts +19 -0
  109. package/build/utils/subjectEndpoints.js +98 -0
  110. package/build/utils/testFileClassification.d.ts +11 -0
  111. package/build/utils/testFileClassification.js +47 -0
  112. package/build/utils/uiPageEnumerator.d.ts +45 -19
  113. package/build/utils/uiPageEnumerator.js +95 -51
  114. package/build/utils/utils-verify/allow.d.ts +16 -0
  115. package/build/utils/utils-verify/allow.js +68 -0
  116. package/build/utils/utils-verify/call-sites.d.ts +34 -0
  117. package/build/utils/utils-verify/call-sites.js +154 -0
  118. package/build/utils/utils-verify/index.d.ts +7 -0
  119. package/build/utils/utils-verify/index.js +7 -0
  120. package/build/utils/utils-verify/language-spec.d.ts +91 -0
  121. package/build/utils/utils-verify/language-spec.js +210 -0
  122. package/build/utils/utils-verify/locate.d.ts +39 -0
  123. package/build/utils/utils-verify/locate.js +199 -0
  124. package/build/utils/utils-verify/parse.d.ts +34 -0
  125. package/build/utils/utils-verify/parse.js +177 -0
  126. package/build/utils/utils-verify/stage.d.ts +24 -0
  127. package/build/utils/utils-verify/stage.js +107 -0
  128. package/build/utils/utils-verify/verify.d.ts +63 -0
  129. package/build/utils/utils-verify/verify.js +168 -0
  130. package/build/utils/utils.d.ts +3 -1
  131. package/build/utils/utils.js +3 -1
  132. package/build/workspace/workspace.d.ts +32 -32
  133. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +9 -5
  134. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +16 -0
  135. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -0
  136. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +115 -14
  137. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +13 -1
  138. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
  139. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -0
  140. package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +27 -253
  141. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-DtudTj_v.js → codeMirrorModule-DJMC4zNo.js} +1 -1
  142. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BW82eAUI.js +196 -0
  143. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
  144. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-FNMuBzX1.js → codeMirrorModule-CZfp96qZ.js} +1 -1
  145. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-gpLo02E0.js +809 -0
  146. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.Bq1r1URj.js +2 -0
  147. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  148. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.VEfqi1qN.js +5 -0
  149. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  150. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  151. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  152. package/node_modules/playwright/package.json +1 -1
  153. package/package.json +2 -2
  154. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +0 -422
  155. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +0 -1035
  156. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +0 -2
  157. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +0 -5
@@ -0,0 +1,98 @@
1
+ import { findStepOnChangedRoute } from "./changedRoutes.js";
2
+ import { extractResourceFromPath, isParamSegment } from "./routeParsers.js";
3
+ const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
4
+ /**
5
+ * A final-position DELETE that removes a resource an earlier POST created is
6
+ * cleanup, not the subject. SKYR-4214 measured the cost of counting it: the
7
+ * same fixture keyed as POST::flows on one run and DELETE::flows on the next,
8
+ * decided only by whether the model appended a cleanup step.
9
+ *
10
+ * The step count matters. A two-step POST-then-DELETE has no room for a
11
+ * verification step, so the DELETE is what the test is about; the cleanup shape
12
+ * always has a step between the create and the delete. A scenario that asserts
13
+ * the delete worked (…DELETE, then GET 404) leaves the DELETE off the final
14
+ * position, so it is not excluded either.
15
+ */
16
+ function isTeardownStep(steps, index) {
17
+ const step = steps[index];
18
+ if (index !== steps.length - 1)
19
+ return false;
20
+ if ((step.method ?? "").toUpperCase() !== "DELETE")
21
+ return false;
22
+ if (steps.length < 3)
23
+ return false;
24
+ const resource = extractResourceFromPath(step.path ?? "");
25
+ // "unknown" is the sentinel for a path that names no resource, so comparing
26
+ // it by equality makes any two unidentifiable paths look like the same
27
+ // resource: `POST /` … `DELETE /{id}` read as create-then-cleanup and the
28
+ // DELETE — the real subject — got skipped. Only a named resource can pair.
29
+ if (resource === "unknown")
30
+ return false;
31
+ return steps.slice(0, index).some((earlier) => (earlier.method ?? "").toUpperCase() === "POST" &&
32
+ extractResourceFromPath(earlier.path ?? "") === resource);
33
+ }
34
+ /**
35
+ * A route declaration made only of parameter segments (or none at all, e.g.
36
+ * "/" or "") names no resource, so it cannot tell one endpoint from another —
37
+ * every step's path ends in the same param segment. SKYR-4214: a changed
38
+ * `@router.delete("/{id}")` line matched a step's trailing DELETE cleanup
39
+ * step just as well as the step it was actually meant to catch, which
40
+ * reinstated the bug this file exists to fix. Rule 1 only accepts a route
41
+ * that contributes at least one non-parameter segment.
42
+ */
43
+ function routeIdentifiesResource(routePath) {
44
+ return routePath.split("/").filter(Boolean).some((segment) => !isParamSegment(segment));
45
+ }
46
+ /**
47
+ * The endpoints a scenario tests, in step order.
48
+ *
49
+ * 1. Every step on a changed diff line that names a resource. A test written
50
+ * for a pull request is about what the pull request changed, and a
51
+ * scenario can exercise more than one changed endpoint.
52
+ * 2. If no step matches: the last mutating step that is not a teardown step.
53
+ * Earlier mutations are usually setup (POST /products before PATCH /orders).
54
+ * 3. If no mutating step remains: the last step.
55
+ *
56
+ * Returns an empty list only for a scenario with no steps. Every caller must
57
+ * treat an empty list as "no information", never as "covered".
58
+ */
59
+ export function resolveSubjectEndpoints(scenario, opts) {
60
+ const steps = scenario.steps ?? [];
61
+ if (steps.length === 0)
62
+ return [];
63
+ const changedRoutes = (opts.changedRoutes ?? []).filter((route) => routeIdentifiesResource(route.path));
64
+ if (changedRoutes.length > 0) {
65
+ // Attribute PER ROUTE, not per step. A diff-extracted route is local to its
66
+ // own router declaration — the mount prefix lives in another file, so a bare
67
+ // `POST /items` suffix-matches a step on `/orders/{id}/items` and one on
68
+ // `/products/{id}/items` equally. Recording both would invent a subject the
69
+ // diff does not name, and dedup then needs every unrelated key covered
70
+ // before the candidate can be removed.
71
+ //
72
+ // A route matching exactly one step names that step. A route matching
73
+ // several names none of them: there is no evidence for choosing, and a
74
+ // guess is worse than an omission. Other routes still contribute, so a
75
+ // scenario that genuinely tests two changed endpoints keeps both subjects.
76
+ const attributed = [];
77
+ for (const route of changedRoutes) {
78
+ const matches = steps.filter((step) => findStepOnChangedRoute([step], [route]));
79
+ const distinct = new Map(matches.map((step) => [`${step.method} ${step.path}`, step]));
80
+ if (distinct.size === 1)
81
+ attributed.push([...distinct.values()][0]);
82
+ }
83
+ // Keep step order and drop the duplicates two routes can both name.
84
+ const onChanged = steps.filter((step) => attributed.includes(step));
85
+ if (onChanged.length > 0) {
86
+ return onChanged.map((step) => ({ method: step.method, path: step.path }));
87
+ }
88
+ }
89
+ for (let i = steps.length - 1; i >= 0; i--) {
90
+ if (!MUTATING_METHODS.has((steps[i].method ?? "").toUpperCase()))
91
+ continue;
92
+ if (isTeardownStep(steps, i))
93
+ continue;
94
+ return [{ method: steps[i].method, path: steps[i].path }];
95
+ }
96
+ const last = steps[steps.length - 1];
97
+ return [{ method: last.method, path: last.path }];
98
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The single definition of "discovery treats this path as a test file", shared by
3
+ * discovery itself and by callers that must agree with what discovery classified as
4
+ * external coverage. A narrower predicate silently disagrees: `tests/test_orders.py`
5
+ * is external coverage here but is not a test file to `isTestFile` in
6
+ * scopeAssessment.ts, which only knows the Skyramp and JS/TS spec spellings.
7
+ *
8
+ * Accepts absolute or repo-relative paths. The directory patterns need a leading
9
+ * separator, which a repo-relative path has not got, so one is prepended.
10
+ */
11
+ export declare function isDiscoveredTestFile(filePath: string): boolean;
@@ -0,0 +1,47 @@
1
+ import * as path from "path";
2
+ // Test file naming patterns — match against basename.
3
+ // Includes Skyramp-generated suffixes so that files like orders_smoke.py placed
4
+ // outside a recognized test directory are still discovered.
5
+ const TEST_FILE_PATTERNS = [
6
+ /^test_.*\.(py|js|ts|rb|go|php)$/, // test_*.py, test_*.rb, test_*.go
7
+ /.*_test\.(py|ts|js|go|rs)$/, // *_test.py, *_test.go, *_test.rs
8
+ /.*\.test\.(ts|js|tsx|jsx)$/, // *.test.ts, *.test.js, *.test.tsx
9
+ /.*\..*spec\.(ts|js|tsx|jsx|rb)$/, // *.spec.ts, *.e2e-spec.ts, *.unit-spec.ts
10
+ /.*Test\.(java|kt|kts|cs|scala|swift|m)$/, // *Test.java, *Test.kt, *Test.m (ObjC)
11
+ /.*Tests\.(cs|swift|m)$/, // *Tests.cs, *Tests.swift, *Tests.m (ObjC)
12
+ /.*_spec\.rb$/, // *_spec.rb (RSpec)
13
+ /.*\.test\.php$/, // *.test.php
14
+ /.*Test\.php$/, // *Test.php (PHPUnit)
15
+ // Skyramp-generated test suffixes — must be matched even outside test dirs
16
+ /.*_(?:smoke|contract|fuzz|integration|load|e2e|ui)\.(py|ts|js|java|rb|go|cs|kt|kts|scala|swift|php|rs)$/i,
17
+ ];
18
+ // Directory patterns that signal test-owned files — works with both / and \ separators.
19
+ // Includes standard test dirs, snapshot dirs, and common helper/fixture dirs used by
20
+ // Playwright, Cypress, and other frameworks for page objects and support code.
21
+ const TEST_DIR_PATTERNS = [
22
+ /[\\/]tests?[\\/]/,
23
+ /[\\/]__tests__[\\/]/,
24
+ /[\\/]__snapshots__[\\/]/, // Jest/Vitest snapshot directories
25
+ /[\\/]spec[\\/]/,
26
+ /[\\/]e2e[\\/]/,
27
+ /[\\/]cypress[\\/]/, // Cypress support, fixtures, plugins
28
+ /[\\/]playwright[\\/]/, // Playwright fixtures and helpers
29
+ /[\\/]page-?objects?[\\/]/, // page-objects/, page-object/, pageObjects/
30
+ /[\\/]fixtures[\\/](?!db[\\/]|seed[\\/]|migrations[\\/])/, // test fixture files (not db/seed/migration fixtures)
31
+ /[\\/]cypress[\\/]support[\\/]/, // Cypress support directory (scoped to avoid matching app support/ dirs)
32
+ ];
33
+ /**
34
+ * The single definition of "discovery treats this path as a test file", shared by
35
+ * discovery itself and by callers that must agree with what discovery classified as
36
+ * external coverage. A narrower predicate silently disagrees: `tests/test_orders.py`
37
+ * is external coverage here but is not a test file to `isTestFile` in
38
+ * scopeAssessment.ts, which only knows the Skyramp and JS/TS spec spellings.
39
+ *
40
+ * Accepts absolute or repo-relative paths. The directory patterns need a leading
41
+ * separator, which a repo-relative path has not got, so one is prepended.
42
+ */
43
+ export function isDiscoveredTestFile(filePath) {
44
+ if (TEST_FILE_PATTERNS.some((p) => p.test(path.basename(filePath))))
45
+ return true;
46
+ return TEST_DIR_PATTERNS.some((p) => p.test(`/${filePath}`));
47
+ }
@@ -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,16 +32,38 @@
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
- export type CandidateUiPageStrategy = "framework-route-grep" | "source-grounded-routes" | "dart-go-router" | "root-fallback";
40
+ import type { FrontendFileIntegration } from "../types/FrontendIntegration.js";
41
+ export type CandidateUiPageStrategy = "framework-route-grep" | "import-graph" | "source-grounded-routes" | "dart-go-router" | "root-fallback";
31
42
  export interface CandidateUiPage {
32
- /** Fully qualified URL (workspace baseUrl + framework-derived path). */
43
+ /**
44
+ * Fully qualified URL (workspace baseUrl + framework-derived path), or a bare
45
+ * URL path when `baseUrlResolved` is false.
46
+ */
33
47
  url: string;
34
48
  /** Source files that suggested this page (for traceability). */
35
49
  sourcedFrom: string[];
36
50
  /** Which strategy enumerated it. */
37
51
  strategy: CandidateUiPageStrategy;
52
+ /**
53
+ * Present (false) only when no frontend baseUrl could be resolved and `url`
54
+ * is a bare path the agent must prefix. Absent means `url` is complete.
55
+ */
56
+ baseUrlResolved?: false;
57
+ /** import-graph only: the route files that import `sourcedFrom` and produced this path. */
58
+ via?: string[];
38
59
  }
60
+ /**
61
+ * Upper bound on candidate pages. Strategy 1b is bounded by changed files ×
62
+ * importers, and every candidate costs the agent a navigate + a capture, so the
63
+ * list is cut here — direct route matches first, then import-graph pages that
64
+ * render the most changed files.
65
+ */
66
+ export declare const MAX_CANDIDATE_PAGES = 10;
39
67
  /**
40
68
  * File-system → URL mapping for filesystem-routed frameworks.
41
69
  *
@@ -63,15 +91,7 @@ export declare function frameworkFileToUrlPath(filePath: string): string | null;
63
91
  * and the `frontendFiles[]`-derived candidate frontend roots.
64
92
  */
65
93
  export declare function detectsFilesystemRouting(repositoryPath: string): boolean;
66
- /**
67
- * Strategy 1 implementation. Filters frontend files through the framework
68
- * route mapper; collects the URL paths that mapped successfully.
69
- *
70
- * Suppressed when the repo doesn't have a recognized filesystem-routing
71
- * framework config — see `detectsFilesystemRouting`. Without that guard,
72
- * repos with conventional `pages/` directories but code-defined routes
73
- * (e.g. React with React Router) produce false-positive URLs.
74
- */
94
+ /** Strategy 1 only (no import graph), joined to `baseUrl`. */
75
95
  export declare function findCandidatePagesByFrameworkRoute(frontendFiles: string[], baseUrl: string, repositoryPath?: string): CandidateUiPage[];
76
96
  /**
77
97
  * Strategy 2 implementation. Walks router/App files with the TS Compiler API
@@ -126,21 +146,27 @@ export declare function findRootFallbackPage(repositoryPath: string): Promise<Ca
126
146
  * is the picked frontend service's serviceName, sanitized via
127
147
  * `sanitizeServiceName`)
128
148
  * 2. Global override: `SKYRAMP_TEST_BASE_URL` (single service or all
129
- * services share one URL)
149
+ * services share one URL). Also honored when the workspace declares no
150
+ * services at all — the action can know the URL without a workspace entry.
130
151
  * 3. Static workspace.yml value: `api.baseUrl` from the picked frontend
131
152
  * service
132
153
  *
133
- * Returns null when no service has a usable URL.
154
+ * Returns null when neither the environment nor any service has a usable URL.
134
155
  *
135
156
  * Exported (vs private) so analyze_changes and future tools can reuse the
136
157
  * same selection without re-implementing it.
137
158
  */
138
159
  export declare function pickFrontendBaseUrl(repositoryPath: string): Promise<string | null>;
139
160
  /**
140
- * Run the strategy ladder in order: framework route grep (1) → source-grounded
141
- * routes (2) → root fallback (3). Each strategy is consulted only if the
142
- * previous one produced no candidates. Returns whatever the ladder produces
143
- * (possibly empty if the workspace has no frontend baseUrl AND none of the
144
- * earlier strategies matched).
161
+ * Run the strategy ladder: framework route grep + import graph (1/1b) →
162
+ * source-grounded routes (2) → Dart GoRouter (2.5) → root fallback (3). Each
163
+ * later strategy is consulted only if the earlier ones produced no candidates.
164
+ *
165
+ * `integration` is the production-importer check for the same files; when given,
166
+ * changed non-route files resolve to the pages that import them.
167
+ *
168
+ * Without a resolvable frontend baseUrl only strategies 1/1b can run; their
169
+ * paths are returned with `baseUrlResolved: false` for the agent to prefix.
170
+ * Returns empty only when nothing resolves at all.
145
171
  */
146
- export declare function enumerateCandidateUiPages(repositoryPath: string, frontendFiles: string[]): Promise<CandidateUiPage[]>;
172
+ export declare function enumerateCandidateUiPages(repositoryPath: string, frontendFiles: string[], integration?: FrontendFileIntegration[]): Promise<CandidateUiPage[]>;
@@ -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>;