@skyramp/mcp 0.3.4 → 0.3.6-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/playwright/registerPlaywrightTools.js +92 -30
- package/build/playwright/traceRecordingPrompt.d.ts +6 -0
- package/build/playwright/traceRecordingPrompt.js +6 -2
- package/build/prompts/code-reuse.d.ts +1 -2
- package/build/prompts/code-reuse.js +182 -77
- package/build/prompts/modularization/integration-test-modularization.d.ts +2 -0
- package/build/prompts/modularization/integration-test-modularization.js +83 -41
- package/build/prompts/modularization/render.d.ts +18 -0
- package/build/prompts/modularization/render.js +12 -0
- package/build/prompts/modularization/ui-test-modularization.d.ts +3 -1
- package/build/prompts/modularization/ui-test-modularization.js +89 -47
- package/build/prompts/pom-aware-code-reuse.js +3 -1
- package/build/prompts/shared-helper-policy.d.ts +57 -0
- package/build/prompts/shared-helper-policy.js +135 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.js +62 -56
- package/build/prompts/test-recommendation/fullRepoCatalog.js +19 -8
- package/build/prompts/test-recommendation/recommendationShared.d.ts +28 -6
- package/build/prompts/test-recommendation/recommendationShared.js +90 -16
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +22 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +3 -3
- package/build/prompts/testbot/testbot-prompts.js +88 -33
- package/build/recommendation/budgeters/shared.js +105 -27
- package/build/recommendation/discriminators.js +13 -2
- package/build/recommendation/planRanker.d.ts +6 -6
- package/build/recommendation/planRanker.js +6 -61
- package/build/services/AnalyticsService.d.ts +7 -0
- package/build/services/AnalyticsService.js +7 -1
- package/build/services/ModularizationService.js +1 -3
- package/build/services/TestDiscoveryService.d.ts +0 -2
- package/build/services/TestDiscoveryService.js +2 -37
- package/build/services/TestGenerationService.d.ts +16 -0
- package/build/services/TestGenerationService.js +86 -10
- package/build/services/containerEnv.js +13 -12
- package/build/tools/code-refactor/codeReuseTool.js +279 -93
- package/build/tools/code-refactor/enhance-state.d.ts +49 -0
- package/build/tools/code-refactor/enhance-state.js +109 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -1
- package/build/tools/code-refactor/modularizationTool.js +9 -2
- package/build/tools/code-refactor/reuse-outcome.d.ts +23 -1
- package/build/tools/code-refactor/reuse-outcome.js +14 -4
- package/build/tools/code-refactor/reuse-state.d.ts +127 -5
- package/build/tools/code-refactor/reuse-state.js +628 -16
- package/build/tools/code-refactor/utils-verify-gates.d.ts +26 -0
- package/build/tools/code-refactor/utils-verify-gates.js +100 -0
- package/build/tools/code-refactor/verify-gates.d.ts +2 -1
- package/build/tools/code-refactor/verify-gates.js +90 -25
- package/build/tools/executeSkyrampTestTool.d.ts +19 -0
- package/build/tools/executeSkyrampTestTool.js +158 -8
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -2
- package/build/tools/generate-tests/generateE2ERestTool.js +16 -0
- package/build/tools/generate-tests/generateUIRestTool.d.ts +1 -0
- package/build/tools/generate-tests/generateUIRestTool.js +22 -0
- package/build/tools/generate-tests/scenarioLint.d.ts +2 -0
- package/build/tools/generate-tests/scenarioLint.js +127 -19
- package/build/tools/generate-tests/trace-reuse-guard.d.ts +20 -0
- package/build/tools/generate-tests/trace-reuse-guard.js +93 -0
- package/build/tools/submitReportTool.d.ts +38 -38
- package/build/tools/submitReportTool.js +487 -104
- package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
- package/build/tools/test-management/analyzeChangesTool.js +75 -12
- package/build/tools/test-management/analyzeTestHealthTool.js +7 -7
- package/build/tools/test-management/registerTestPlanTool.d.ts +203 -0
- package/build/tools/test-management/registerTestPlanTool.js +149 -23
- package/build/types/Recommendation.d.ts +34 -5
- package/build/types/RepositoryAnalysis.d.ts +133 -114
- package/build/types/RepositoryAnalysis.js +1 -1
- package/build/types/ReuseOutcome.d.ts +102 -6
- package/build/types/ReuseOutcome.js +16 -2
- package/build/types/TestRecommendation.js +21 -3
- package/build/types/TestTypes.js +14 -8
- package/build/types/TestbotReport.d.ts +25 -3
- package/build/types/index.d.ts +2 -2
- package/build/types/index.js +1 -1
- package/build/utils/AnalysisStateManager.d.ts +69 -1
- package/build/utils/AnalysisStateManager.js +69 -5
- package/build/utils/branchDiff.d.ts +10 -0
- package/build/utils/branchDiff.js +28 -0
- package/build/utils/changedRoutes.d.ts +29 -0
- package/build/utils/changedRoutes.js +87 -0
- package/build/utils/featureFlags.d.ts +21 -0
- package/build/utils/featureFlags.js +23 -0
- package/build/utils/frontendIntegration.js +34 -4
- package/build/utils/importerHop.d.ts +2 -8
- package/build/utils/importerHop.js +15 -53
- package/build/utils/pathMatching.d.ts +38 -0
- package/build/utils/pathMatching.js +71 -0
- package/build/utils/pathSignatures.d.ts +22 -0
- package/build/utils/pathSignatures.js +57 -0
- package/build/utils/planMatchKeys.d.ts +16 -3
- package/build/utils/planMatchKeys.js +26 -10
- package/build/utils/pluralization.d.ts +10 -0
- package/build/utils/pluralization.js +18 -0
- package/build/utils/pom-catalog-parse.d.ts +52 -0
- package/build/utils/pom-catalog-parse.js +141 -0
- package/build/utils/pom-scope/selector-extractor.d.ts +12 -0
- package/build/utils/pom-scope/selector-extractor.js +34 -8
- package/build/utils/pom-verify/verify.d.ts +6 -5
- package/build/utils/pom-verify/verify.js +8 -6
- package/build/utils/reportLanguage.d.ts +43 -0
- package/build/utils/reportLanguage.js +125 -0
- package/build/utils/reportVerification.d.ts +74 -4
- package/build/utils/reportVerification.js +259 -3
- package/build/utils/reuseRouting.d.ts +3 -0
- package/build/utils/reuseRouting.js +50 -0
- package/build/utils/routeParsers.d.ts +2 -0
- package/build/utils/routeParsers.js +65 -8
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +57 -45
- package/build/utils/subjectEndpoints.d.ts +19 -0
- package/build/utils/subjectEndpoints.js +98 -0
- package/build/utils/testFileClassification.d.ts +11 -0
- package/build/utils/testFileClassification.js +47 -0
- package/build/utils/uiPageEnumerator.d.ts +45 -19
- package/build/utils/uiPageEnumerator.js +95 -51
- package/build/utils/utils-verify/allow.d.ts +16 -0
- package/build/utils/utils-verify/allow.js +68 -0
- package/build/utils/utils-verify/call-sites.d.ts +34 -0
- package/build/utils/utils-verify/call-sites.js +154 -0
- package/build/utils/utils-verify/index.d.ts +7 -0
- package/build/utils/utils-verify/index.js +7 -0
- package/build/utils/utils-verify/language-spec.d.ts +91 -0
- package/build/utils/utils-verify/language-spec.js +210 -0
- package/build/utils/utils-verify/locate.d.ts +39 -0
- package/build/utils/utils-verify/locate.js +199 -0
- package/build/utils/utils-verify/parse.d.ts +34 -0
- package/build/utils/utils-verify/parse.js +177 -0
- package/build/utils/utils-verify/stage.d.ts +24 -0
- package/build/utils/utils-verify/stage.js +107 -0
- package/build/utils/utils-verify/verify.d.ts +63 -0
- package/build/utils/utils-verify/verify.js +168 -0
- package/build/utils/utils.d.ts +3 -1
- package/build/utils/utils.js +3 -1
- package/build/workspace/workspace.d.ts +32 -32
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +9 -5
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +16 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +115 -14
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +13 -1
- package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +27 -253
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-DtudTj_v.js → codeMirrorModule-DJMC4zNo.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BW82eAUI.js +196 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-FNMuBzX1.js → codeMirrorModule-CZfp96qZ.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-gpLo02E0.js +809 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.Bq1r1URj.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.VEfqi1qN.js +5 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- 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/package.json +1 -1
- package/package.json +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +0 -422
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +0 -1035
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +0 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +0 -5
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ScenarioStep } from "../types/RepositoryAnalysis.js";
|
|
2
|
+
/** A method+path pair extracted from an actually-changed (`+`/`-`) diff line. */
|
|
3
|
+
export interface ChangedRoute {
|
|
4
|
+
method: string;
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Extract method+path from every changed (`+`/`-`, non-header) line of a raw
|
|
9
|
+
* unified diff. Reuses `parseRouteLine`, which already strips the leading
|
|
10
|
+
* `+`/`-` marker and matches the same route-decorator patterns the endpoint
|
|
11
|
+
* scanner does.
|
|
12
|
+
*/
|
|
13
|
+
export declare function collectChangedRouteLines(diffText: string): ChangedRoute[];
|
|
14
|
+
/**
|
|
15
|
+
* The first step of `steps` that targets a method+path on the changed hunk, or
|
|
16
|
+
* undefined. Returns the STEP, not a boolean, because two callers need
|
|
17
|
+
* different things from the same comparison: the ranker asks whether any step
|
|
18
|
+
* matches, and the subject resolver needs which ones do.
|
|
19
|
+
*
|
|
20
|
+
* Diff-extracted paths are local to the file's own router declaration (e.g.
|
|
21
|
+
* "/suggestions"); scenario step paths are fully mounted (e.g.
|
|
22
|
+
* "/api/recipes/suggestions"). A local path matches when the step path ends
|
|
23
|
+
* with it at a segment boundary (`pathSuffixMatches`), so the cross-file mount
|
|
24
|
+
* prefix difference (see recoverRemovedEndpointsFromBase, SKYR-4026) does not
|
|
25
|
+
* prevent the match — while a plain `endsWith` would also wrongly match an
|
|
26
|
+
* unrelated path that merely shares a trailing substring (e.g. "/preorders"
|
|
27
|
+
* against "orders").
|
|
28
|
+
*/
|
|
29
|
+
export declare function findStepOnChangedRoute(steps: ScenarioStep[] | undefined, changedRoutes: ChangedRoute[]): ScenarioStep | undefined;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { parseRouteLine, normalizeDiffPath } from "./routeParsers.js";
|
|
2
|
+
import { isBareIdPath, pathSuffixMatches } from "./pathMatching.js";
|
|
3
|
+
/**
|
|
4
|
+
* Extract method+path from every changed (`+`/`-`, non-header) line of a raw
|
|
5
|
+
* unified diff. Reuses `parseRouteLine`, which already strips the leading
|
|
6
|
+
* `+`/`-` marker and matches the same route-decorator patterns the endpoint
|
|
7
|
+
* scanner does.
|
|
8
|
+
*/
|
|
9
|
+
export function collectChangedRouteLines(diffText) {
|
|
10
|
+
const routes = [];
|
|
11
|
+
let currentFile = "";
|
|
12
|
+
let removedFile = "";
|
|
13
|
+
for (const line of diffText.split("\n")) {
|
|
14
|
+
// Track the current file from the unified-diff header so parseRouteLine
|
|
15
|
+
// gets the real path — its UI-component guard (UI_COMPONENT_EXT) depends
|
|
16
|
+
// on it, or a route-shaped line inside a .tsx/.jsx file (e.g. a client
|
|
17
|
+
// router registration) gets misparsed as a changed backend route.
|
|
18
|
+
// A deleted file has "+++ /dev/null", so the old side is the only name it
|
|
19
|
+
// has. Without it parseRouteLine gets an empty path and every
|
|
20
|
+
// extension-gated framework (Go, Rails, PHP) rejects the line, losing the
|
|
21
|
+
// routes the deletion removed.
|
|
22
|
+
if (line.startsWith("--- ")) {
|
|
23
|
+
const spec = line.slice(4).trim().split("\t")[0];
|
|
24
|
+
removedFile = spec === "/dev/null" ? "" : normalizeDiffPath(spec);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (line.startsWith("+++ ")) {
|
|
28
|
+
const spec = line.slice(4).trim().split("\t")[0];
|
|
29
|
+
currentFile = spec === "/dev/null" ? removedFile : normalizeDiffPath(spec);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (line.startsWith("diff --git") || line.startsWith("index "))
|
|
33
|
+
continue;
|
|
34
|
+
if (!(line.startsWith("+") || line.startsWith("-")))
|
|
35
|
+
continue;
|
|
36
|
+
const parsed = parseRouteLine(line, currentFile);
|
|
37
|
+
if (parsed)
|
|
38
|
+
routes.push({ method: parsed.method, path: parsed.path });
|
|
39
|
+
}
|
|
40
|
+
return routes;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The first step of `steps` that targets a method+path on the changed hunk, or
|
|
44
|
+
* undefined. Returns the STEP, not a boolean, because two callers need
|
|
45
|
+
* different things from the same comparison: the ranker asks whether any step
|
|
46
|
+
* matches, and the subject resolver needs which ones do.
|
|
47
|
+
*
|
|
48
|
+
* Diff-extracted paths are local to the file's own router declaration (e.g.
|
|
49
|
+
* "/suggestions"); scenario step paths are fully mounted (e.g.
|
|
50
|
+
* "/api/recipes/suggestions"). A local path matches when the step path ends
|
|
51
|
+
* with it at a segment boundary (`pathSuffixMatches`), so the cross-file mount
|
|
52
|
+
* prefix difference (see recoverRemovedEndpointsFromBase, SKYR-4026) does not
|
|
53
|
+
* prevent the match — while a plain `endsWith` would also wrongly match an
|
|
54
|
+
* unrelated path that merely shares a trailing substring (e.g. "/preorders"
|
|
55
|
+
* against "orders").
|
|
56
|
+
*/
|
|
57
|
+
export function findStepOnChangedRoute(steps, changedRoutes) {
|
|
58
|
+
if (changedRoutes.length === 0)
|
|
59
|
+
return undefined;
|
|
60
|
+
for (const step of steps ?? []) {
|
|
61
|
+
const stepMethod = (step.method ?? "").toUpperCase();
|
|
62
|
+
const stepPath = (step.path ?? "").replace(/\/+$/, "");
|
|
63
|
+
for (const route of changedRoutes) {
|
|
64
|
+
if (route.method.toUpperCase() !== stepMethod)
|
|
65
|
+
continue;
|
|
66
|
+
const routePath = route.path.replace(/\/+$/, "");
|
|
67
|
+
// A diff line declaring nothing but a bare id — `router.delete("/:id")`
|
|
68
|
+
// from a mounted Express router, `@Delete(":id")` from NestJS — names no
|
|
69
|
+
// resource, so it suffix-matches every `DELETE …/{id}` step. Both callers
|
|
70
|
+
// ask "does this changed route touch this step", and the answer there is
|
|
71
|
+
// no: for the ranker it lifted an unrelated candidate over a business-rule
|
|
72
|
+
// one, and for subject resolution it would make every step a subject.
|
|
73
|
+
// A NAMED parameter (`/:order_id`) still matches only its own resource,
|
|
74
|
+
// because the helper unifies a parameter's spelling and not its name.
|
|
75
|
+
if (isBareIdPath(routePath))
|
|
76
|
+
continue;
|
|
77
|
+
if (routePath === "") {
|
|
78
|
+
if (stepPath === "" || stepPath === "/")
|
|
79
|
+
return step;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (pathSuffixMatches(stepPath, routePath))
|
|
83
|
+
return step;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
@@ -66,3 +66,24 @@ export declare function isOneClickEnabled(): boolean;
|
|
|
66
66
|
* the verification gates apply.
|
|
67
67
|
*/
|
|
68
68
|
export declare function isPomReuseEnabled(): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Gates SkyrampUtils code reuse for integration tests on the testbot path, and
|
|
71
|
+
* for UI tests whenever the POM path is off (SKYRAMP_FEATURE_UTILS_REUSE=1).
|
|
72
|
+
*
|
|
73
|
+
* When OFF (default), the testbot prompt does not set `codeReuse` on
|
|
74
|
+
* `skyramp_integration_test_generation` and its post-generation
|
|
75
|
+
* modularize→reuse step is disabled — integration tests are generated exactly
|
|
76
|
+
* as before, with no file-rewriting steps. UI tests keep their pre-existing
|
|
77
|
+
* single `skyramp_reuse_code` pass, which finds no helpers in inline Playwright
|
|
78
|
+
* output and no-ops.
|
|
79
|
+
*
|
|
80
|
+
* When ON, generated tests on modularize-first flows (see
|
|
81
|
+
* `isModularizeFirstTarget` in utils/reuseRouting.ts for which test types and
|
|
82
|
+
* flag combinations qualify) are modularized and their helpers consolidated
|
|
83
|
+
* into a shared SkyrampUtils file across sibling tests; the modularization
|
|
84
|
+
* prompts add canonical helper naming and the shared-helper assertion policy.
|
|
85
|
+
* On the NL path callers opt in per call via the `codeReuse` parameter, and the
|
|
86
|
+
* modularize-first routing still requires this flag — with it off, `codeReuse`
|
|
87
|
+
* yields the single reuse pass it did before the feature existed.
|
|
88
|
+
*/
|
|
89
|
+
export declare function isUtilsReuseEnabled(): boolean;
|
|
@@ -76,3 +76,26 @@ export function isOneClickEnabled() {
|
|
|
76
76
|
export function isPomReuseEnabled() {
|
|
77
77
|
return process.env.SKYRAMP_FEATURE_POM_REUSE === "1";
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Gates SkyrampUtils code reuse for integration tests on the testbot path, and
|
|
81
|
+
* for UI tests whenever the POM path is off (SKYRAMP_FEATURE_UTILS_REUSE=1).
|
|
82
|
+
*
|
|
83
|
+
* When OFF (default), the testbot prompt does not set `codeReuse` on
|
|
84
|
+
* `skyramp_integration_test_generation` and its post-generation
|
|
85
|
+
* modularize→reuse step is disabled — integration tests are generated exactly
|
|
86
|
+
* as before, with no file-rewriting steps. UI tests keep their pre-existing
|
|
87
|
+
* single `skyramp_reuse_code` pass, which finds no helpers in inline Playwright
|
|
88
|
+
* output and no-ops.
|
|
89
|
+
*
|
|
90
|
+
* When ON, generated tests on modularize-first flows (see
|
|
91
|
+
* `isModularizeFirstTarget` in utils/reuseRouting.ts for which test types and
|
|
92
|
+
* flag combinations qualify) are modularized and their helpers consolidated
|
|
93
|
+
* into a shared SkyrampUtils file across sibling tests; the modularization
|
|
94
|
+
* prompts add canonical helper naming and the shared-helper assertion policy.
|
|
95
|
+
* On the NL path callers opt in per call via the `codeReuse` parameter, and the
|
|
96
|
+
* modularize-first routing still requires this flag — with it off, `codeReuse`
|
|
97
|
+
* yields the single reuse pass it did before the feature existed.
|
|
98
|
+
*/
|
|
99
|
+
export function isUtilsReuseEnabled() {
|
|
100
|
+
return process.env.SKYRAMP_FEATURE_UTILS_REUSE === "1";
|
|
101
|
+
}
|
|
@@ -29,7 +29,7 @@ import * as path from "path";
|
|
|
29
29
|
import { isTestFile, isI18nLocaleFile } from "../prompts/test-recommendation/scopeAssessment.js";
|
|
30
30
|
import { logger } from "./logger.js";
|
|
31
31
|
import { walkDir } from "./fileWalk.js";
|
|
32
|
-
import { buildPathSignatures } from "./pathSignatures.js";
|
|
32
|
+
import { buildPathSignatures, importsIndexModule, isIndexModule } from "./pathSignatures.js";
|
|
33
33
|
import { IntegrationReason } from "../types/FrontendIntegration.js";
|
|
34
34
|
const MAX_IMPORTERS = 10;
|
|
35
35
|
const MAX_FILES_WALKED = 3000;
|
|
@@ -184,9 +184,11 @@ function checkOneFile(repositoryPath, file) {
|
|
|
184
184
|
}
|
|
185
185
|
try {
|
|
186
186
|
const signatures = buildPathSignatures(file);
|
|
187
|
-
const
|
|
188
|
-
.filter((candidate) => candidate !== file && !isExcludedImporterPath(candidate))
|
|
189
|
-
|
|
187
|
+
const candidates = searchProductionImporters(repositoryPath, signatures)
|
|
188
|
+
.filter((candidate) => candidate !== file && !isExcludedImporterPath(candidate));
|
|
189
|
+
const importers = isIndexModule(file)
|
|
190
|
+
? selectIndexModuleImporters(repositoryPath, file, candidates)
|
|
191
|
+
: candidates.slice(0, MAX_IMPORTERS);
|
|
190
192
|
return {
|
|
191
193
|
file,
|
|
192
194
|
integrated: importers.length > 0,
|
|
@@ -202,6 +204,34 @@ function checkOneFile(repositoryPath, file) {
|
|
|
202
204
|
return { file, integrated: true, importers: [], reason: IntegrationReason.ScanError };
|
|
203
205
|
}
|
|
204
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* An index module's directory stem (`/_constants'`) is a plain string match, so a
|
|
209
|
+
* file importing its own unrelated `./_constants` also hits. Keep only candidates
|
|
210
|
+
* whose specifier resolves to this module's directory. Reads stop once
|
|
211
|
+
* MAX_IMPORTERS are accepted; files over MAX_FILE_SIZE_BYTES are skipped like in
|
|
212
|
+
* manualWalkGrep; an unreadable candidate is kept (fail-open, like the module).
|
|
213
|
+
*/
|
|
214
|
+
function selectIndexModuleImporters(repositoryPath, file, candidates) {
|
|
215
|
+
const accepted = [];
|
|
216
|
+
for (const candidate of candidates) {
|
|
217
|
+
if (accepted.length >= MAX_IMPORTERS)
|
|
218
|
+
break;
|
|
219
|
+
const abs = path.join(repositoryPath, candidate);
|
|
220
|
+
let content;
|
|
221
|
+
try {
|
|
222
|
+
if (fs.statSync(abs).size > MAX_FILE_SIZE_BYTES)
|
|
223
|
+
continue;
|
|
224
|
+
content = fs.readFileSync(abs, "utf8");
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
accepted.push(candidate);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (importsIndexModule(candidate, content, file))
|
|
231
|
+
accepted.push(candidate);
|
|
232
|
+
}
|
|
233
|
+
return accepted;
|
|
234
|
+
}
|
|
205
235
|
/**
|
|
206
236
|
* Checks each changed frontend file for a production importer — the
|
|
207
237
|
* deterministic version of the grep the testbot prompt used to ask the
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sliceAddedLinesByFile } from "./branchDiff.js";
|
|
1
2
|
import type { ScannedEndpoint } from "./repoScanner.js";
|
|
2
3
|
/**
|
|
3
4
|
* Derives a singular, lowercase "resource token" from a backend source file
|
|
@@ -114,14 +115,7 @@ export declare function buildHopResponseShapeInputs(hopResult: ImporterHopResult
|
|
|
114
115
|
* to be exhaustive.
|
|
115
116
|
*/
|
|
116
117
|
export declare function extractFieldNamesFromAddedLines(lines: string[]): string[];
|
|
117
|
-
|
|
118
|
-
* Splits a unified diff into per-file added-line arrays, keyed by the `b/`
|
|
119
|
-
* (post-change) path. Exported so other file->resource-affinity callers
|
|
120
|
-
* (e.g. `pathAffinityClassification.ts`'s fallback, SKYR-3857) can derive the
|
|
121
|
-
* same per-file diff hunks this module uses for `deriveResourceTokensForFile`,
|
|
122
|
-
* without re-implementing the diff-header walk.
|
|
123
|
-
*/
|
|
124
|
-
export declare function sliceAddedLinesByFile(diffContent: string): Map<string, string[]>;
|
|
118
|
+
export { sliceAddedLinesByFile };
|
|
125
119
|
/**
|
|
126
120
|
* Splits a unified diff into per-file raw sections — every line after a file's
|
|
127
121
|
* `diff --git` header up to the next one — keyed by the `b/` (post-change) path.
|
|
@@ -56,8 +56,10 @@
|
|
|
56
56
|
import * as fs from "fs";
|
|
57
57
|
import * as path from "path";
|
|
58
58
|
import { logger } from "./logger.js";
|
|
59
|
-
import { extractResourceFromPath, isVersionLikeSegment, SKIP_PATH_SEGMENTS } from "./routeParsers.js";
|
|
60
|
-
import {
|
|
59
|
+
import { extractResourceFromPath, isParamSegment, isVersionLikeSegment, SKIP_PATH_SEGMENTS } from "./routeParsers.js";
|
|
60
|
+
import { sliceAddedLinesByFile } from "./branchDiff.js";
|
|
61
|
+
import { buildPathSignatures, importsIndexModule, isIndexModule } from "./pathSignatures.js";
|
|
62
|
+
import { singularize } from "./pluralization.js";
|
|
61
63
|
import { isFrontendFile, isTestFile } from "../prompts/test-recommendation/scopeAssessment.js";
|
|
62
64
|
const MAX_FILE_SIZE_BYTES = 512 * 1024;
|
|
63
65
|
// Mirrors the small `SOURCE_EXTS` copies already living in routeParsers.ts and
|
|
@@ -165,19 +167,6 @@ function tokensFromSymbol(symbol) {
|
|
|
165
167
|
!GENERIC_SYMBOL_TOKENS.has(token) &&
|
|
166
168
|
!GENERIC_RESOURCE_TOKENS.has(token));
|
|
167
169
|
}
|
|
168
|
-
/**
|
|
169
|
-
* Mirrors the private `singularize` in scenarioDrafting.ts (irregular-plural
|
|
170
|
-
* rules for -ies/-ses/trailing-s). Duplicated rather than imported to avoid a
|
|
171
|
-
* cross-module coupling for a 6-line pure function; keep the two in sync if
|
|
172
|
-
* the pluralization rules ever change.
|
|
173
|
-
*/
|
|
174
|
-
function singularize(word) {
|
|
175
|
-
if (word.endsWith("ies") && word.length > 3)
|
|
176
|
-
return word.slice(0, -3) + "y";
|
|
177
|
-
if (word.endsWith("ses") && word.length > 4)
|
|
178
|
-
return word.slice(0, -2);
|
|
179
|
-
return word.endsWith("s") && word.length > 1 ? word.slice(0, -1) : word;
|
|
180
|
-
}
|
|
181
170
|
/**
|
|
182
171
|
* Derives a singular, lowercase "resource token" from a backend source file
|
|
183
172
|
* path — e.g. "src/dtos/user.dto.ts" -> "user",
|
|
@@ -257,17 +246,6 @@ export function deriveResourceTokensForFile(file, diffHunk) {
|
|
|
257
246
|
}
|
|
258
247
|
return tokens.slice(0, MAX_RESOURCE_TOKENS);
|
|
259
248
|
}
|
|
260
|
-
// Mirrors the param-segment forms `isPathParam` in scenarioDrafting.ts
|
|
261
|
-
// recognizes — OpenAPI-style "{id}", Express-style ":id", and Next.js-style
|
|
262
|
-
// "[id]" / catch-all "[...id]" / optional catch-all "[[...id]]" (which needs
|
|
263
|
-
// its own alternative: it contains an inner "]", so the single-bracket form's
|
|
264
|
-
// [^\]]+ can never span it) — so the "primary" segment picked below never
|
|
265
|
-
// lands on a parameter regardless of which framework's route syntax produced
|
|
266
|
-
// the path.
|
|
267
|
-
const PARAM_SEGMENT_RE = /^\{[^}]+\}$|^:[A-Za-z_]\w*$|^\[\[[^\]]+\]\]$|^\[[^\]]+\]$/;
|
|
268
|
-
function isParamSegment(segment) {
|
|
269
|
-
return PARAM_SEGMENT_RE.test(segment);
|
|
270
|
-
}
|
|
271
249
|
/**
|
|
272
250
|
* All singular resource tokens implied by an endpoint path, used for affinity
|
|
273
251
|
* matching against a `deriveResourceToken` result. Shared by the importer-hop
|
|
@@ -353,9 +331,14 @@ export function mapUnmatchedFilesToImporterEndpoints(repositoryPath, unmatchedFi
|
|
|
353
331
|
if (resourceTokens.length === 0)
|
|
354
332
|
continue;
|
|
355
333
|
const signatures = buildPathSignatures(file);
|
|
334
|
+
// For an index module the directory stem is a plain string match, so also
|
|
335
|
+
// require the specifier to resolve to this module's directory.
|
|
336
|
+
const indexModule = isIndexModule(file);
|
|
356
337
|
const rawCandidates = scannedEndpoints.filter((ep) => {
|
|
357
338
|
const content = readCached(ep.sourceFile);
|
|
358
|
-
|
|
339
|
+
if (content === null || !signatures.some((sig) => content.includes(sig)))
|
|
340
|
+
return false;
|
|
341
|
+
return !indexModule || importsIndexModule(ep.sourceFile, content, file);
|
|
359
342
|
});
|
|
360
343
|
if (rawCandidates.length === 0)
|
|
361
344
|
continue;
|
|
@@ -451,32 +434,11 @@ export function extractFieldNamesFromAddedLines(lines) {
|
|
|
451
434
|
}
|
|
452
435
|
return [...fields];
|
|
453
436
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
* without re-implementing the diff-header walk.
|
|
460
|
-
*/
|
|
461
|
-
export function sliceAddedLinesByFile(diffContent) {
|
|
462
|
-
const sections = new Map();
|
|
463
|
-
let currentFile = null;
|
|
464
|
-
for (const line of diffContent.split("\n")) {
|
|
465
|
-
const headerMatch = /^diff --git a\/.+ b\/(.+)$/.exec(line);
|
|
466
|
-
if (headerMatch) {
|
|
467
|
-
currentFile = headerMatch[1];
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
if (!currentFile)
|
|
471
|
-
continue;
|
|
472
|
-
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
473
|
-
const arr = sections.get(currentFile) ?? [];
|
|
474
|
-
arr.push(line);
|
|
475
|
-
sections.set(currentFile, arr);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
return sections;
|
|
479
|
-
}
|
|
437
|
+
// `sliceAddedLinesByFile` now lives in `branchDiff.ts` (imported above) — moved
|
|
438
|
+
// there so modules this one already imports can reuse it without an import
|
|
439
|
+
// cycle. Re-exported here so existing importers of it from this module
|
|
440
|
+
// (pathAffinityClassification.ts) don't need to change their import path.
|
|
441
|
+
export { sliceAddedLinesByFile };
|
|
480
442
|
/**
|
|
481
443
|
* Splits a unified diff into per-file raw sections — every line after a file's
|
|
482
444
|
* `diff --git` header up to the next one — keyed by the `b/` (post-change) path.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Segment-aware path comparisons shared by endpoint/route matching across the
|
|
3
|
+
* codebase. Plain `startsWith`/`endsWith` are not sufficient for API paths:
|
|
4
|
+
* "/orders-archive".startsWith("/orders") and "/preorders".endsWith("orders")
|
|
5
|
+
* are both true even though the paths name different resources. These
|
|
6
|
+
* helpers require the match to land on a "/" (or path-end) boundary.
|
|
7
|
+
*
|
|
8
|
+
* Lives in its own module, independent of `routeParsers.ts` and
|
|
9
|
+
* `importerHop.ts`, so callers in either direction (`planRanker.ts` imports
|
|
10
|
+
* `routeParsers.ts`; `scenarioDrafting.ts` imports `importerHop.ts`, which
|
|
11
|
+
* imports `routeParsers.ts`) can both use it without risking an import cycle.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Returns true when `a` starts with `b` at a path-segment boundary.
|
|
15
|
+
* Plain `startsWith` is not sufficient — "/orders-archive".startsWith("/orders") is true
|
|
16
|
+
* even though they are different resources. Requiring the next character to be "/" or
|
|
17
|
+
* end-of-string ensures only genuine path prefixes are matched.
|
|
18
|
+
*/
|
|
19
|
+
export declare const isSegmentPrefix: (a: string, b: string) => boolean;
|
|
20
|
+
/**
|
|
21
|
+
* True when a path is nothing but a bare `id` parameter — `/:id`, `/{id}`.
|
|
22
|
+
*
|
|
23
|
+
* `canonicalSegment` unifies the SPELLING of a parameter, never its name, so
|
|
24
|
+
* `/{order_id}` still matches only a path ending in `{order_id}` — the name
|
|
25
|
+
* carries the resource and the match stays specific. A bare `id` carries
|
|
26
|
+
* nothing, so `/:id` ends `/api/users/{id}` and `/api/orders/{id}` alike. That
|
|
27
|
+
* is the shape a mounted Express router (`router.delete("/:id")`) and a NestJS
|
|
28
|
+
* handler (`@Delete(":id")`) declare, so it is common rather than exotic.
|
|
29
|
+
*
|
|
30
|
+
* Exported rather than folded into `pathSuffixMatches` because the two callers
|
|
31
|
+
* want opposite answers. `scenarioDrafting` resolves a router-relative path
|
|
32
|
+
* against the resource groups it knows, and its own test uses `/{order_id}` —
|
|
33
|
+
* a named parameter, which this predicate deliberately leaves alone.
|
|
34
|
+
* `planRanker` and subject resolution ask whether a diff line touches a given
|
|
35
|
+
* step, and there a bare `id` match is a false positive.
|
|
36
|
+
*/
|
|
37
|
+
export declare const isBareIdPath: (path: string) => boolean;
|
|
38
|
+
export declare function pathSuffixMatches(fullPath: string, suffix: string): boolean;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Segment-aware path comparisons shared by endpoint/route matching across the
|
|
3
|
+
* codebase. Plain `startsWith`/`endsWith` are not sufficient for API paths:
|
|
4
|
+
* "/orders-archive".startsWith("/orders") and "/preorders".endsWith("orders")
|
|
5
|
+
* are both true even though the paths name different resources. These
|
|
6
|
+
* helpers require the match to land on a "/" (or path-end) boundary.
|
|
7
|
+
*
|
|
8
|
+
* Lives in its own module, independent of `routeParsers.ts` and
|
|
9
|
+
* `importerHop.ts`, so callers in either direction (`planRanker.ts` imports
|
|
10
|
+
* `routeParsers.ts`; `scenarioDrafting.ts` imports `importerHop.ts`, which
|
|
11
|
+
* imports `routeParsers.ts`) can both use it without risking an import cycle.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Returns true when `a` starts with `b` at a path-segment boundary.
|
|
15
|
+
* Plain `startsWith` is not sufficient — "/orders-archive".startsWith("/orders") is true
|
|
16
|
+
* even though they are different resources. Requiring the next character to be "/" or
|
|
17
|
+
* end-of-string ensures only genuine path prefixes are matched.
|
|
18
|
+
*/
|
|
19
|
+
export const isSegmentPrefix = (a, b) => a.startsWith(b) && (a[b.length] === "/" || a[b.length] === undefined);
|
|
20
|
+
/**
|
|
21
|
+
* One spelling for a route parameter. Express and NestJS declare a parameter as
|
|
22
|
+
* `:uid`, so that is what a diff line carries; OpenAPI and every candidate path
|
|
23
|
+
* write `{uid}`. The two name the same segment, so a comparison that reads them
|
|
24
|
+
* as different text rejects a genuine match — `/v2/org/{orgId}/members/{uid}`
|
|
25
|
+
* against a changed `@Delete("/members/:uid")` is the measured case.
|
|
26
|
+
*
|
|
27
|
+
* Only the spelling is unified, not the parameter NAME: on the recorded pairs
|
|
28
|
+
* the names always agree, and treating any parameter as a wildcard would let
|
|
29
|
+
* unrelated routes match.
|
|
30
|
+
*/
|
|
31
|
+
const canonicalSegment = (segment) => segment.startsWith(":") ? `{${segment.slice(1)}}` : segment;
|
|
32
|
+
/**
|
|
33
|
+
* True when a path is nothing but a bare `id` parameter — `/:id`, `/{id}`.
|
|
34
|
+
*
|
|
35
|
+
* `canonicalSegment` unifies the SPELLING of a parameter, never its name, so
|
|
36
|
+
* `/{order_id}` still matches only a path ending in `{order_id}` — the name
|
|
37
|
+
* carries the resource and the match stays specific. A bare `id` carries
|
|
38
|
+
* nothing, so `/:id` ends `/api/users/{id}` and `/api/orders/{id}` alike. That
|
|
39
|
+
* is the shape a mounted Express router (`router.delete("/:id")`) and a NestJS
|
|
40
|
+
* handler (`@Delete(":id")`) declare, so it is common rather than exotic.
|
|
41
|
+
*
|
|
42
|
+
* Exported rather than folded into `pathSuffixMatches` because the two callers
|
|
43
|
+
* want opposite answers. `scenarioDrafting` resolves a router-relative path
|
|
44
|
+
* against the resource groups it knows, and its own test uses `/{order_id}` —
|
|
45
|
+
* a named parameter, which this predicate deliberately leaves alone.
|
|
46
|
+
* `planRanker` and subject resolution ask whether a diff line touches a given
|
|
47
|
+
* step, and there a bare `id` match is a false positive.
|
|
48
|
+
*/
|
|
49
|
+
export const isBareIdPath = (path) => {
|
|
50
|
+
const segs = path.split("/").filter(Boolean);
|
|
51
|
+
return (segs.length > 0 &&
|
|
52
|
+
segs.every((seg) => {
|
|
53
|
+
const canonical = canonicalSegment(seg);
|
|
54
|
+
return canonical.startsWith("{") && canonical.slice(1, -1).toLowerCase() === "id";
|
|
55
|
+
}));
|
|
56
|
+
};
|
|
57
|
+
// Segment-boundary suffix match: avoids "/orders" matching "/preorders".
|
|
58
|
+
export function pathSuffixMatches(fullPath, suffix) {
|
|
59
|
+
if (fullPath === suffix)
|
|
60
|
+
return true;
|
|
61
|
+
const fullSegs = fullPath.split("/").filter(Boolean);
|
|
62
|
+
const suffixSegs = suffix.split("/").filter(Boolean);
|
|
63
|
+
if (suffixSegs.length === 0 || suffixSegs.length > fullSegs.length)
|
|
64
|
+
return false;
|
|
65
|
+
const offset = fullSegs.length - suffixSegs.length;
|
|
66
|
+
for (let i = 0; i < suffixSegs.length; i++) {
|
|
67
|
+
if (canonicalSegment(fullSegs[offset + i]) !== canonicalSegment(suffixSegs[i]))
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fixed-string signatures that identify an import of `file` by module path.
|
|
3
|
+
* For an `index.*` module the parent directory name is a stem too (`/Button'`).
|
|
3
4
|
* Extensionless (`/Foo'`) covers TS/JS convention; extension-included
|
|
4
5
|
* (`/Foo.vue'`) covers Vue/Svelte, where imports keep the extension.
|
|
5
6
|
*/
|
|
6
7
|
export declare function buildPathSignatures(file: string): string[];
|
|
8
|
+
/** True for `index.<ext>` modules, including `index.d.ts` (the same test buildPathSignatures uses). */
|
|
9
|
+
export declare function isIndexModule(file: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* True when `importerContent` (the source of `importerFile`) contains an import
|
|
12
|
+
* specifier that targets the directory of `indexFile`. Companion to the
|
|
13
|
+
* parent-directory stem above: that stem is matched as a plain string, so
|
|
14
|
+
* `/_constants'` also hits a file importing its own, unrelated `./_constants`.
|
|
15
|
+
* Callers use this to keep only importers whose specifier targets this module.
|
|
16
|
+
*
|
|
17
|
+
* Specifier forms: `from '<spec>'`, `import '<spec>'` (side-effect), `import('<spec>')`,
|
|
18
|
+
* `require('<spec>')`. A trailing `/index` with any extension (`index.vue`,
|
|
19
|
+
* `index.d.ts`) is ignored, so extension-included imports (Vue/Svelte) match.
|
|
20
|
+
*
|
|
21
|
+
* Relative specifiers resolve against the importer's directory. Alias specifiers
|
|
22
|
+
* (`@/x`, `~/x`, `~~/x`, `#/x`, `src/x`) are accepted when the module directory
|
|
23
|
+
* ends with the aliased path AND the importer lives under the same root that the
|
|
24
|
+
* alias maps to — `packages/store/Page.tsx` importing `@/app/shop/_constants` does
|
|
25
|
+
* not match `packages/admin/app/shop/_constants/index.ts`. All paths are
|
|
26
|
+
* repo-relative with forward slashes.
|
|
27
|
+
*/
|
|
28
|
+
export declare function importsIndexModule(importerFile: string, importerContent: string, indexFile: string): boolean;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import * as path from "path";
|
|
18
18
|
/**
|
|
19
19
|
* Fixed-string signatures that identify an import of `file` by module path.
|
|
20
|
+
* For an `index.*` module the parent directory name is a stem too (`/Button'`).
|
|
20
21
|
* Extensionless (`/Foo'`) covers TS/JS convention; extension-included
|
|
21
22
|
* (`/Foo.vue'`) covers Vue/Svelte, where imports keep the extension.
|
|
22
23
|
*/
|
|
@@ -29,5 +30,61 @@ export function buildPathSignatures(file) {
|
|
|
29
30
|
const basename = isDts ? path.basename(file, ".d.ts") : path.basename(file, ext);
|
|
30
31
|
const quotes = [`'`, `"`, "`"];
|
|
31
32
|
const stems = [`/${basename}`, `/${basename}${ext}`];
|
|
33
|
+
// An index module is normally imported by its directory (`from './_constants'`,
|
|
34
|
+
// `from '@/components/Button'`); that import string never contains "index",
|
|
35
|
+
// so the basename stems alone miss every such importer.
|
|
36
|
+
if (isIndexModule(file)) {
|
|
37
|
+
const dir = path.basename(path.dirname(file));
|
|
38
|
+
if (dir && dir !== "." && dir !== "/")
|
|
39
|
+
stems.push(`/${dir}`);
|
|
40
|
+
}
|
|
32
41
|
return stems.flatMap((stem) => quotes.map((q) => `${stem}${q}`));
|
|
33
42
|
}
|
|
43
|
+
/** True for `index.<ext>` modules, including `index.d.ts` (the same test buildPathSignatures uses). */
|
|
44
|
+
export function isIndexModule(file) {
|
|
45
|
+
const isDts = /\.d\.ts$/i.test(file);
|
|
46
|
+
return (isDts ? path.basename(file, ".d.ts") : path.basename(file, path.extname(file))) === "index";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* True when `importerContent` (the source of `importerFile`) contains an import
|
|
50
|
+
* specifier that targets the directory of `indexFile`. Companion to the
|
|
51
|
+
* parent-directory stem above: that stem is matched as a plain string, so
|
|
52
|
+
* `/_constants'` also hits a file importing its own, unrelated `./_constants`.
|
|
53
|
+
* Callers use this to keep only importers whose specifier targets this module.
|
|
54
|
+
*
|
|
55
|
+
* Specifier forms: `from '<spec>'`, `import '<spec>'` (side-effect), `import('<spec>')`,
|
|
56
|
+
* `require('<spec>')`. A trailing `/index` with any extension (`index.vue`,
|
|
57
|
+
* `index.d.ts`) is ignored, so extension-included imports (Vue/Svelte) match.
|
|
58
|
+
*
|
|
59
|
+
* Relative specifiers resolve against the importer's directory. Alias specifiers
|
|
60
|
+
* (`@/x`, `~/x`, `~~/x`, `#/x`, `src/x`) are accepted when the module directory
|
|
61
|
+
* ends with the aliased path AND the importer lives under the same root that the
|
|
62
|
+
* alias maps to — `packages/store/Page.tsx` importing `@/app/shop/_constants` does
|
|
63
|
+
* not match `packages/admin/app/shop/_constants/index.ts`. All paths are
|
|
64
|
+
* repo-relative with forward slashes.
|
|
65
|
+
*/
|
|
66
|
+
export function importsIndexModule(importerFile, importerContent, indexFile) {
|
|
67
|
+
const moduleDir = path.posix.dirname(indexFile.replace(/\\/g, "/"));
|
|
68
|
+
const importerPosix = importerFile.replace(/\\/g, "/");
|
|
69
|
+
const importerDir = path.posix.dirname(importerPosix);
|
|
70
|
+
const specRe = /(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|\bimport\s+)(['"`])([^'"`]+)\1/g;
|
|
71
|
+
for (const m of importerContent.matchAll(specRe)) {
|
|
72
|
+
const spec = m[2].replace(/\/index(\.[\w]+)*$/, "").replace(/\/$/, "");
|
|
73
|
+
if (spec.startsWith(".")) {
|
|
74
|
+
if (path.posix.normalize(path.posix.join(importerDir, spec)) === moduleDir)
|
|
75
|
+
return true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const aliased = spec.replace(/^(@\/|~~\/|~\/|#\/|src\/)/, "");
|
|
79
|
+
if (aliased === spec)
|
|
80
|
+
continue; // bare package specifier — not a repo path
|
|
81
|
+
if (moduleDir === aliased)
|
|
82
|
+
return true; // alias maps to the repo root
|
|
83
|
+
if (moduleDir.endsWith("/" + aliased)) {
|
|
84
|
+
const root = moduleDir.slice(0, -(aliased.length + 1));
|
|
85
|
+
if (importerPosix.startsWith(root + "/"))
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
@@ -26,9 +26,22 @@ export interface PlanMatchQuery {
|
|
|
26
26
|
/** Lowercase, collapse non-alphanumeric runs to '-', trim leading/trailing '-'. */
|
|
27
27
|
export declare function slugifyName(name: string | undefined): string;
|
|
28
28
|
/**
|
|
29
|
-
* Normalize a path so param segments collapse:
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* Normalize a path so param/id segments collapse: `{brace}`, `:colon`,
|
|
30
|
+
* `[bracket]` params in any of routeParsers.ts's syntaxes, plus numeric,
|
|
31
|
+
* UUID, and long-hex id VALUES, all become `:p`. Delegates to
|
|
32
|
+
* routeParsers.ts's `isParamSegment`/`isOpaqueIdSegment` (SKYR-4214 task 5)
|
|
33
|
+
* rather than a second inline copy — this module previously had its own
|
|
34
|
+
* looser hex rule (`/^[0-9a-fA-F-]{16,}$/`, accepting `-`) and no
|
|
35
|
+
* bracket-syntax rule at all, so a Next.js `[id]` segment never collapsed
|
|
36
|
+
* and kept a match key from matching its `{id}`-spelled counterpart (the
|
|
37
|
+
* SKYR-4123 failure mode: a generated test's key stops matching its plan
|
|
38
|
+
* item). The characterization test in
|
|
39
|
+
* `pathSegmentClassification.characterization.test.ts` confirms every
|
|
40
|
+
* previously-agreeing input (UUID, numeric, 16+/24-char hex) still
|
|
41
|
+
* collapses to `:p` after this change — safe within one run because the
|
|
42
|
+
* register-time key and the generation-time query are computed by the same
|
|
43
|
+
* build; a plan file is never persisted across builds. Mirrors
|
|
44
|
+
* discriminators.ts's normalizePath (duplicated rather than imported —
|
|
32
45
|
* that function verifies discriminator claims against a diff, a different
|
|
33
46
|
* concern from plan matching, and keeping them independent avoids coupling
|
|
34
47
|
* this module's stability to phase 1's internal helper).
|