@skyramp/mcp 0.1.8 → 0.2.0-rc.2

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 (122) hide show
  1. package/build/index.js +4 -2
  2. package/build/playwright/registerPlaywrightTools.js +12 -0
  3. package/build/playwright/traceRecordingPrompt.js +15 -0
  4. package/build/prompts/code-reuse.js +106 -7
  5. package/build/prompts/pom-aware-code-reuse.js +106 -7
  6. package/build/prompts/startTraceCollectionPrompts.js +37 -15
  7. package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -31
  8. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +40 -1
  9. package/build/prompts/test-maintenance/driftAnalysisSections.js +90 -86
  10. package/build/prompts/test-recommendation/analysisOutputPrompt.js +286 -163
  11. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -45
  12. package/build/prompts/test-recommendation/diffExecutionPlan.js +246 -117
  13. package/build/prompts/test-recommendation/promptPlan.js +290 -0
  14. package/build/prompts/test-recommendation/promptPlan.test.js +336 -0
  15. package/build/prompts/test-recommendation/recommendationSections.js +4 -3
  16. package/build/prompts/test-recommendation/recommendationShared.js +23 -1
  17. package/build/prompts/test-recommendation/scopeAssessment.js +65 -14
  18. package/build/prompts/test-recommendation/scopeAssessment.test.js +93 -2
  19. package/build/prompts/test-recommendation/test-recommendation-prompt.js +36 -12
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +316 -1
  21. package/build/prompts/testbot/testbot-prompts.js +73 -13
  22. package/build/prompts/testbot/testbot-prompts.test.js +114 -1
  23. package/build/resources/testbotResource.js +1 -1
  24. package/build/services/ScenarioGenerationService.integration.test.js +158 -0
  25. package/build/services/ScenarioGenerationService.js +47 -4
  26. package/build/services/ScenarioGenerationService.test.js +158 -22
  27. package/build/services/TestExecutionService.js +73 -15
  28. package/build/services/TestExecutionService.test.js +105 -0
  29. package/build/services/TestGenerationService.js +11 -1
  30. package/build/tools/executeSkyrampTestTool.js +1 -10
  31. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
  32. package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
  33. package/build/tools/generate-tests/generateUIRestTool.js +2 -0
  34. package/build/tools/test-management/actionsTool.js +152 -63
  35. package/build/tools/test-management/analyzeChangesTool.js +178 -64
  36. package/build/tools/test-management/analyzeChangesTool.test.js +103 -16
  37. package/build/tools/test-management/analyzeTestHealthTool.js +30 -81
  38. package/build/tools/test-management/index.js +1 -0
  39. package/build/tools/test-management/uiAnalyzeChangesTool.js +149 -0
  40. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +100 -0
  41. package/build/tools/trace/resolveSaveStoragePath.js +16 -0
  42. package/build/tools/trace/resolveSaveStoragePath.test.js +17 -0
  43. package/build/tools/trace/resolveSessionPaths.js +39 -0
  44. package/build/tools/trace/resolveSessionPaths.test.js +103 -0
  45. package/build/tools/trace/sessionState.js +14 -0
  46. package/build/tools/trace/sessionState.test.js +17 -0
  47. package/build/tools/trace/startTraceCollectionTool.js +84 -14
  48. package/build/tools/trace/stopTraceCollectionTool.js +9 -2
  49. package/build/types/TestAnalysis.js +50 -0
  50. package/build/types/TestRecommendation.js +6 -58
  51. package/build/types/TestTypes.js +1 -1
  52. package/build/utils/AnalysisStateManager.js +22 -11
  53. package/build/utils/branchDiff.js +11 -2
  54. package/build/utils/docker.test.js +1 -1
  55. package/build/utils/gitStaging.js +52 -3
  56. package/build/utils/gitStaging.test.js +19 -1
  57. package/build/utils/repoScanner.js +18 -10
  58. package/build/utils/repoScanner.test.js +92 -0
  59. package/build/utils/routeParsers.js +180 -25
  60. package/build/utils/routeParsers.test.js +180 -1
  61. package/build/utils/scenarioDrafting.js +220 -17
  62. package/build/utils/scenarioDrafting.test.js +182 -9
  63. package/build/utils/sourceRouteExtractor.js +806 -0
  64. package/build/utils/sourceRouteExtractor.test.js +565 -0
  65. package/build/utils/uiPageEnumerator.js +319 -0
  66. package/build/utils/uiPageEnumerator.test.js +422 -0
  67. package/build/utils/utils.js +27 -0
  68. package/build/utils/versions.js +1 -1
  69. package/build/utils/workspaceAuth.js +33 -4
  70. package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
  71. package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
  72. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1210 -0
  73. package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
  74. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
  75. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
  76. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +254 -0
  77. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +304 -0
  78. package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
  79. package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
  80. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
  81. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
  82. package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
  83. package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
  84. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
  85. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
  86. package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
  87. package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
  88. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
  89. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
  90. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.js +150 -0
  91. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.test.js +470 -0
  92. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
  93. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
  94. package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
  95. package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
  96. package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
  97. package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
  98. package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
  99. package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
  100. package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
  101. package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
  102. package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
  103. package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
  104. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
  105. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +146 -0
  106. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +140 -0
  107. package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
  108. package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
  109. package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
  110. package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
  111. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
  112. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
  113. package/node_modules/playwright/package.json +1 -1
  114. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
  115. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
  116. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
  117. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
  118. package/package.json +3 -3
  119. package/build/services/TestHealthService.js +0 -694
  120. package/build/services/TestHealthService.test.js +0 -241
  121. package/build/types/TestDriftAnalysis.js +0 -1
  122. package/build/types/TestHealth.js +0 -4
@@ -1,31 +1,74 @@
1
1
  import { AnalysisScope } from "../../types/RepositoryAnalysis.js";
2
- // File extensions that indicate a frontend-only file — no API route definitions.
3
- // Uses extensions rather than path-segment patterns so backend files named
4
- // e.g. Spring /services/Foo.java or Gin handlers/main.go are not misclassified.
5
- // Keep this narrow: .json/.md can contain backend-affecting artifacts (OpenAPI specs,
6
- // config, API docs) and would incorrectly classify a PR as UI-only.
7
- const FRONTEND_EXT = /\.(tsx?|jsx?|vue|svelte|css|scss|less|html|svg)$/i;
8
- /**
9
- * Returns a Step 1.5 instruction block that forces the LLM to read the
10
- * entry-point files from the Router Mounting section and build an authoritative
11
- * filefull-prefix table before touching any endpoint URLs.
12
- * Returned as an empty string when no router context is available.
13
- */
14
- function buildPathResolutionTableStep(p) {
15
- // Case A: spec was fetched successfully — instruct LLM to validate paths against it
2
+ import { PromptPlan } from "./promptPlan.js";
3
+ import { isFrontendFile } from "./scopeAssessment.js";
4
+ import { readDiffFile } from "../../utils/utils.js";
5
+ // ── Step label constants ─────────────────────────────────────────────────────
6
+ // Derived from the PromptPlan declaration below. Non-conditional steps export
7
+ // static string constants (safe to use in tests without passing params).
8
+ // The one conditional sub-step (TRACE_CALLERS) exports a function accessor.
9
+ //
10
+ // Labels:
11
+ // READ_FILES "1" (main step, always present)
12
+ // RESOLVE_PATHS "1.1" (sub-step of 1, always present)
13
+ // EXTRACT → "2" (main step, always present)
14
+ // TRACE_CALLERS → "2.1" (sub-step of 2, conditional — use getStepTraceCallers())
15
+ // DRAFT → "3" (main step, always present)
16
+ // CALL_TOOL → "4" (main step, always present)
17
+ // Extensions that indicate a backend code file (has symbols/functions to trace).
18
+ // Used to filter unmatchedFiles before emitting the trace-callers sub-step.
19
+ const BACKEND_CODE_EXT = /\.(ts|js|mjs|cjs|py|java|kt|rb|go|cs|php|rs|scala|swift|c|cpp|h|hpp)$/i;
20
+ // ── Inline note for Java Spring route resolution ──────────────────────────────
21
+ // Appended to any step where the LLM reads Java source files. Java Spring has no
22
+ // router-mounting file — each controller defines its own class-level prefix, and
23
+ // that prefix may reference a constant defined elsewhere.
24
+ const JAVA_SPRING_NOTE = `For Java Spring: full URL = class-level \`@RequestMapping\` prefix + method-level path. If the prefix is a constant reference (e.g. \`@RequestMapping(Url.PAGE_URL)\`), find the constant — same file, inner class, or a separate \`Url.java\` — and resolve it (including \`+\` concatenation).`;
25
+ const INLINE_DIFF_LIMIT = 12_000;
26
+ function _deriveContext(p) {
27
+ const diffFiles = p.parsedDiff?.changedFiles ?? [];
28
+ const preDetectedEndpoints = !!(p.parsedDiff &&
29
+ (p.parsedDiff.newEndpoints.length > 0 ||
30
+ p.parsedDiff.modifiedEndpoints.length > 0 ||
31
+ (p.parsedDiff.removedEndpoints?.length ?? 0) > 0));
32
+ const isUIOnly = diffFiles.length > 0 &&
33
+ !preDetectedEndpoints &&
34
+ diffFiles.every((f) => isFrontendFile(f));
35
+ const canInline = !!(p.diffContent && p.diffContent.length <= INLINE_DIFF_LIMIT);
36
+ return { diffFiles, preDetectedEndpoints, isUIOnly, canInline };
37
+ }
38
+ // ── Private step title / body helpers ────────────────────────────────────────
39
+ // Each function receives AnalysisOutputParams and returns either the step title
40
+ // string or the step body text (WITHOUT the "### Step N: Title" header — that is
41
+ // added by PromptPlan.render()). Using function declarations so they are hoisted
42
+ // and can be referenced in the _plan definition below.
43
+ function _readFilesBody(p) {
44
+ const changedFiles = p.parsedDiff?.changedFiles.join(", ") ?? "";
45
+ const { canInline } = _deriveContext(p);
46
+ const diffFileRef = canInline
47
+ ? `\n<diff>\n${p.diffContent}\n</diff>\n`
48
+ : p.diffFilePath
49
+ ? `\n**Full diff file**: \`${p.diffFilePath}\` — **you MUST read this file before proceeding to Step ${ANALYSIS_STEP_EXTRACT}.** It contains the complete unified diff for this PR.\n`
50
+ : "";
51
+ return `${changedFiles}${diffFileRef}`;
52
+ }
53
+ function _resolvePathsTitle(p) {
54
+ if (p.wsSchemaPath && p.specFetchSucceeded) {
55
+ return "Validate all endpoint paths against the OpenAPI spec";
56
+ }
57
+ if (p.routerMountContext.length) {
58
+ return "Build path resolution table";
59
+ }
60
+ return "Verify endpoint paths from source files";
61
+ }
62
+ function _resolvePathsBody(p) {
16
63
  if (p.wsSchemaPath && p.specFetchSucceeded) {
17
- return `### Step 1.5: Validate all endpoint paths against the OpenAPI spec
18
- Fetch \`${p.wsSchemaPath}\` and extract all keys from \`spec.paths\`.
64
+ return `Fetch \`${p.wsSchemaPath}\` and extract all keys from \`spec.paths\`.
19
65
  **Before placing any path in a tool call**, confirm it exists in that list.
20
66
  If a path is NOT in the spec **and it did not come from the PR diff**, find the correct spelling by matching resource name — do NOT use it unverified.
21
- Paths the PR explicitly added or modified may not yet appear in the spec (spec lag) — treat those as valid.
22
- `;
67
+ Paths the PR explicitly added or modified may not yet appear in the spec (spec lag) — treat those as valid.`;
23
68
  }
24
- // Case B: no spec (or spec unreachable) but router mount context available
25
69
  if (p.routerMountContext.length) {
26
70
  const hasInlined = (p.routerFileContents?.length ?? 0) > 0;
27
- return `### Step 1.5: Build path resolution table
28
- ${hasInlined
71
+ return `${hasInlined
29
72
  ? "The **Routing entry-point files** section above contains the inlined file contents — use them directly to trace every router mount call"
30
73
  : "The **Routing entry-point files** section above lists the files to read.\n\n**Read each of those files** and trace every router mount call"} to understand nesting — the pattern varies by framework but the structure is universal: a parent attaches a child router with an optional extra prefix segment. If a prefix is a variable (e.g. \`prefix=api_prefix\`), resolve the variable's value by reading the assignment or the config/settings file it comes from. Examples of what to look for (non-exhaustive):
31
74
  - Python (FastAPI/Flask): \`parent.include_router(child, prefix="...")\`, \`app.register_blueprint(...)\`
@@ -41,88 +84,28 @@ Chain all segments from the app root down through every intermediate mount to ea
41
84
  |-------------|----------------|
42
85
  | (leaf router file) | (fully chained prefix, e.g. /api/v1/products/{id}/reviews) |
43
86
 
44
- **This table is authoritative.** Before placing any URL in a tool call, look up the source file. If the pre-built catalog shows a different path, use the table value.
45
-
46
- `;
87
+ **This table is authoritative.** Before placing any URL in a tool call, look up the source file. If the pre-built catalog shows a different path, use the table value.`;
47
88
  }
48
- // Case C: no spec AND no router context — source-verify fallback
49
- // Note: also fires when a spec was configured (wsSchemaPath set) but could not be
50
- // fetched at analysis time (specFetchSucceeded = false). When that happens the LLM
51
- // should know a spec was expected so it can be extra-skeptical about path correctness.
52
89
  const specFailedNote = p.wsSchemaPath && !p.specFetchSucceeded
53
90
  ? `\n> ⚠️ A spec was configured (\`${p.wsSchemaPath}\`) but could not be loaded at analysis time — treat all paths as unverified until confirmed against source.`
54
91
  : "";
55
- return `### Step 1.5: Verify endpoint paths from source files
56
- The endpoint catalog below was produced by static regex analysis and is **unverified**.
92
+ return `The endpoint catalog below was produced by static regex analysis and is **unverified**.
57
93
  Before using any path in a tool call, read the route definition file identified in the "Source" column and confirm the path string exactly.
58
- Pay special attention to mount prefixes — a router at \`/api/v1\` + route \`/version\` → path is \`/api/v1/version\`, not \`/api/server-version\`.
59
- ${specFailedNote}
60
- `;
94
+ Pay special attention to mount prefixes — a router at \`/api/v1\` + route \`/version\` → path is \`/api/v1/version\`, not \`/api/server-version\`.${specFailedNote}`;
61
95
  }
62
- // Inline note added to any step where the LLM reads Java source files. Java Spring
63
- // has no router-mounting file each controller defines its own class-level prefix,
64
- // and that prefix may reference a constant defined elsewhere.
65
- const JAVA_SPRING_NOTE = `For Java Spring: full URL = class-level \`@RequestMapping\` prefix + method-level path. If the prefix is a constant reference (e.g. \`@RequestMapping(Url.PAGE_URL)\`), find the constant — same file, inner class, or a separate \`Url.java\` — and resolve it (including \`+\` concatenation).`;
66
- function buildEnrichmentInstructions(p) {
67
- const isDiffScope = p.analysisScope === AnalysisScope.CurrentBranchDiff;
68
- const useHealthFlow = p.nextTool === "skyramp_analyze_test_health";
69
- if (!isDiffScope) {
70
- const nextStep = useHealthFlow
71
- ? `### Step 3: Identify tests at risk of drift
72
- Call \`skyramp_analyze_test_health\` with \`stateFile: "${p.stateFile ?? p.sessionId}"\``
73
- : `### Step 3: Present the catalog
74
- The ranked test recommendation catalog is pre-built and shown below (after the separator line).
75
-
76
- **Your only job is to present it.**
77
-
78
- 1. Fill in every \`<…from source>\` placeholder using the field names, computed formulas, and auth details you found in Steps 1–2.
79
- 2. Output the completed catalog **exactly as formatted — grouped by test type (### E2E / ### UI / ### Integration / ### Contract)**. Do NOT restructure, reorder, rename sections, or generate a new format.
80
- 3. Do NOT call any Skyramp generation tools. The catalog shows ready-to-use tool calls that can be executed on demand.
81
-
82
- **If** Steps 1–2 revealed additional scenarios the catalog does not cover (e.g. a computed formula or FK relationship that was missed), you may optionally call \`skyramp_recommend_tests\` with \`stateFile: "${p.stateFile ?? p.sessionId}"\` and \`enrichedScenarios\` to regenerate a more complete catalog — but only after presenting the current one.`;
83
- const hasJavaFiles = p.candidateRouteFiles?.some(f => /\.(java|kt)$/.test(f)) ?? false;
84
- const routeFilesSection = p.candidateRouteFiles && p.candidateRouteFiles.length > 0
85
- ? `\nRoute/controller files found by static scan (read these to discover endpoints — the regex-based catalog below may be incomplete for your framework):\n${p.candidateRouteFiles.map(f => `- ${f}`).join("\n")}\n`
86
- : "";
87
- const resolvePathsNote = p.routerMountContext.length
88
- ? `**Resolve nested paths** using your Step 1.5 table — a router in the table with prefix \`/api/v1/products/{product_id}/reviews\` means every endpoint in that file lives under that full path.`
89
- : `**Resolve full paths** using the prefixes you identified in Step 1 (e.g. Java Spring class-level \`@RequestMapping\` prefix + method-level path).`;
90
- return `## Your Task — Fill in and Present the Catalog (full repo)
91
-
92
- ### Step 1: Read key files
93
- ${routeFilesSection}Read the route/controller files above **and** model/schema files (Pydantic models, Zod schemas, DTOs) to find: required request body fields, computed response fields and formulas, auth middleware type, storage backend, and how sub-routers are mounted (cross-check against Router Mounting section above).
94
- ${hasJavaFiles ? JAVA_SPRING_NOTE : ""}
95
- If the endpoint catalog below is missing endpoints visible in these files (e.g. from a framework the static scanner doesn't recognise), extract them now and include them in Step 3's \`enrichedScenarios\`.
96
-
97
- ${buildPathResolutionTableStep(p)}### Step 2: Map cross-resource relationships and resolve endpoint paths
98
- (Distinct from Step 1 — Step 1 reads individual schemas; Step 2 maps how endpoints relate to each other.)
99
- For each endpoint: which POST creates resources consumed by other endpoints?
100
- ${resolvePathsNote}
101
- For GET list endpoints: identify query params (\`limit\`, \`offset\`, \`order\`, \`orderBy\`) from framework annotations (FastAPI \`Query()\`, Express \`req.query\`, etc.).
102
-
103
- ${nextStep}`;
104
- }
105
- const changedFiles = p.parsedDiff?.changedFiles.join(", ") ?? "";
106
- // Whether the scanner found API endpoints in any changed file.
107
- const preDetectedEndpoints = p.parsedDiff && (p.parsedDiff.newEndpoints.length > 0 || p.parsedDiff.modifiedEndpoints.length > 0 || (p.parsedDiff.removedEndpoints?.length ?? 0) > 0);
108
- const diffFiles = p.parsedDiff?.changedFiles ?? [];
109
- const isUIOnly = diffFiles.length > 0 &&
110
- !preDetectedEndpoints &&
111
- diffFiles.every(f => FRONTEND_EXT.test(f));
112
- const diffHasJavaFiles = diffFiles.some(f => /\.(java|kt)$/.test(f));
113
- // Inline small diffs so the LLM sees them without a tool call. Large diffs
114
- // stay as a temp file reference to avoid bloating the prompt.
115
- const INLINE_DIFF_LIMIT = 12_000; // chars — roughly 300 lines
116
- const canInline = p.diffContent && p.diffContent.length <= INLINE_DIFF_LIMIT;
117
- const diffFileRef = canInline
118
- ? `\n<diff>\n${p.diffContent}\n</diff>\n`
119
- + (p.diffFilePath ? `Full diff also available at \`${p.diffFilePath}\`.\n` : "")
120
- : p.diffFilePath
121
- ? `\n**Full diff file**: \`${p.diffFilePath}\` — **you MUST read this file before proceeding to Step 2.** It contains the complete unified diff for this PR.\n`
122
- : "";
123
- const step2 = isUIOnly
124
- ? `### Step 2: Identify consumed API endpoints and integration status
125
- UI-only PR — perform two checks:
96
+ function _extractTitle(p) {
97
+ const { isUIOnly, canInline } = _deriveContext(p);
98
+ if (isUIOnly)
99
+ return "Identify consumed API endpoints and integration status";
100
+ if (canInline || p.diffFilePath)
101
+ return "Extract new, modified, and removed API endpoints from the diff";
102
+ return "Extract new, modified, and removed API endpoints from source files";
103
+ }
104
+ function _extractBody(p) {
105
+ const { diffFiles, isUIOnly, canInline, preDetectedEndpoints } = _deriveContext(p);
106
+ const diffHasJavaFiles = diffFiles.some((f) => /\.(java|kt)$/.test(f));
107
+ if (isUIOnly) {
108
+ return `UI-only PR perform two checks:
126
109
  1. Read changed frontend files to find API calls (fetch, axios, hooks).
127
110
  2. For each changed component file (skip CSS/HTML/style-only files — they have no exported component name to search for): check whether any production source file imports, re-exports, or renders it.
128
111
  - Search for both the component's exported name AND its module path/filename to catch aliased and default imports (e.g. \`import Foo from './CartLine'\`).
@@ -132,10 +115,16 @@ UI-only PR — perform two checks:
132
115
  If no production file imports, re-exports, or renders a changed component, mark it as **unintegrated** in the Execution Plan output.
133
116
  Exception: if the same PR also adds a route/page file (e.g. under Next.js \`pages/\` or \`app/\`) that imports the component, the route IS the integration point — do NOT mark it as unintegrated.
134
117
  Do NOT apply the unintegrated heuristic to route/entrypoint files themselves — those are always reachable by convention.
135
- An unintegrated non-route component has no DOM node in the running app and cannot be browser-tested — it qualifies as a dead-code / unintegrated-component no-surface PR regardless of how complex the component logic is.`
136
- : (canInline || p.diffFilePath)
137
- ? `### Step 2: Extract new, modified, and removed API endpoints from the diff
138
- ${canInline ? "Read the `<diff>` above" : `Read the diff file at \`${p.diffFilePath}\``} and identify every new or modified API endpoint — route registrations, handler methods, controller annotations. Then use the **Router Mounting / Nesting** section above to reconstruct the full URL path for each endpoint by chaining all parent router prefixes down to the handler (e.g. a handler in a file with \`prefix="/reviews"\` that is mounted at \`/{product_id}\` under a router mounted at \`/api/v1/products\` full path \`/api/v1/products/{product_id}/reviews\`).
118
+ An unintegrated non-route component has no DOM node in the running app and cannot be browser-tested — it qualifies as a dead-code / unintegrated-component no-surface PR regardless of how complex the component logic is.
119
+
120
+ **While reading, also note these patterns** (tag findings with their category for Step ${ANALYSIS_STEP_DRAFT}):
121
+ - **Unique constraints**: \`@unique\`, \`unique: true\`, unique indexes, \`.refine()\` uniqueness checks, \`UNIQUE\` in SQL migrations_data_integrity_
122
+ - **Cascade deletes**: \`ON DELETE CASCADE\`, \`.onDelete("cascade")\`, manual cascade logic in delete handlers → _data_integrity_
123
+ - **Permission checks**: auth middleware, ownership guards (\`req.user.id === resource.ownerId\`), role-based access control, \`isOwner\` assertions → _security_boundary_
124
+ - **Breaking changes in diff**: route renames, deleted route definitions, auth header changes, removed required fields, changed status codes → _breaking_change_`;
125
+ }
126
+ if (canInline || p.diffFilePath) {
127
+ return `${canInline ? "Read the \`<diff>\` above" : `Read the diff file at \`${p.diffFilePath}\``} and identify every new or modified API endpoint — route registrations, handler methods, controller annotations. Then use the **Router Mounting / Nesting** section above to reconstruct the full URL path for each endpoint by chaining all parent router prefixes down to the handler (e.g. a handler in a file with \`prefix="/reviews"\` that is mounted at \`/{product_id}\` under a router mounted at \`/api/v1/products\` → full path \`/api/v1/products/{product_id}/reviews\`).
139
128
  ${diffHasJavaFiles ? JAVA_SPRING_NOTE : ""}
140
129
  For each endpoint found: note the HTTP method, full path, and source file.
141
130
  ${preDetectedEndpoints ? "The endpoint catalog above already lists some changed endpoints — verify and augment with anything it missed." : "No endpoints were pre-detected in the changed files — extract them from the diff."}
@@ -143,54 +132,55 @@ ${preDetectedEndpoints ? "The endpoint catalog above already lists some changed
143
132
  **CRITICAL — Query params vs body:** For GET endpoints (especially search/filter/list),
144
133
  identify which parameters are URL query params vs request body. Look at framework-specific
145
134
  annotations (FastAPI \`Query()\`, Express \`req.query\`, Spring \`@RequestParam\`, etc.).
146
- Pass these as \`queryParams\` (not \`requestBody\`) when generating scenarios.`
147
- : `### Step 2: Extract new, modified, and removed API endpoints from source files
148
- No diff was available read the changed source files listed above directly to identify new or modified API endpoints. Use the **Router Mounting / Nesting** section to reconstruct full paths.
135
+ Pass these as \`queryParams\` (not \`requestBody\`) when generating scenarios.
136
+
137
+ **While reading, also note these patterns** (tag findings with their category for Step ${ANALYSIS_STEP_DRAFT}):
138
+ - **Unique constraints**: \`@unique\`, \`unique: true\`, unique indexes, \`.refine()\` uniqueness checks, \`UNIQUE\` in SQL migrations → _data_integrity_
139
+ - **Cascade deletes**: \`ON DELETE CASCADE\`, \`.onDelete("cascade")\`, manual cascade logic in delete handlers → _data_integrity_
140
+ - **Permission checks**: auth middleware, ownership guards (\`req.user.id === resource.ownerId\`), role-based access control, \`isOwner\` assertions → _security_boundary_
141
+ - **Breaking changes in diff**: route renames, deleted route definitions, auth header changes, removed required fields, changed status codes → _breaking_change_`;
142
+ }
143
+ return `No diff was available — read the changed source files listed above directly to identify new or modified API endpoints. Use the **Router Mounting / Nesting** section to reconstruct full paths.
149
144
  ${diffHasJavaFiles ? JAVA_SPRING_NOTE : ""}
150
145
  For each endpoint found: note the HTTP method, full path, and source file.
151
- Also compare against the endpoint catalog to identify any endpoints that appear in the catalog but are no longer present in the source files — these are removed endpoints.`;
152
- // Step 2.3: Caller-tracing instruction — only emitted when the PR touches backend code
153
- // files that contain no route annotations (utilities, helpers, services). Tells the LLM
154
- // to search for callers of the changed functions to find the actual HTTP surface
155
- // rather than falling back to the proximity-scanned CRUD endpoints. (Bug 5 fix)
156
- //
157
- // We filter out:
158
- // - Frontend component files (.jsx/.tsx/.vue/.svelte) — UI changes have no callers
159
- // in the HTTP graph; emitting this block for them produces irrelevant instructions.
160
- // - Non-code files (docs, config, assets, lockfiles) they have no "changed symbols"
161
- // to trace and listing them as bullets is misleading.
162
- const BACKEND_CODE_EXT = /\.(ts|js|mjs|cjs|py|java|kt|rb|go|cs|php|rs|scala|swift|c|cpp|h|hpp)$/i;
163
- const traceableUnmatched = (p.unmatchedFiles ?? []).filter(f => BACKEND_CODE_EXT.test(f));
164
- const callerTracingStep = isDiffScope && !isUIOnly && traceableUnmatched.length > 0
165
- ? `
166
- ### Step 2.3: Trace callers of changed non-route files
167
- The following changed files contain **no HTTP endpoint registrations** (no route annotations, controller mappings, or handler decorators). Their changes will only be tested if you find and target the HTTP endpoints that *call* them:
146
+ Also compare against the endpoint catalog to identify any endpoints that appear in the catalog but are no longer present in the source files — these are removed endpoints.
147
+
148
+ **While reading, also note these patterns** (tag findings with their category for Step ${ANALYSIS_STEP_DRAFT}):
149
+ - **Unique constraints**: \`@unique\`, \`unique: true\`, unique indexes, \`.refine()\` uniqueness checks, \`UNIQUE\` in SQL migrations → _data_integrity_
150
+ - **Cascade deletes**: \`ON DELETE CASCADE\`, \`.onDelete("cascade")\`, manual cascade logic in delete handlers → _data_integrity_
151
+ - **Permission checks**: auth middleware, ownership guards (\`req.user.id === resource.ownerId\`), role-based access control, \`isOwner\` assertions → _security_boundary_
152
+ - **Breaking changes in diff**: route renames, deleted route definitions, auth header changes, removed required fields, changed status codes → _breaking_change_`;
153
+ }
154
+ function _traceCallersCondition(p) {
155
+ // Only emit in diff scopethe plan may be preview()'d with full-repo params.
156
+ const isDiffScope = p.analysisScope === AnalysisScope.CurrentBranchDiff;
157
+ const { isUIOnly } = _deriveContext(p);
158
+ const traceableUnmatched = (p.unmatchedFiles ?? []).filter((f) => BACKEND_CODE_EXT.test(f));
159
+ return isDiffScope && !isUIOnly && traceableUnmatched.length > 0;
160
+ }
161
+ function _traceCallersBody(p) {
162
+ const traceableUnmatched = (p.unmatchedFiles ?? []).filter((f) => BACKEND_CODE_EXT.test(f));
163
+ return `The following changed files contain **no HTTP endpoint registrations** (no route annotations, controller mappings, or handler decorators). Their changes will only be tested if you find and target the HTTP endpoints that *call* them:
168
164
 
169
- ${traceableUnmatched.map(f => `- \`${f}\``).join("\n")}
165
+ ${traceableUnmatched.map((f) => `- \`${f}\``).join("\n")}
170
166
 
171
167
  For each file above:
172
168
  1. **Find the changed symbols** — read the diff (or the file) to identify which functions, methods, or classes were modified.
173
169
  2. **Search for callers** — look for import statements and call sites of those symbols across service, handler, and controller files. Use fully qualified names (e.g. \`DataUtils.addFileData\`, not just \`addFileData\`) to avoid false matches in large monorepos.
174
170
  3. **Trace to HTTP registration** — from each caller, follow up to the route/controller registration (Spring \`@PostMapping\`, Express \`router.post\`, FastAPI \`@router.post\`, etc.) to identify the endpoint(s) that invoke the changed logic.
175
- 4. **Augment the endpoint list** from Step 2 with these execution-path endpoints.
176
- 5. If an execution or processing endpoint is found (path ending in \`/execute\`, \`/run\`, \`/trigger\`, \`/process\`, \`/invoke\`, or similar), it **MUST** be included in the test candidates. Do not produce coverage consisting solely of CRUD endpoints when an execution-path endpoint was found — CRUD tests may still be included but must not be the only coverage.
177
- `
178
- : "";
179
- const criticalPatternStep = `### Step 2.5: Identify critical patterns for test categorization
180
- Look for these patterns in model/schema/handler files to inform test recommendations:
181
- - **Unique constraints**: \`@unique\`, \`unique: true\`, unique indexes, \`.refine()\` uniqueness checks, \`UNIQUE\` in SQL migrations
182
- - **Cascade deletes**: \`ON DELETE CASCADE\`, \`.onDelete("cascade")\`, manual cascade logic in delete handlers
183
- - **Permission checks**: auth middleware, ownership guards (\`req.user.id === resource.ownerId\`), role-based access control, \`isOwner\` assertions
184
- - **Breaking changes in diff**: route renames, deleted route definitions (endpoints removed from modified files), auth header changes, removed required fields, changed status codes
185
- Tag each finding with its category (security_boundary, business_rule, data_integrity, breaking_change) for the recommendation step.`;
186
- const step3Content = useHealthFlow
187
- ? `### Step 3: Identify tests at risk of drift
188
- Assess which existing tests may be broken by the changes in this diff.
189
-
190
- ### Step 4: Call analyze test health
191
- Call \`skyramp_analyze_test_health\` with \`stateFile: "${p.stateFile ?? p.sessionId}"\``
192
- : `### Step 3: Draft integration scenarios
193
- Draft multi-step scenarios simulating realistic user workflows:
171
+ 4. **Augment the endpoint list** from Step ${ANALYSIS_STEP_EXTRACT} with these execution-path endpoints.
172
+ 5. If an execution or processing endpoint is found (path ending in \`/execute\`, \`/run\`, \`/trigger\`, \`/process\`, \`/invoke\`, or similar), it **MUST** be included in the test candidates. Do not produce coverage consisting solely of CRUD endpoints when an execution-path endpoint was found — CRUD tests may still be included but must not be the only coverage.`;
173
+ }
174
+ function _draftTitle(p) {
175
+ return p.nextTool === "skyramp_analyze_test_health"
176
+ ? "Identify tests at risk of drift"
177
+ : "Draft integration scenarios";
178
+ }
179
+ function _draftBody(p) {
180
+ if (p.nextTool === "skyramp_analyze_test_health") {
181
+ return "Assess which existing tests may be broken by the changes in this diff.";
182
+ }
183
+ return `Draft multi-step scenarios simulating realistic user workflows:
194
184
  - **Cross-resource data flow**: Foreign key relationships, parent→child creation, verification
195
185
  - **Search/filter verification**: Create data, search for it using \`queryParams\`, verify results
196
186
  - **Negative/error paths**: Invalid references → appropriate errors
@@ -208,23 +198,122 @@ response data verification, actual field names for chaining.
208
198
  - \`"discount formula: total_amount = subtotal * (1 - discount_value / 100) — wrong if addition is used instead of subtraction"\`
209
199
  - \`"items not recalculated after PATCH — total_amount stays at old value if collection update is ignored"\`
210
200
  - \`"missing 404 guard on resource ID — returns 500 instead of 404 for unknown IDs"\`
211
- This field is used at test generation time to compute exact assertion values. Leave it empty only if no specific formula or constraint applies.
212
-
213
- ### Step 4: Call recommend tests
214
- Call \`skyramp_recommend_tests\` with:
201
+ This field is used at test generation time to compute exact assertion values. Leave it empty only if no specific formula or constraint applies.`;
202
+ }
203
+ function _callToolTitle(p) {
204
+ return p.nextTool === "skyramp_analyze_test_health"
205
+ ? "Call analyze test health"
206
+ : "Call recommend tests";
207
+ }
208
+ function _callToolBody(p) {
209
+ if (p.nextTool === "skyramp_analyze_test_health") {
210
+ return `Call \`skyramp_analyze_test_health\` with \`stateFile: "${p.stateFile ?? p.sessionId}"\``;
211
+ }
212
+ return `Call \`skyramp_recommend_tests\` with:
215
213
  - \`stateFile: "${p.stateFile}"\`
216
- - \`enrichedScenarios\`: (optional) JSON array of your Step 3 scenarios — see the tool's inputSchema for the exact shape. Your enriched scenarios override server-side ones with the same \`scenarioName\` and are prioritized in ranking. Omit if you drafted nothing in Step 3.`;
217
- return `## Your Task — Enrich & Recommend (PR-scoped)
214
+ - \`enrichedScenarios\`: (optional) JSON array of your Step ${ANALYSIS_STEP_DRAFT} scenarios — see the tool's inputSchema for the exact shape. Your enriched scenarios override server-side ones with the same \`scenarioName\` and are prioritized in ranking. Omit if you drafted nothing in Step ${ANALYSIS_STEP_DRAFT}.`;
215
+ }
216
+ // ── PromptPlan declaration ─────────────────────────────────────────────────────
217
+ // Defines the diff-scope prompt structure: phases, steps, sub-steps, and conditions.
218
+ // Used to:
219
+ // 1. Derive the exported step label constants below.
220
+ // 2. Render the diff-scope prompt via _plan.render(p).
221
+ // 3. Enable developer review via _plan.preview(p).
222
+ const _plan = new PromptPlan()
223
+ .addPhase("analysis", "Your Task — Enrich & Recommend (PR-scoped)", {
224
+ headerLevel: "##",
225
+ stepFormat: "hash",
226
+ })
227
+ .step("READ_FILES", "Read the changed files and diff", _readFilesBody)
228
+ .subStep("RESOLVE_PATHS", _resolvePathsTitle, _resolvePathsBody)
229
+ .step("EXTRACT", _extractTitle, _extractBody)
230
+ .subStep("TRACE_CALLERS", "Trace callers of changed non-route files", _traceCallersBody, {
231
+ when: _traceCallersCondition,
232
+ whenDesc: "non-frontend unmatched backend files present in diff scope",
233
+ })
234
+ .step("DRAFT", _draftTitle, _draftBody)
235
+ .step("CALL_TOOL", _callToolTitle, _callToolBody)
236
+ .done();
237
+ // ── Exported step label constants ─────────────────────────────────────────────
238
+ /** "1" — Read source files / changed files */
239
+ export const ANALYSIS_STEP_READ_FILES = _plan.labels.READ_FILES; // "1"
240
+ /** "1.1" — Verify/build/validate endpoint path resolution table */
241
+ export const ANALYSIS_STEP_RESOLVE_PATHS = _plan.labels.RESOLVE_PATHS; // "1.1"
242
+ /** "2" — Extract or map endpoints */
243
+ export const ANALYSIS_STEP_EXTRACT = _plan.labels.EXTRACT; // "2"
244
+ /** "3" — Draft scenarios / present catalog / identify drift */
245
+ export const ANALYSIS_STEP_DRAFT = _plan.labels.DRAFT; // "3"
246
+ /** "4" — Call the downstream tool (recommend_tests / analyze_test_health) */
247
+ export const ANALYSIS_STEP_CALL_TOOL = _plan.labels.CALL_TOOL; // "4"
248
+ /**
249
+ * Returns the step label for the trace-callers sub-step given runtime params.
250
+ * Returns "2.1" when the step is active (non-frontend unmatched backend files
251
+ * present in diff scope), or null when the step is absent.
252
+ *
253
+ * Use in tests instead of hardcoding "2.1":
254
+ * const label = getStepTraceCallers(params);
255
+ * if (label) expect(output).toContain(`### Step ${label}: Trace callers`);
256
+ * else expect(output).not.toContain("Trace callers");
257
+ */
258
+ export function getStepTraceCallers(p) {
259
+ return _plan.labelFor("TRACE_CALLERS", p);
260
+ }
261
+ /**
262
+ * Returns the full "### Step N.N: Title\nbody" block for the path resolution
263
+ * sub-step. Used by the full-repo scope path which builds its prompt manually
264
+ * rather than calling _plan.render().
265
+ */
266
+ function buildPathResolutionTableSection(p) {
267
+ const label = ANALYSIS_STEP_RESOLVE_PATHS;
268
+ const title = _resolvePathsTitle(p);
269
+ const body = _resolvePathsBody(p);
270
+ return `### Step ${label}: ${title}\n${body}\n`;
271
+ }
272
+ function buildEnrichmentInstructions(p) {
273
+ const isDiffScope = p.analysisScope === AnalysisScope.CurrentBranchDiff;
274
+ const useHealthFlow = p.nextTool === "skyramp_analyze_test_health";
275
+ if (!isDiffScope) {
276
+ const nextStep = useHealthFlow
277
+ ? `### Step ${ANALYSIS_STEP_DRAFT}: Identify tests at risk of drift
278
+ Call \`skyramp_analyze_test_health\` with \`stateFile: "${p.stateFile ?? p.sessionId}"\``
279
+ : `### Step ${ANALYSIS_STEP_DRAFT}: Present the catalog
280
+ The ranked test recommendation catalog is pre-built and shown below (after the separator line).
281
+
282
+ **Your only job is to present it.**
218
283
 
219
- ### Step 1: Read the changed files and diff
220
- ${changedFiles}${diffFileRef}
221
- ${buildPathResolutionTableStep(p)}${step2}
222
- ${callerTracingStep}
223
- ${criticalPatternStep}
284
+ 1. Fill in every \`<…from source>\` placeholder using the field names, computed formulas, and auth details you found in Steps 1–2.
285
+ 2. Output the completed catalog **exactly as formatted — grouped by test type (### E2E / ### UI / ### Integration / ### Contract)**. Do NOT restructure, reorder, rename sections, or generate a new format.
286
+ 3. Do NOT call any Skyramp generation tools. The catalog shows ready-to-use tool calls that can be executed on demand.
224
287
 
225
- ${step3Content}`;
288
+ **If** Steps 1–2 revealed additional scenarios the catalog does not cover (e.g. a computed formula or FK relationship that was missed), you may optionally call \`skyramp_recommend_tests\` with \`stateFile: "${p.stateFile ?? p.sessionId}"\` and \`enrichedScenarios\` to regenerate a more complete catalog — but only after presenting the current one.`;
289
+ const hasJavaFiles = p.candidateRouteFiles?.some((f) => /\.(java|kt)$/.test(f)) ?? false;
290
+ const routeFilesSection = p.candidateRouteFiles && p.candidateRouteFiles.length > 0
291
+ ? `\nRoute/controller files found by static scan (read these to discover endpoints — the regex-based catalog below may be incomplete for your framework):\n${p.candidateRouteFiles.map((f) => `- ${f}`).join("\n")}\n`
292
+ : "";
293
+ const resolvePathsNote = p.routerMountContext.length
294
+ ? `**Resolve nested paths** using your Step ${ANALYSIS_STEP_RESOLVE_PATHS} table — a router in the table with prefix \`/api/v1/products/{product_id}/reviews\` means every endpoint in that file lives under that full path.`
295
+ : `**Resolve full paths** using the prefixes you identified in Step ${ANALYSIS_STEP_READ_FILES} (e.g. Java Spring class-level \`@RequestMapping\` prefix + method-level path).`;
296
+ return `## Your Task — Fill in and Present the Catalog (full repo)
297
+
298
+ ### Step ${ANALYSIS_STEP_READ_FILES}: Read key files
299
+ ${routeFilesSection}Read the route/controller files above **and** model/schema files (Pydantic models, Zod schemas, DTOs) to find: required request body fields, computed response fields and formulas, auth middleware type, storage backend, and how sub-routers are mounted (cross-check against Router Mounting section above).
300
+ ${hasJavaFiles ? JAVA_SPRING_NOTE : ""}
301
+ If the endpoint catalog below is missing endpoints visible in these files (e.g. from a framework the static scanner doesn't recognise), extract them now${useHealthFlow ? "." : " and include them in Step ${ANALYSIS_STEP_DRAFT}'s `enrichedScenarios`."}
302
+
303
+ ${buildPathResolutionTableSection(p)}### Step ${ANALYSIS_STEP_EXTRACT}: Map cross-resource relationships and resolve endpoint paths
304
+ (Distinct from Step ${ANALYSIS_STEP_READ_FILES} — Step ${ANALYSIS_STEP_READ_FILES} reads individual schemas; Step ${ANALYSIS_STEP_EXTRACT} maps how endpoints relate to each other.)
305
+ For each endpoint: which POST creates resources consumed by other endpoints?
306
+ ${resolvePathsNote}
307
+ For GET list endpoints: identify query params (\`limit\`, \`offset\`, \`order\`, \`orderBy\`) from framework annotations (FastAPI \`Query()\`, Express \`req.query\`, etc.).
308
+
309
+ ${nextStep}`;
310
+ }
311
+ // Diff scope — delegate entirely to the plan.
312
+ return _plan.render(p);
226
313
  }
227
314
  export function buildAnalysisOutputText(p) {
315
+ // Centralize diff reading here — resolve content from file once so helpers don't each read it.
316
+ p = { ...p, diffContent: readDiffFile(p.diffFilePath) };
228
317
  const isDiffScope = p.analysisScope === AnalysisScope.CurrentBranchDiff;
229
318
  // Router mounting context is unique to this prompt; shown whenever mount context
230
319
  // is available, regardless of whether a spec is configured.
@@ -232,18 +321,51 @@ export function buildAnalysisOutputText(p) {
232
321
  ? `
233
322
  ## Routing entry-point files
234
323
  ${p.routerFileContents?.length
235
- ? p.routerFileContents.map(({ file, content }) => `### \`${file}\`\n\`\`\`\n${content}\n\`\`\``)
236
- .join("\n\n") + (p.routerMountContext.length > (p.routerFileContents?.length ?? 0)
237
- ? `\n\nAdditional files (too large to inline — read manually if needed):\n` +
238
- p.routerMountContext
239
- .filter(f => !(p.routerFileContents ?? []).some(r => r.file === f))
240
- .map(f => `- \`${f}\``)
241
- .join("\n")
242
- : "")
243
- : `Read these in Step 1.5 to trace the full router/module hierarchy:\n` +
244
- p.routerMountContext.map(f => `- \`${f}\``).join("\n")}`
324
+ ? p.routerFileContents
325
+ .map(({ file, content }) => `### \`${file}\`\n\`\`\`\n${content}\n\`\`\``)
326
+ .join("\n\n") +
327
+ (p.routerMountContext.length > (p.routerFileContents?.length ?? 0)
328
+ ? `\n\nAdditional files (too large to inline read manually if needed):\n` +
329
+ p.routerMountContext
330
+ .filter((f) => !(p.routerFileContents ?? []).some((r) => r.file === f))
331
+ .map((f) => `- \`${f}\``)
332
+ .join("\n")
333
+ : "")
334
+ : `Read these in Step ${ANALYSIS_STEP_RESOLVE_PATHS} to trace the full router/module hierarchy:\n` +
335
+ p.routerMountContext.map((f) => `- \`${f}\``).join("\n")}`
245
336
  : "";
246
337
  const enrichment = buildEnrichmentInstructions(p);
338
+ // LLM fallback: when heuristic scanning may have missed backend route files.
339
+ const scannerFallbackHasCatalog = p.scannedEndpoints.length > 0;
340
+ const scannerFallbackReason = scannerFallbackHasCatalog
341
+ ? "The heuristic endpoint scanner may have missed route/controller files in this diff, even though endpoint data is present from another source"
342
+ : "The heuristic endpoint scanner found **0 endpoints**";
343
+ const scannerFallbackInstruction = scannerFallbackHasCatalog
344
+ ? "Treat this as a supplemental gap check: merge only HTTP endpoints that are visibly missing from the existing catalog/spec data, and keep the existing catalog as the primary endpoint source."
345
+ : "Build a table of discovered endpoints (method, full path, source file) and use it as the authoritative endpoint list for all subsequent steps.";
346
+ const llmFallbackSection = (isDiffScope || p.scannedEndpoints.length === 0) &&
347
+ p.candidateRouteFiles &&
348
+ p.candidateRouteFiles.length > 0
349
+ ? `
350
+ ## Scanner Fallback — Manual Endpoint Discovery Required
351
+
352
+ ${scannerFallbackReason}, and this repository contains ${p.candidateRouteFiles.length} file(s) that appear to define HTTP routes. The scanner likely failed due to multi-line definitions, framework-specific patterns, indirection the regex does not cover, or endpoint data being supplied by an OpenAPI spec instead of source scanning.
353
+
354
+ **Read the following controller/router files and identify all HTTP route registrations (method + path).** Include these discovered endpoints when executing the steps above.
355
+
356
+ ${p.candidateRouteFiles.slice(0, 15).map((f) => `- \`${f}\``).join("\n")}
357
+ ${p.candidateRouteFiles.length > 15 ? `\n_(${p.candidateRouteFiles.length - 15} more files not shown)_` : ""}
358
+
359
+ For each file, look for:
360
+ - Express/Fastify/Koa/Hapi: \`router.<method>('/path', ...)\` or \`app.<method>('/path', ...)\`
361
+ - FastAPI/Flask: \`@router.<method>('/path')\` or \`@app.route('/path', ...)\`
362
+ - Spring/NestJS: \`@GetMapping\`, \`@PostMapping\`, \`@Controller\`, etc.
363
+ - Go (Gin/Echo/Chi): \`r.GET("/path", ...)\`, \`r.Group("/prefix")\`
364
+ - GraphQL: schema/resolver artifacts are unsupported for REST test generation; do not invent REST endpoints from them
365
+ - Any other framework-specific route registration pattern
366
+
367
+ ${scannerFallbackInstruction}`
368
+ : "";
247
369
  return `# Repository Analysis
248
370
 
249
371
  **Session ID**: \`${p.sessionId}\`
@@ -251,6 +373,7 @@ ${p.routerFileContents?.length
251
373
  **Analysis Scope**: \`${p.analysisScope}\`
252
374
  ${isDiffScope ? `**Diff endpoints**: ${(p.parsedDiff?.newEndpoints.length ?? 0) + (p.parsedDiff?.modifiedEndpoints.length ?? 0) + (p.parsedDiff?.removedEndpoints?.length ?? 0)}` : `**Pre-scanned endpoints**: ${p.scannedEndpoints.length}`}
253
375
  ${routerSection}
376
+ ${llmFallbackSection}
254
377
  ${enrichment}
255
378
 
256
379
  **CRITICAL**: No .json/.md file creation. Prioritize cross-resource workflows.`;