@skyramp/mcp 0.3.8 → 0.4.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/commands/commandLibrary.d.ts +1 -1
- package/build/commands/commandLibrary.js +3 -3
- package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
- package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
- package/build/commands/testThisEndpointCommand.js +35 -19
- package/build/index.js +9 -3
- package/build/playwright/blueprintDigest.d.ts +15 -0
- package/build/playwright/blueprintDigest.js +152 -0
- package/build/playwright/blueprintDigestStore.d.ts +31 -0
- package/build/playwright/blueprintDigestStore.js +117 -0
- package/build/playwright/registerPlaywrightTools.js +60 -12
- package/build/playwright/traceRecordingPrompt.js +8 -7
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
- package/build/prompts/promptAssets.d.ts +20 -0
- package/build/prompts/promptAssets.js +55 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
- package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
- package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
- package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
- package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
- package/build/prompts/test-recommendation/recommendationSections.js +67 -309
- package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
- package/build/prompts/test-recommendation/recommendationShared.js +49 -155
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
- package/build/prompts/testbot/planDeclarations.d.ts +6 -0
- package/build/prompts/testbot/planDeclarations.js +9 -0
- package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
- package/build/prompts/testbot/testbot-prompts.js +256 -381
- package/build/recommendation/answers.d.ts +35 -0
- package/build/recommendation/answers.js +96 -0
- package/build/recommendation/registerPlan.d.ts +49 -0
- package/build/recommendation/registerPlan.js +117 -0
- package/build/recommendation/runVerifiers.d.ts +10 -0
- package/build/recommendation/runVerifiers.js +49 -0
- package/build/recommendation/subjectStep.d.ts +42 -0
- package/build/recommendation/subjectStep.js +86 -0
- package/build/recommendation/types.d.ts +163 -0
- package/build/recommendation/types.js +20 -0
- package/build/recommendation/verifierContracts.d.ts +382 -0
- package/build/recommendation/verifierContracts.js +263 -0
- package/build/recommendation/verifiers/changedFile.d.ts +2 -0
- package/build/recommendation/verifiers/changedFile.js +82 -0
- package/build/recommendation/verifiers/citedPath.d.ts +12 -0
- package/build/recommendation/verifiers/citedPath.js +35 -0
- package/build/recommendation/verifiers/coverage.d.ts +7 -0
- package/build/recommendation/verifiers/coverage.js +617 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
- package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
- package/build/recommendation/verifiers/endpointGrounded.js +128 -0
- package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
- package/build/recommendation/verifiers/existingCoverage.js +51 -0
- package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
- package/build/recommendation/verifiers/expectedOutcome.js +105 -0
- package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
- package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
- package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
- package/build/recommendation/verifiers/reportedCategory.js +84 -0
- package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
- package/build/recommendation/verifiers/screenRoute.js +118 -0
- package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
- package/build/recommendation/verifiers/statedDifference.js +140 -0
- package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
- package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
- package/build/resources/analysisResources.js +1 -114
- package/build/resources/testbotResource.js +23 -13
- package/build/services/ModularizationService.js +2 -1
- package/build/services/TestDiscoveryService.d.ts +3 -72
- package/build/services/TestDiscoveryService.js +10 -303
- package/build/services/containerEnv.d.ts +1 -1
- package/build/services/containerEnv.js +12 -0
- package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
- package/build/skills/fixTestImportErrorsSkill.js +20 -0
- package/build/toolNames.d.ts +1 -0
- package/build/toolNames.js +1 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
- package/build/tools/code-refactor/modularizationTool.js +2 -1
- package/build/tools/executeSkyrampTestTool.d.ts +80 -0
- package/build/tools/executeSkyrampTestTool.js +246 -19
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
- package/build/tools/generate-tests/generateContractRestTool.js +3 -3
- package/build/tools/generate-tests/planGuard.d.ts +2 -2
- package/build/tools/generate-tests/planGuard.js +78 -18
- package/build/tools/one-click/oneClickTool.d.ts +0 -1
- package/build/tools/one-click/oneClickTool.js +0 -5
- package/build/tools/submitReportTool.d.ts +48 -42
- package/build/tools/submitReportTool.js +576 -193
- package/build/tools/test-management/actionsTool.js +72 -4
- package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
- package/build/tools/test-management/analyzeChangesTool.js +212 -1219
- package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
- package/build/tools/test-management/index.d.ts +1 -0
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
- package/build/tools/test-management/registerTestPlanTool.js +609 -542
- package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
- package/build/tools/test-management/resolveScreenTool.js +289 -0
- package/build/types/BlueprintDigest.d.ts +34 -0
- package/build/types/BlueprintDigest.js +1 -0
- package/build/types/RepositoryAnalysis.d.ts +20 -1559
- package/build/types/RepositoryAnalysis.js +2 -58
- package/build/types/StepMethod.d.ts +40 -0
- package/build/types/StepMethod.js +77 -0
- package/build/types/TestAnalysis.d.ts +12 -0
- package/build/types/TestExecution.d.ts +4 -0
- package/build/types/TestRecommendation.d.ts +24 -24
- package/build/types/TestRecommendation.js +91 -89
- package/build/types/TestbotPromptOptions.d.ts +0 -4
- package/build/types/TestbotReport.d.ts +64 -2
- package/build/utils/AnalysisStateManager.d.ts +79 -113
- package/build/utils/AnalysisStateManager.js +147 -57
- package/build/utils/assertion-verify/api-shared-lints.js +1 -1
- package/build/utils/assertion-verify/metrics.js +85 -36
- package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
- package/build/utils/assertion-verify/ui-lints.js +32 -0
- package/build/utils/branchDiff.d.ts +63 -31
- package/build/utils/branchDiff.js +242 -94
- package/build/utils/containedPath.d.ts +18 -0
- package/build/utils/containedPath.js +73 -0
- package/build/utils/dartRouteExtractor.d.ts +18 -34
- package/build/utils/dartRouteExtractor.js +101 -173
- package/build/utils/featureFlags.d.ts +12 -0
- package/build/utils/featureFlags.js +14 -0
- package/build/utils/frontendSelectors.d.ts +48 -27
- package/build/utils/frontendSelectors.js +241 -80
- package/build/utils/pathMatching.d.ts +2 -4
- package/build/utils/pathMatching.js +2 -4
- package/build/utils/planMatchKeys.d.ts +38 -47
- package/build/utils/planMatchKeys.js +143 -81
- package/build/utils/rebaselineSnapshots.d.ts +24 -0
- package/build/utils/rebaselineSnapshots.js +65 -0
- package/build/utils/removedUiElements.d.ts +22 -0
- package/build/utils/removedUiElements.js +106 -0
- package/build/utils/reportVerification.d.ts +2 -6
- package/build/utils/reportVerification.js +61 -2
- package/build/utils/screenRoutes.d.ts +66 -0
- package/build/utils/screenRoutes.js +727 -0
- package/build/utils/sourceRouteExtractor.js +320 -112
- package/build/utils/testFileClassification.d.ts +11 -2
- package/build/utils/testFileClassification.js +44 -2
- package/build/utils/testFixtures.d.ts +5 -0
- package/build/utils/testFixtures.js +13 -0
- package/build/utils/utils.d.ts +0 -1
- package/build/utils/utils.js +0 -11
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +12 -12
- package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +4 -3
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/plugin.json +6 -0
- package/plugin/prompts/declaring-a-plan.md +20 -0
- package/plugin/prompts/generate-tests/context-fetching.md +4 -0
- package/plugin/prompts/generate-tests/execution-plan.md +63 -0
- package/plugin/prompts/generate-tests/generation.md +108 -0
- package/plugin/prompts/generate-tests/path-parameters.md +1 -0
- package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
- package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
- package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
- package/plugin/prompts/plan-tests.md +42 -0
- package/plugin/prompts/testbot-task1.md +82 -0
- package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
- package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
- package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
- package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
- package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
- package/build/recommendation/budgeters/shared.d.ts +0 -32
- package/build/recommendation/budgeters/shared.js +0 -246
- package/build/recommendation/discriminators.d.ts +0 -37
- package/build/recommendation/discriminators.js +0 -379
- package/build/recommendation/diversity.d.ts +0 -47
- package/build/recommendation/diversity.js +0 -101
- package/build/recommendation/planRanker.d.ts +0 -65
- package/build/recommendation/planRanker.js +0 -83
- package/build/recommendation/testFixtures.d.ts +0 -25
- package/build/recommendation/testFixtures.js +0 -45
- package/build/types/FrontendIntegration.d.ts +0 -28
- package/build/types/FrontendIntegration.js +0 -22
- package/build/types/Recommendation.d.ts +0 -146
- package/build/types/Recommendation.js +0 -74
- package/build/utils/changedRoutes.d.ts +0 -29
- package/build/utils/changedRoutes.js +0 -87
- package/build/utils/frontendIntegration.d.ts +0 -9
- package/build/utils/frontendIntegration.js +0 -243
- package/build/utils/importerHop.d.ts +0 -135
- package/build/utils/importerHop.js +0 -489
- package/build/utils/pathAffinityClassification.d.ts +0 -49
- package/build/utils/pathAffinityClassification.js +0 -180
- package/build/utils/pythonMountPrefixes.d.ts +0 -25
- package/build/utils/pythonMountPrefixes.js +0 -347
- package/build/utils/repoScanner.d.ts +0 -34
- package/build/utils/repoScanner.js +0 -300
- package/build/utils/routeParsers.d.ts +0 -95
- package/build/utils/routeParsers.js +0 -951
- package/build/utils/scenarioDrafting.d.ts +0 -92
- package/build/utils/scenarioDrafting.js +0 -951
- package/build/utils/subjectEndpoints.d.ts +0 -19
- package/build/utils/subjectEndpoints.js +0 -98
- package/build/utils/uiPageEnumerator.d.ts +0 -172
- package/build/utils/uiPageEnumerator.js +0 -474
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Path-affinity classification fallback — SKYR-3857.
|
|
3
|
-
*
|
|
4
|
-
* `classifyEndpointsByChangedFiles` (routeParsers.ts) matches changed files to
|
|
5
|
-
* scanned endpoints by exact `sourceFile` equality. That match structurally fails
|
|
6
|
-
* for an entire repository whenever the endpoint catalog carries no usable
|
|
7
|
-
* `sourceFile` linkage for its route files — either because the framework isn't
|
|
8
|
-
* (yet) handled by `parseFileEndpoints` (the repo scan contributes nothing for
|
|
9
|
-
* those files), or because the catalog is predominantly spec-derived
|
|
10
|
-
* (`filterEndpointsBySpec` merges in OpenAPI-only entries with `sourceFile: ""`).
|
|
11
|
-
* In both cases every changed route file lands in `unmatchedFiles`, `changed`/
|
|
12
|
-
* `new` counts stay at 0 for every PR touching that repo, and downstream
|
|
13
|
-
* `hasApiChanges` derivation misclassifies genuinely mixed PRs as UI-only (see
|
|
14
|
-
* testbot run 26968884237, koljahe/infisical PR #16 — Fastify backend, 1,972
|
|
15
|
-
* spec-merged catalog entries, all with `sourceFile: ""`).
|
|
16
|
-
*
|
|
17
|
-
* This module recovers those files by matching on PATH instead of `sourceFile`:
|
|
18
|
-
* for each unmatched, route-like changed file, it derives a resource token from
|
|
19
|
-
* the file's own name and looks for catalog endpoints whose path implies the
|
|
20
|
-
* same resource — narrowed by a version/prefix conjunction (see
|
|
21
|
-
* `deriveVersionSegments`) so a file under `routes/v4/` cannot match a `/v1/`
|
|
22
|
-
* or `/v2/` sibling endpoint just because the resource name lines up.
|
|
23
|
-
*
|
|
24
|
-
* Deliberately conservative: only fired for files `classifyEndpointsByChangedFiles`
|
|
25
|
-
* already found nothing for (never overrides a real sourceFile match), and a file
|
|
26
|
-
* with no surviving affinity match is left exactly as unmatched as it is today.
|
|
27
|
-
* Every entry point here is fail-safe — a per-file error is logged and that file
|
|
28
|
-
* is skipped, never thrown to the caller.
|
|
29
|
-
*
|
|
30
|
-
* KNOWN LIMITATION (conscious first-cut scope): matching is resource-token +
|
|
31
|
-
* version/prefix only. It does not corroborate against literal relative-URL
|
|
32
|
-
* string evidence in the changed file's content (route files quote their own
|
|
33
|
-
* URL suffixes, e.g. `"/:secretName"`) — the token+version conjunction already
|
|
34
|
-
* narrows precisely enough for the motivating case, and content-based
|
|
35
|
-
* corroboration adds real complexity (multi-language quoting, template-literal
|
|
36
|
-
* interpolation) for a marginal precision gain. Left as a follow-up if
|
|
37
|
-
* real-world false positives surface.
|
|
38
|
-
*
|
|
39
|
-
* SKYR-3857 consolidation: both halves of the token matching are shared with
|
|
40
|
-
* `importerHop.ts` rather than reimplemented here — `deriveResourceTokensForFile`
|
|
41
|
-
* for the file side (which also gives this fallback symbol-based affinity: a
|
|
42
|
-
* declared class/interface/schema name on an added diff line can anchor a match
|
|
43
|
-
* for route files whose filename alone is too generic, e.g.
|
|
44
|
-
* `routes/v4/index-router.ts`) and `endpointResourceTokens` for the endpoint
|
|
45
|
-
* side. The version vocabulary comes from `isVersionLikeSegment` in
|
|
46
|
-
* `routeParsers.ts`, next to the `SKIP_PATH_SEGMENTS` set it extends.
|
|
47
|
-
*/
|
|
48
|
-
import { logger } from "./logger.js";
|
|
49
|
-
import { isRouteLikeFile, isVersionLikeSegment } from "./routeParsers.js";
|
|
50
|
-
import { isFrontendFile, isTestFile } from "../prompts/test-recommendation/scopeAssessment.js";
|
|
51
|
-
import { deriveResourceTokensForFile, endpointResourceTokens, sliceAddedLinesByFile } from "./importerHop.js";
|
|
52
|
-
/** Default cap on endpoints classified per unmatched file — keeps a broad/wrong
|
|
53
|
-
* resource-token match from flooding the change set. */
|
|
54
|
-
const DEFAULT_MAX_ENDPOINTS_PER_FILE = 15;
|
|
55
|
-
/** Version/prefix directory segments ("v4", "api", ...) present in a changed
|
|
56
|
-
* file's own path — required, as a conjunction, in any matching endpoint path.
|
|
57
|
-
* Prevents a `routes/v4/secret-router.ts` change from matching a `/api/v1/*`
|
|
58
|
-
* sibling endpoint on resource-token alone. */
|
|
59
|
-
export function deriveVersionSegments(file) {
|
|
60
|
-
const normalized = file.replace(/\\/g, "/");
|
|
61
|
-
const segments = normalized.split("/").filter(Boolean);
|
|
62
|
-
// Exclude the basename — the resource token is derived from it separately,
|
|
63
|
-
// and a hyphenated basename (e.g. "secret-v4-router.ts") isn't a directory.
|
|
64
|
-
const dirSegments = segments.slice(0, -1);
|
|
65
|
-
return new Set(dirSegments.filter(isVersionLikeSegment).map((s) => s.toLowerCase()));
|
|
66
|
-
}
|
|
67
|
-
/** Segments of an endpoint path, lowercased, for the version-conjunction check. */
|
|
68
|
-
function endpointPathSegments(endpointPath) {
|
|
69
|
-
return new Set(endpointPath.split("/").filter(Boolean).map((s) => s.toLowerCase()));
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Maps changed files that `classifyEndpointsByChangedFiles` could not match by
|
|
73
|
-
* `sourceFile` (its `unmatchedFiles`) to catalog endpoints that plausibly belong
|
|
74
|
-
* to the same REST resource, purely from the file's own path and the endpoint's
|
|
75
|
-
* own path — no `sourceFile` needed on either side. Intended to run against the
|
|
76
|
-
* *final* endpoint catalog (after any OpenAPI spec merge), since a catalog with
|
|
77
|
-
* no source-scan coverage for a file is, by definition, only useful for this
|
|
78
|
-
* fallback once spec-derived entries are present to match against.
|
|
79
|
-
*
|
|
80
|
-
* `alreadyClassifiedEndpoints` (typically `changedEndpoints` + `newEndpoints`
|
|
81
|
-
* from the same classification pass) is used only for dedup — never duplicates
|
|
82
|
-
* an endpoint already classified directly.
|
|
83
|
-
*
|
|
84
|
-
* `diffContent`, when supplied, is the full unified diff — sliced per-file
|
|
85
|
-
* (via `sliceAddedLinesByFile`, shared with `mapUnmatchedFilesToImporterEndpoints`)
|
|
86
|
-
* and passed to `deriveResourceTokensForFile` so a route file whose filename
|
|
87
|
-
* alone is too generic to anchor a match (e.g. `routes/v4/index-router.ts`)
|
|
88
|
-
* can still match via a symbol declared on its added diff lines. Without it,
|
|
89
|
-
* matching falls back to the filename-only token, identical to pre-symbol-
|
|
90
|
-
* affinity behavior.
|
|
91
|
-
*
|
|
92
|
-
* `removedEndpoints` (the classifier's removal result, recovered from the base
|
|
93
|
-
* tree) is excluded from the candidate set BY PATH+METHOD, not via the
|
|
94
|
-
* sourceFile-keyed dedup: removal is classified at Step 2.5 against the
|
|
95
|
-
* pre-spec-merge catalog, but this fallback matches the post-spec-merge
|
|
96
|
-
* catalog — where a stale checked-in OpenAPI spec can still list a deleted
|
|
97
|
-
* endpoint as a `sourceFile: ""` entry. Without this guard, a sibling
|
|
98
|
-
* unmatched file sharing the resource token would resurrect the removed
|
|
99
|
-
* endpoint as "modified", contradicting the removal classification. A `MULTI`
|
|
100
|
-
* removed method covers every method on that path; a candidate survives if
|
|
101
|
-
* any of its methods is not removed.
|
|
102
|
-
*
|
|
103
|
-
* Matched endpoints are always attributed as MODIFIED (never new/removed) —
|
|
104
|
-
* path affinity says an endpoint is *related* to the change, not that the
|
|
105
|
-
* relationship is an addition or removal.
|
|
106
|
-
*/
|
|
107
|
-
export function classifyUnmatchedFilesByPathAffinity(unmatchedFiles, scannedEndpoints, alreadyClassifiedEndpoints = [], maxEndpointsPerFile = DEFAULT_MAX_ENDPOINTS_PER_FILE, diffContent, removedEndpoints = []) {
|
|
108
|
-
const endpoints = [];
|
|
109
|
-
const mappedFiles = [];
|
|
110
|
-
const eligibleFiles = unmatchedFiles.filter((file) => !isFrontendFile(file) && !isTestFile(file) && isRouteLikeFile(file));
|
|
111
|
-
if (eligibleFiles.length === 0)
|
|
112
|
-
return { endpoints, mappedFiles };
|
|
113
|
-
const alreadyKeys = new Set(alreadyClassifiedEndpoints.map((ep) => `${ep.sourceFile}::${ep.path}`));
|
|
114
|
-
const addedKeys = new Set();
|
|
115
|
-
const addedLinesByFile = diffContent ? sliceAddedLinesByFile(diffContent) : undefined;
|
|
116
|
-
const removedMethodKeys = new Set();
|
|
117
|
-
const removedMultiPaths = new Set();
|
|
118
|
-
for (const ep of removedEndpoints) {
|
|
119
|
-
for (const method of ep.methods) {
|
|
120
|
-
if (method === "MULTI")
|
|
121
|
-
removedMultiPaths.add(ep.path);
|
|
122
|
-
else
|
|
123
|
-
removedMethodKeys.add(`${method}::${ep.path}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
const isRemoved = (ep) => removedMultiPaths.has(ep.path) ||
|
|
127
|
-
(ep.methods.length > 0 && ep.methods.every((m) => removedMethodKeys.has(`${m}::${ep.path}`)));
|
|
128
|
-
// Endpoint-side tokens/segments depend only on the endpoint's own path —
|
|
129
|
-
// derive them once up front rather than per (file, endpoint) pair (the
|
|
130
|
-
// motivating spec-merged catalogs run to thousands of entries).
|
|
131
|
-
const endpointIndex = scannedEndpoints
|
|
132
|
-
.filter((ep) => !isRemoved(ep))
|
|
133
|
-
.map((ep) => ({
|
|
134
|
-
ep,
|
|
135
|
-
tokens: endpointResourceTokens(ep.path),
|
|
136
|
-
segments: endpointPathSegments(ep.path),
|
|
137
|
-
}));
|
|
138
|
-
for (const file of eligibleFiles) {
|
|
139
|
-
try {
|
|
140
|
-
const fileAddedLines = addedLinesByFile?.get(file);
|
|
141
|
-
const diffHunk = fileAddedLines && fileAddedLines.length > 0 ? fileAddedLines.join("\n") : undefined;
|
|
142
|
-
const resourceTokens = deriveResourceTokensForFile(file, diffHunk);
|
|
143
|
-
if (resourceTokens.length === 0)
|
|
144
|
-
continue;
|
|
145
|
-
const requiredVersionSegments = deriveVersionSegments(file);
|
|
146
|
-
const candidates = endpointIndex.filter(({ tokens, segments }) => {
|
|
147
|
-
if (!resourceTokens.some((token) => tokens.has(token)))
|
|
148
|
-
return false;
|
|
149
|
-
if (requiredVersionSegments.size === 0)
|
|
150
|
-
return true;
|
|
151
|
-
return [...requiredVersionSegments].every((seg) => segments.has(seg));
|
|
152
|
-
});
|
|
153
|
-
if (candidates.length === 0)
|
|
154
|
-
continue;
|
|
155
|
-
const capped = candidates.slice(0, maxEndpointsPerFile);
|
|
156
|
-
for (const { ep } of capped) {
|
|
157
|
-
const key = `${ep.sourceFile}::${ep.path}`;
|
|
158
|
-
if (alreadyKeys.has(key) || addedKeys.has(key))
|
|
159
|
-
continue;
|
|
160
|
-
addedKeys.add(key);
|
|
161
|
-
endpoints.push(ep);
|
|
162
|
-
}
|
|
163
|
-
mappedFiles.push(file);
|
|
164
|
-
logger.info("Path-affinity fallback classified endpoints for sourceFile-less file", {
|
|
165
|
-
file,
|
|
166
|
-
resourceTokens,
|
|
167
|
-
versionSegments: [...requiredVersionSegments],
|
|
168
|
-
matched: capped.length,
|
|
169
|
-
truncated: candidates.length > capped.length,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
catch (err) {
|
|
173
|
-
logger.warning("Path-affinity classification failed for file — skipping", {
|
|
174
|
-
file,
|
|
175
|
-
error: err instanceof Error ? err.message : String(err),
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return { endpoints, mappedFiles };
|
|
180
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/** Repo-relative POSIX path of a Python source file. */
|
|
2
|
-
export type RelFile = string;
|
|
3
|
-
/** A router variable name as written in source (e.g. "router"). */
|
|
4
|
-
export type RouterVar = string;
|
|
5
|
-
/** The ancestor mount prefix a router inherits from its include_router chain (e.g. "/api"). */
|
|
6
|
-
export type MountPrefix = string;
|
|
7
|
-
/**
|
|
8
|
-
* `sourceFile → (routerVar → ancestor mount prefix)`, produced by
|
|
9
|
-
* `resolvePythonMountPrefixes`. Only routers that inherit a non-empty ancestor
|
|
10
|
-
* prefix appear; everything else is absent (single-file parsing is unchanged).
|
|
11
|
-
*/
|
|
12
|
-
export type MountPrefixesByFile = Map<RelFile, Map<RouterVar, MountPrefix>>;
|
|
13
|
-
/**
|
|
14
|
-
* Join a prefix and a child path segment with exactly one slash, tolerating
|
|
15
|
-
* leading/trailing slashes on either side. Empty child → the prefix unchanged;
|
|
16
|
-
* empty prefix → the child made absolute.
|
|
17
|
-
*/
|
|
18
|
-
export declare function joinRoutePrefix(prefix: string, child: string): string;
|
|
19
|
-
/**
|
|
20
|
-
* Build `sourceFile → (routerVar → ancestorMountPrefix)` for every FastAPI
|
|
21
|
-
* router reachable through an `include_router` chain. Only non-empty ancestor
|
|
22
|
-
* prefixes are returned, so files whose routers sit at the root are absent and
|
|
23
|
-
* fall back to unchanged single-file parsing.
|
|
24
|
-
*/
|
|
25
|
-
export declare function resolvePythonMountPrefixes(repositoryPath: string): MountPrefixesByFile;
|
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import { walkSourceFiles } from "./fileWalk.js";
|
|
4
|
-
import { logger } from "./logger.js";
|
|
5
|
-
/**
|
|
6
|
-
* Cross-file FastAPI mount-prefix resolution.
|
|
7
|
-
*
|
|
8
|
-
* FastAPI composes a route's full URL from prefixes applied at three levels:
|
|
9
|
-
* 1. the router's own `APIRouter(prefix=…)` (captured per-file by
|
|
10
|
-
* `parseFileEndpoints`), and
|
|
11
|
-
* 2. every `include_router(child, prefix=…)` / `app.include_router(child, …)`
|
|
12
|
-
* mount point the router passes through — which routinely live in *other*
|
|
13
|
-
* files (an `app.py` / `routes/__init__.py` aggregator).
|
|
14
|
-
*
|
|
15
|
-
* The single-file route parser can only see (1), so a leaf route declared as
|
|
16
|
-
* `@router.get("")` on a `router = APIRouter(prefix="/recipes")` that an
|
|
17
|
-
* aggregator mounts under `APIRouter(prefix="/api")` was emitted as `/recipes`
|
|
18
|
-
* instead of `/api/recipes` — generated tests then hit the un-prefixed URL and
|
|
19
|
-
* 405. (Seen on Mealie's endpoint catalog.)
|
|
20
|
-
*
|
|
21
|
-
* This module does a repo-level pre-pass: it parses every Python file for router
|
|
22
|
-
* declarations, `include_router` mount edges, and the intra-repo imports that
|
|
23
|
-
* connect them, then walks the mount graph to compute, for each router, the
|
|
24
|
-
* **ancestor** prefix it inherits from its mount chain (NOT including the
|
|
25
|
-
* router's own prefix, which `parseFileEndpoints` still applies). The result is
|
|
26
|
-
* keyed by `sourceFile → routerVarName → ancestorPrefix`.
|
|
27
|
-
*
|
|
28
|
-
* Best-effort by design (mirrors the rest of the extractor): an import that
|
|
29
|
-
* can't be resolved simply leaves that router as a graph root with an empty
|
|
30
|
-
* ancestor prefix — i.e. it falls back to today's single-file behavior rather
|
|
31
|
-
* than emitting a wrong path.
|
|
32
|
-
*/
|
|
33
|
-
/** Skip these directory names when walking for Python sources. */
|
|
34
|
-
const SKIP_DIRS = new Set([
|
|
35
|
-
"node_modules", ".git", "dist", "build", "out",
|
|
36
|
-
"venv", ".venv", "env", ".env", "__pycache__", ".tox",
|
|
37
|
-
"site-packages", ".mypy_cache", ".pytest_cache", "coverage",
|
|
38
|
-
]);
|
|
39
|
-
/** Cap on Python files parsed — a backstop against pathological repos. */
|
|
40
|
-
const MAX_PY_FILES = 5000;
|
|
41
|
-
/** Guard against cycles / runaway mount chains. */
|
|
42
|
-
const MAX_MOUNT_DEPTH = 32;
|
|
43
|
-
function nodeId(relFile, varName) {
|
|
44
|
-
return `${relFile}\n${varName}`;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Join a prefix and a child path segment with exactly one slash, tolerating
|
|
48
|
-
* leading/trailing slashes on either side. Empty child → the prefix unchanged;
|
|
49
|
-
* empty prefix → the child made absolute.
|
|
50
|
-
*/
|
|
51
|
-
export function joinRoutePrefix(prefix, child) {
|
|
52
|
-
const left = prefix.replace(/\/+$/, "");
|
|
53
|
-
const right = child.replace(/^\/+/, "");
|
|
54
|
-
if (!right)
|
|
55
|
-
return left;
|
|
56
|
-
if (!left)
|
|
57
|
-
return "/" + right;
|
|
58
|
-
return `${left}/${right}`;
|
|
59
|
-
}
|
|
60
|
-
function normalizeRel(p) {
|
|
61
|
-
return p.replace(/\\/g, "/");
|
|
62
|
-
}
|
|
63
|
-
/** Recursively collect `.py` files, returning repo-relative (posix) paths. */
|
|
64
|
-
function collectPyFiles(repositoryPath) {
|
|
65
|
-
const abs = walkSourceFiles(repositoryPath, {
|
|
66
|
-
extensions: [".py"],
|
|
67
|
-
skipDirs: SKIP_DIRS,
|
|
68
|
-
limit: MAX_PY_FILES,
|
|
69
|
-
});
|
|
70
|
-
if (abs.length >= MAX_PY_FILES) {
|
|
71
|
-
logger.warning("resolvePythonMountPrefixes: hit MAX_PY_FILES cap — some Python files were not scanned, so cross-file mount prefixes may be under-resolved on this repo", { cap: MAX_PY_FILES });
|
|
72
|
-
}
|
|
73
|
-
return abs.map((full) => normalizeRel(full.startsWith(repositoryPath) ? full.slice(repositoryPath.length + 1) : full));
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Merge physical lines into logical lines, joining any line whose parentheses
|
|
77
|
-
* are left open (multi-line `APIRouter(`, `include_router(`, and
|
|
78
|
-
* `from x import ( … )`). String contents are not specially handled — a stray
|
|
79
|
-
* paren inside a string literal is a rare edge that only degrades to a missed
|
|
80
|
-
* prefix (fail-safe).
|
|
81
|
-
*/
|
|
82
|
-
function toLogicalLines(content) {
|
|
83
|
-
const logical = [];
|
|
84
|
-
let buf = "";
|
|
85
|
-
let depth = 0;
|
|
86
|
-
for (const raw of content.split("\n")) {
|
|
87
|
-
const line = raw.replace(/#.*$/, ""); // drop trailing comments
|
|
88
|
-
buf = buf ? `${buf} ${line.trim()}` : line;
|
|
89
|
-
for (const ch of line) {
|
|
90
|
-
if (ch === "(" || ch === "[" || ch === "{")
|
|
91
|
-
depth++;
|
|
92
|
-
else if (ch === ")" || ch === "]" || ch === "}")
|
|
93
|
-
depth = Math.max(0, depth - 1);
|
|
94
|
-
}
|
|
95
|
-
if (depth === 0) {
|
|
96
|
-
logical.push(buf);
|
|
97
|
-
buf = "";
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
if (buf)
|
|
101
|
-
logical.push(buf);
|
|
102
|
-
return logical;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Resolve an intra-repo Python module reference to a source file in `fileSet`.
|
|
106
|
-
* `dots` is the leading-dot count (0 = absolute), `modulePath` the dotted
|
|
107
|
-
* remainder (e.g. `mealie.routes` or `recipe`). Returns the repo-relative file
|
|
108
|
-
* path or null when it can't be resolved to a known file.
|
|
109
|
-
*/
|
|
110
|
-
function resolveModuleFile(fromRel, dots, modulePath, fileSet) {
|
|
111
|
-
const segments = modulePath ? modulePath.split(".").filter(Boolean) : [];
|
|
112
|
-
const candidates = [];
|
|
113
|
-
if (dots > 0) {
|
|
114
|
-
// Relative import: walk up `dots` directories from the current file's dir.
|
|
115
|
-
let base = path.posix.dirname(fromRel);
|
|
116
|
-
for (let i = 1; i < dots; i++)
|
|
117
|
-
base = path.posix.dirname(base);
|
|
118
|
-
const joined = [base === "." ? "" : base, ...segments].filter(Boolean).join("/");
|
|
119
|
-
candidates.push(`${joined}.py`, `${joined}/__init__.py`);
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
// Absolute import: resolve from repo root, but also suffix-match so `src/`
|
|
123
|
-
// (or other package-root) layouts resolve too.
|
|
124
|
-
const joined = segments.join("/");
|
|
125
|
-
candidates.push(`${joined}.py`, `${joined}/__init__.py`);
|
|
126
|
-
}
|
|
127
|
-
for (const c of candidates) {
|
|
128
|
-
if (fileSet.has(c))
|
|
129
|
-
return c;
|
|
130
|
-
}
|
|
131
|
-
// Suffix fallback for absolute imports under a package root (e.g. src/).
|
|
132
|
-
if (dots === 0) {
|
|
133
|
-
const joined = segments.join("/");
|
|
134
|
-
const suffixes = [`/${joined}.py`, `/${joined}/__init__.py`];
|
|
135
|
-
for (const f of fileSet) {
|
|
136
|
-
if (suffixes.some((s) => f.endsWith(s)))
|
|
137
|
-
return f;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
const ROUTER_DECL_RE = /^(\w+)\s*=\s*\w*APIRouter\s*\(/;
|
|
143
|
-
const PREFIX_KW_RE = /(?:^|[,(\s])prefix\s*=\s*["']([^"']*)["']/;
|
|
144
|
-
const CONST_RE = /^(\w+)\s*=\s*["']([^"']*)["']\s*$/;
|
|
145
|
-
const IMPORT_FROM_RE = /^from\s+(\.*)([\w.]*)\s+import\s+(.+)$/;
|
|
146
|
-
const IMPORT_RE = /^import\s+([\w.]+)(?:\s+as\s+(\w+))?/;
|
|
147
|
-
const INCLUDE_ROUTER_RE = /(\w+)\.include_router\s*\(\s*([\w.]+)\s*(?:,(.*))?\)/;
|
|
148
|
-
function parseImportedNames(blob) {
|
|
149
|
-
return blob
|
|
150
|
-
.replace(/[()]/g, "")
|
|
151
|
-
.split(",")
|
|
152
|
-
.map((s) => s.trim())
|
|
153
|
-
.filter(Boolean)
|
|
154
|
-
.map((tok) => {
|
|
155
|
-
const m = tok.match(/^(\w+)(?:\s+as\s+(\w+))?$/);
|
|
156
|
-
if (!m)
|
|
157
|
-
return null;
|
|
158
|
-
return { name: m[1], alias: m[2] ?? m[1] };
|
|
159
|
-
})
|
|
160
|
-
.filter((x) => x !== null);
|
|
161
|
-
}
|
|
162
|
-
function collectFileFacts(relFile, content, fileSet) {
|
|
163
|
-
const facts = {
|
|
164
|
-
ownPrefixes: new Map(),
|
|
165
|
-
stringConsts: new Map(),
|
|
166
|
-
moduleAlias: new Map(),
|
|
167
|
-
symbolAlias: new Map(),
|
|
168
|
-
mounts: [],
|
|
169
|
-
};
|
|
170
|
-
for (const line of toLogicalLines(content)) {
|
|
171
|
-
const trimmed = line.trim();
|
|
172
|
-
if (!trimmed)
|
|
173
|
-
continue;
|
|
174
|
-
// Router declaration + its own prefix.
|
|
175
|
-
const decl = trimmed.match(ROUTER_DECL_RE);
|
|
176
|
-
if (decl) {
|
|
177
|
-
const pm = trimmed.match(PREFIX_KW_RE);
|
|
178
|
-
facts.ownPrefixes.set(decl[1], pm ? pm[1] : "");
|
|
179
|
-
// fall through: a declaration line has no include_router / import.
|
|
180
|
-
}
|
|
181
|
-
// Module-level string const (for prefix=CONST resolution).
|
|
182
|
-
const cm = trimmed.match(CONST_RE);
|
|
183
|
-
if (cm)
|
|
184
|
-
facts.stringConsts.set(cm[1], cm[2]);
|
|
185
|
-
// `from <module> import <names>`
|
|
186
|
-
const fromM = trimmed.match(IMPORT_FROM_RE);
|
|
187
|
-
if (fromM) {
|
|
188
|
-
const dots = fromM[1].length;
|
|
189
|
-
const modulePath = fromM[2];
|
|
190
|
-
const names = parseImportedNames(fromM[3]);
|
|
191
|
-
if (modulePath) {
|
|
192
|
-
// Symbols imported FROM a specific module (e.g. `from .x import router`).
|
|
193
|
-
const modFile = resolveModuleFile(relFile, dots, modulePath, fileSet);
|
|
194
|
-
if (modFile) {
|
|
195
|
-
for (const { name, alias } of names) {
|
|
196
|
-
facts.symbolAlias.set(alias, { file: modFile, varName: name });
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
else if (dots > 0) {
|
|
201
|
-
// `from . import a, b` — each name is a submodule.
|
|
202
|
-
for (const { name, alias } of names) {
|
|
203
|
-
const modFile = resolveModuleFile(relFile, dots, name, fileSet);
|
|
204
|
-
if (modFile)
|
|
205
|
-
facts.moduleAlias.set(alias, modFile);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
// `import a.b.c as d`
|
|
211
|
-
const impM = trimmed.match(IMPORT_RE);
|
|
212
|
-
if (impM) {
|
|
213
|
-
const modFile = resolveModuleFile(relFile, 0, impM[1], fileSet);
|
|
214
|
-
if (modFile) {
|
|
215
|
-
const alias = impM[2] ?? impM[1].split(".")[0];
|
|
216
|
-
facts.moduleAlias.set(alias, modFile);
|
|
217
|
-
}
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
// include_router mount edges (a line may contain several).
|
|
221
|
-
let m;
|
|
222
|
-
const re = new RegExp(INCLUDE_ROUTER_RE, "g");
|
|
223
|
-
while ((m = re.exec(trimmed)) !== null) {
|
|
224
|
-
const prefixExpr = extractMountPrefixExpr(m[3] ?? "");
|
|
225
|
-
facts.mounts.push({ parentVar: m[1], arg: m[2], prefixExpr });
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return facts;
|
|
229
|
-
}
|
|
230
|
-
/** Pull the `prefix=` argument (literal or bare identifier) out of the call tail. */
|
|
231
|
-
function extractMountPrefixExpr(argsTail) {
|
|
232
|
-
const lit = argsTail.match(/prefix\s*=\s*["']([^"']*)["']/);
|
|
233
|
-
if (lit)
|
|
234
|
-
return JSON.stringify(lit[1]); // quote to mark it a literal
|
|
235
|
-
const ident = argsTail.match(/prefix\s*=\s*(\w+)/);
|
|
236
|
-
if (ident)
|
|
237
|
-
return ident[1]; // bare identifier — resolve against stringConsts
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
/** Resolve an `include_router` first-argument expression to a router NodeId. */
|
|
241
|
-
function resolveMountArg(arg, relFile, facts) {
|
|
242
|
-
const parts = arg.split(".");
|
|
243
|
-
if (parts.length === 1) {
|
|
244
|
-
// Bare name: an imported symbol, else a router declared in this file.
|
|
245
|
-
const sym = facts.symbolAlias.get(parts[0]);
|
|
246
|
-
if (sym)
|
|
247
|
-
return nodeId(sym.file, sym.varName);
|
|
248
|
-
return nodeId(relFile, parts[0]);
|
|
249
|
-
}
|
|
250
|
-
// `mod.router` — module alias + attribute.
|
|
251
|
-
const modFile = facts.moduleAlias.get(parts[0]);
|
|
252
|
-
if (modFile)
|
|
253
|
-
return nodeId(modFile, parts[parts.length - 1]);
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Build `sourceFile → (routerVar → ancestorMountPrefix)` for every FastAPI
|
|
258
|
-
* router reachable through an `include_router` chain. Only non-empty ancestor
|
|
259
|
-
* prefixes are returned, so files whose routers sit at the root are absent and
|
|
260
|
-
* fall back to unchanged single-file parsing.
|
|
261
|
-
*/
|
|
262
|
-
export function resolvePythonMountPrefixes(repositoryPath) {
|
|
263
|
-
const pyFiles = collectPyFiles(repositoryPath);
|
|
264
|
-
const result = new Map();
|
|
265
|
-
if (pyFiles.length === 0)
|
|
266
|
-
return result;
|
|
267
|
-
const fileSet = new Set(pyFiles);
|
|
268
|
-
const factsByFile = new Map();
|
|
269
|
-
const ownPrefixByNode = new Map();
|
|
270
|
-
const edges = [];
|
|
271
|
-
for (const rel of pyFiles) {
|
|
272
|
-
let content;
|
|
273
|
-
try {
|
|
274
|
-
content = fs.readFileSync(path.join(repositoryPath, rel), "utf-8");
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
// Cheap gate: only parse files that actually wire routers together or
|
|
280
|
-
// declare one — avoids tokenizing the whole repo's Python.
|
|
281
|
-
if (!/include_router|APIRouter/.test(content))
|
|
282
|
-
continue;
|
|
283
|
-
const facts = collectFileFacts(rel, content, fileSet);
|
|
284
|
-
factsByFile.set(rel, facts);
|
|
285
|
-
for (const [varName, prefix] of facts.ownPrefixes) {
|
|
286
|
-
ownPrefixByNode.set(nodeId(rel, varName), prefix);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
// Second pass: resolve mount edges now that every file's facts (imports,
|
|
290
|
-
// consts, router vars) are known.
|
|
291
|
-
for (const [rel, facts] of factsByFile) {
|
|
292
|
-
for (const mount of facts.mounts) {
|
|
293
|
-
const child = resolveMountArg(mount.arg, rel, facts);
|
|
294
|
-
if (!child)
|
|
295
|
-
continue;
|
|
296
|
-
let mountPrefix = "";
|
|
297
|
-
if (mount.prefixExpr !== null) {
|
|
298
|
-
if (mount.prefixExpr.startsWith('"')) {
|
|
299
|
-
mountPrefix = JSON.parse(mount.prefixExpr);
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
mountPrefix = facts.stringConsts.get(mount.prefixExpr) ?? "";
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
edges.push({ parent: nodeId(rel, mount.parentVar), child, mountPrefix });
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
// First incoming edge wins (a router mounted in two places is rare; pick one
|
|
309
|
-
// deterministically rather than guessing a "primary" mount).
|
|
310
|
-
const incoming = new Map();
|
|
311
|
-
for (const e of edges) {
|
|
312
|
-
if (!incoming.has(e.child))
|
|
313
|
-
incoming.set(e.child, e);
|
|
314
|
-
}
|
|
315
|
-
const memo = new Map();
|
|
316
|
-
function ancestorPrefix(node, depth) {
|
|
317
|
-
if (depth > MAX_MOUNT_DEPTH)
|
|
318
|
-
return "";
|
|
319
|
-
const cached = memo.get(node);
|
|
320
|
-
if (cached !== undefined)
|
|
321
|
-
return cached;
|
|
322
|
-
memo.set(node, ""); // cycle guard: treat as root until resolved
|
|
323
|
-
const inc = incoming.get(node);
|
|
324
|
-
if (!inc)
|
|
325
|
-
return ""; // root — no inherited prefix
|
|
326
|
-
const parentAncestor = ancestorPrefix(inc.parent, depth + 1);
|
|
327
|
-
const parentOwn = ownPrefixByNode.get(inc.parent) ?? "";
|
|
328
|
-
const full = joinRoutePrefix(joinRoutePrefix(parentAncestor, parentOwn), inc.mountPrefix);
|
|
329
|
-
memo.set(node, full);
|
|
330
|
-
return full;
|
|
331
|
-
}
|
|
332
|
-
for (const node of ownPrefixByNode.keys()) {
|
|
333
|
-
const prefix = ancestorPrefix(node, 0);
|
|
334
|
-
if (!prefix)
|
|
335
|
-
continue; // root router — leave to single-file parsing
|
|
336
|
-
const sep = node.indexOf("\n");
|
|
337
|
-
const relFile = node.slice(0, sep);
|
|
338
|
-
const varName = node.slice(sep + 1);
|
|
339
|
-
let perFile = result.get(relFile);
|
|
340
|
-
if (!perFile) {
|
|
341
|
-
perFile = new Map();
|
|
342
|
-
result.set(relFile, perFile);
|
|
343
|
-
}
|
|
344
|
-
perFile.set(varName, prefix);
|
|
345
|
-
}
|
|
346
|
-
return result;
|
|
347
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export interface ScannedEndpoint {
|
|
2
|
-
path: string;
|
|
3
|
-
methods: string[];
|
|
4
|
-
sourceFile: string;
|
|
5
|
-
/**
|
|
6
|
-
* 1-based line number of the route's declaration in `sourceFile`, when known.
|
|
7
|
-
* Used by hunk-aware diff scoping to attribute a changed line to the single
|
|
8
|
-
* route that owns it (rather than flagging every route in a touched file).
|
|
9
|
-
* Optional — absence disables scoping and falls back to file-level matching.
|
|
10
|
-
*/
|
|
11
|
-
line?: number;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Returns the relative paths of entry-point files that define the routing
|
|
15
|
-
* hierarchy for this repository. The LLM reads these files in Step 1.5 to
|
|
16
|
-
* trace router/module nesting and build an authoritative path resolution table.
|
|
17
|
-
*
|
|
18
|
-
* Intentionally returns only file paths — no extracted lines. Static regex
|
|
19
|
-
* extraction is fragile and framework-specific; the LLM reads the files directly
|
|
20
|
-
* and understands any framework's routing syntax.
|
|
21
|
-
*/
|
|
22
|
-
export declare function grepRouterMountingContext(repositoryPath: string): string[];
|
|
23
|
-
/**
|
|
24
|
-
* Return relative paths of all source files that are likely to define API
|
|
25
|
-
* routes. Two-pass strategy:
|
|
26
|
-
* 1. Fast: filename matches ROUTE_FILE_PATTERN (no file I/O needed).
|
|
27
|
-
* 2. Content: files not matched by name are read and checked for both a
|
|
28
|
-
* URL-path literal AND an HTTP method registration. This covers any
|
|
29
|
-
* framework the filename filter misses — Axum, Ktor, .NET, Hono, etc.
|
|
30
|
-
* Result is capped at MAX_CANDIDATE_FILES to keep the LLM prompt bounded.
|
|
31
|
-
*/
|
|
32
|
-
export declare function findCandidateRouteFiles(repositoryPath: string): string[];
|
|
33
|
-
export declare function scanAllRepoEndpoints(repositoryPath: string): ScannedEndpoint[];
|
|
34
|
-
export declare function scanRelatedEndpoints(repositoryPath: string, changedFiles: string[]): ScannedEndpoint[];
|