@skyramp/mcp 0.3.5 → 0.3.6-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +80 -34
- 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 +411 -114
- package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
- package/build/tools/test-management/analyzeChangesTool.js +71 -10
- 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 +70 -12
- 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 +10 -1
- package/build/types/index.d.ts +2 -2
- package/build/types/index.js +1 -1
- package/build/utils/AnalysisStateManager.d.ts +57 -1
- package/build/utils/AnalysisStateManager.js +54 -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/reportVerification.d.ts +64 -4
- package/build/utils/reportVerification.js +228 -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,98 @@
|
|
|
1
|
+
import { findStepOnChangedRoute } from "./changedRoutes.js";
|
|
2
|
+
import { extractResourceFromPath, isParamSegment } from "./routeParsers.js";
|
|
3
|
+
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4
|
+
/**
|
|
5
|
+
* A final-position DELETE that removes a resource an earlier POST created is
|
|
6
|
+
* cleanup, not the subject. SKYR-4214 measured the cost of counting it: the
|
|
7
|
+
* same fixture keyed as POST::flows on one run and DELETE::flows on the next,
|
|
8
|
+
* decided only by whether the model appended a cleanup step.
|
|
9
|
+
*
|
|
10
|
+
* The step count matters. A two-step POST-then-DELETE has no room for a
|
|
11
|
+
* verification step, so the DELETE is what the test is about; the cleanup shape
|
|
12
|
+
* always has a step between the create and the delete. A scenario that asserts
|
|
13
|
+
* the delete worked (…DELETE, then GET 404) leaves the DELETE off the final
|
|
14
|
+
* position, so it is not excluded either.
|
|
15
|
+
*/
|
|
16
|
+
function isTeardownStep(steps, index) {
|
|
17
|
+
const step = steps[index];
|
|
18
|
+
if (index !== steps.length - 1)
|
|
19
|
+
return false;
|
|
20
|
+
if ((step.method ?? "").toUpperCase() !== "DELETE")
|
|
21
|
+
return false;
|
|
22
|
+
if (steps.length < 3)
|
|
23
|
+
return false;
|
|
24
|
+
const resource = extractResourceFromPath(step.path ?? "");
|
|
25
|
+
// "unknown" is the sentinel for a path that names no resource, so comparing
|
|
26
|
+
// it by equality makes any two unidentifiable paths look like the same
|
|
27
|
+
// resource: `POST /` … `DELETE /{id}` read as create-then-cleanup and the
|
|
28
|
+
// DELETE — the real subject — got skipped. Only a named resource can pair.
|
|
29
|
+
if (resource === "unknown")
|
|
30
|
+
return false;
|
|
31
|
+
return steps.slice(0, index).some((earlier) => (earlier.method ?? "").toUpperCase() === "POST" &&
|
|
32
|
+
extractResourceFromPath(earlier.path ?? "") === resource);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A route declaration made only of parameter segments (or none at all, e.g.
|
|
36
|
+
* "/" or "") names no resource, so it cannot tell one endpoint from another —
|
|
37
|
+
* every step's path ends in the same param segment. SKYR-4214: a changed
|
|
38
|
+
* `@router.delete("/{id}")` line matched a step's trailing DELETE cleanup
|
|
39
|
+
* step just as well as the step it was actually meant to catch, which
|
|
40
|
+
* reinstated the bug this file exists to fix. Rule 1 only accepts a route
|
|
41
|
+
* that contributes at least one non-parameter segment.
|
|
42
|
+
*/
|
|
43
|
+
function routeIdentifiesResource(routePath) {
|
|
44
|
+
return routePath.split("/").filter(Boolean).some((segment) => !isParamSegment(segment));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The endpoints a scenario tests, in step order.
|
|
48
|
+
*
|
|
49
|
+
* 1. Every step on a changed diff line that names a resource. A test written
|
|
50
|
+
* for a pull request is about what the pull request changed, and a
|
|
51
|
+
* scenario can exercise more than one changed endpoint.
|
|
52
|
+
* 2. If no step matches: the last mutating step that is not a teardown step.
|
|
53
|
+
* Earlier mutations are usually setup (POST /products before PATCH /orders).
|
|
54
|
+
* 3. If no mutating step remains: the last step.
|
|
55
|
+
*
|
|
56
|
+
* Returns an empty list only for a scenario with no steps. Every caller must
|
|
57
|
+
* treat an empty list as "no information", never as "covered".
|
|
58
|
+
*/
|
|
59
|
+
export function resolveSubjectEndpoints(scenario, opts) {
|
|
60
|
+
const steps = scenario.steps ?? [];
|
|
61
|
+
if (steps.length === 0)
|
|
62
|
+
return [];
|
|
63
|
+
const changedRoutes = (opts.changedRoutes ?? []).filter((route) => routeIdentifiesResource(route.path));
|
|
64
|
+
if (changedRoutes.length > 0) {
|
|
65
|
+
// Attribute PER ROUTE, not per step. A diff-extracted route is local to its
|
|
66
|
+
// own router declaration — the mount prefix lives in another file, so a bare
|
|
67
|
+
// `POST /items` suffix-matches a step on `/orders/{id}/items` and one on
|
|
68
|
+
// `/products/{id}/items` equally. Recording both would invent a subject the
|
|
69
|
+
// diff does not name, and dedup then needs every unrelated key covered
|
|
70
|
+
// before the candidate can be removed.
|
|
71
|
+
//
|
|
72
|
+
// A route matching exactly one step names that step. A route matching
|
|
73
|
+
// several names none of them: there is no evidence for choosing, and a
|
|
74
|
+
// guess is worse than an omission. Other routes still contribute, so a
|
|
75
|
+
// scenario that genuinely tests two changed endpoints keeps both subjects.
|
|
76
|
+
const attributed = [];
|
|
77
|
+
for (const route of changedRoutes) {
|
|
78
|
+
const matches = steps.filter((step) => findStepOnChangedRoute([step], [route]));
|
|
79
|
+
const distinct = new Map(matches.map((step) => [`${step.method} ${step.path}`, step]));
|
|
80
|
+
if (distinct.size === 1)
|
|
81
|
+
attributed.push([...distinct.values()][0]);
|
|
82
|
+
}
|
|
83
|
+
// Keep step order and drop the duplicates two routes can both name.
|
|
84
|
+
const onChanged = steps.filter((step) => attributed.includes(step));
|
|
85
|
+
if (onChanged.length > 0) {
|
|
86
|
+
return onChanged.map((step) => ({ method: step.method, path: step.path }));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
90
|
+
if (!MUTATING_METHODS.has((steps[i].method ?? "").toUpperCase()))
|
|
91
|
+
continue;
|
|
92
|
+
if (isTeardownStep(steps, i))
|
|
93
|
+
continue;
|
|
94
|
+
return [{ method: steps[i].method, path: steps[i].path }];
|
|
95
|
+
}
|
|
96
|
+
const last = steps[steps.length - 1];
|
|
97
|
+
return [{ method: last.method, path: last.path }];
|
|
98
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single definition of "discovery treats this path as a test file", shared by
|
|
3
|
+
* discovery itself and by callers that must agree with what discovery classified as
|
|
4
|
+
* external coverage. A narrower predicate silently disagrees: `tests/test_orders.py`
|
|
5
|
+
* is external coverage here but is not a test file to `isTestFile` in
|
|
6
|
+
* scopeAssessment.ts, which only knows the Skyramp and JS/TS spec spellings.
|
|
7
|
+
*
|
|
8
|
+
* Accepts absolute or repo-relative paths. The directory patterns need a leading
|
|
9
|
+
* separator, which a repo-relative path has not got, so one is prepended.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isDiscoveredTestFile(filePath: string): boolean;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
// Test file naming patterns — match against basename.
|
|
3
|
+
// Includes Skyramp-generated suffixes so that files like orders_smoke.py placed
|
|
4
|
+
// outside a recognized test directory are still discovered.
|
|
5
|
+
const TEST_FILE_PATTERNS = [
|
|
6
|
+
/^test_.*\.(py|js|ts|rb|go|php)$/, // test_*.py, test_*.rb, test_*.go
|
|
7
|
+
/.*_test\.(py|ts|js|go|rs)$/, // *_test.py, *_test.go, *_test.rs
|
|
8
|
+
/.*\.test\.(ts|js|tsx|jsx)$/, // *.test.ts, *.test.js, *.test.tsx
|
|
9
|
+
/.*\..*spec\.(ts|js|tsx|jsx|rb)$/, // *.spec.ts, *.e2e-spec.ts, *.unit-spec.ts
|
|
10
|
+
/.*Test\.(java|kt|kts|cs|scala|swift|m)$/, // *Test.java, *Test.kt, *Test.m (ObjC)
|
|
11
|
+
/.*Tests\.(cs|swift|m)$/, // *Tests.cs, *Tests.swift, *Tests.m (ObjC)
|
|
12
|
+
/.*_spec\.rb$/, // *_spec.rb (RSpec)
|
|
13
|
+
/.*\.test\.php$/, // *.test.php
|
|
14
|
+
/.*Test\.php$/, // *Test.php (PHPUnit)
|
|
15
|
+
// Skyramp-generated test suffixes — must be matched even outside test dirs
|
|
16
|
+
/.*_(?:smoke|contract|fuzz|integration|load|e2e|ui)\.(py|ts|js|java|rb|go|cs|kt|kts|scala|swift|php|rs)$/i,
|
|
17
|
+
];
|
|
18
|
+
// Directory patterns that signal test-owned files — works with both / and \ separators.
|
|
19
|
+
// Includes standard test dirs, snapshot dirs, and common helper/fixture dirs used by
|
|
20
|
+
// Playwright, Cypress, and other frameworks for page objects and support code.
|
|
21
|
+
const TEST_DIR_PATTERNS = [
|
|
22
|
+
/[\\/]tests?[\\/]/,
|
|
23
|
+
/[\\/]__tests__[\\/]/,
|
|
24
|
+
/[\\/]__snapshots__[\\/]/, // Jest/Vitest snapshot directories
|
|
25
|
+
/[\\/]spec[\\/]/,
|
|
26
|
+
/[\\/]e2e[\\/]/,
|
|
27
|
+
/[\\/]cypress[\\/]/, // Cypress support, fixtures, plugins
|
|
28
|
+
/[\\/]playwright[\\/]/, // Playwright fixtures and helpers
|
|
29
|
+
/[\\/]page-?objects?[\\/]/, // page-objects/, page-object/, pageObjects/
|
|
30
|
+
/[\\/]fixtures[\\/](?!db[\\/]|seed[\\/]|migrations[\\/])/, // test fixture files (not db/seed/migration fixtures)
|
|
31
|
+
/[\\/]cypress[\\/]support[\\/]/, // Cypress support directory (scoped to avoid matching app support/ dirs)
|
|
32
|
+
];
|
|
33
|
+
/**
|
|
34
|
+
* The single definition of "discovery treats this path as a test file", shared by
|
|
35
|
+
* discovery itself and by callers that must agree with what discovery classified as
|
|
36
|
+
* external coverage. A narrower predicate silently disagrees: `tests/test_orders.py`
|
|
37
|
+
* is external coverage here but is not a test file to `isTestFile` in
|
|
38
|
+
* scopeAssessment.ts, which only knows the Skyramp and JS/TS spec spellings.
|
|
39
|
+
*
|
|
40
|
+
* Accepts absolute or repo-relative paths. The directory patterns need a leading
|
|
41
|
+
* separator, which a repo-relative path has not got, so one is prepended.
|
|
42
|
+
*/
|
|
43
|
+
export function isDiscoveredTestFile(filePath) {
|
|
44
|
+
if (TEST_FILE_PATTERNS.some((p) => p.test(path.basename(filePath))))
|
|
45
|
+
return true;
|
|
46
|
+
return TEST_DIR_PATTERNS.some((p) => p.test(`/${filePath}`));
|
|
47
|
+
}
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
* Next.js (app/ and pages/), Nuxt / Vue Router file-based, and SvelteKit.
|
|
15
15
|
* Gated on detection of a framework config file in the repo.
|
|
16
16
|
*
|
|
17
|
+
* Strategy 1b — Import graph
|
|
18
|
+
* For a changed file that is not itself a route (a component, a constants
|
|
19
|
+
* module), map the production files that import it through the same
|
|
20
|
+
* filesystem-route mapper. Uses the importer list the production-importer integration
|
|
21
|
+
* check already computed. Merged with strategy 1 rather than tried after it.
|
|
22
|
+
*
|
|
17
23
|
* Strategy 2 — Source-grounded routes
|
|
18
24
|
* Walks router/App files with the TS Compiler API for code-defined
|
|
19
25
|
* route declarations like `<Route path="/cart" element={<Cart/>}>`.
|
|
@@ -26,16 +32,38 @@
|
|
|
26
32
|
* Strategy 3 — Root fallback
|
|
27
33
|
* Read the workspace's frontend baseUrl and treat it as the single
|
|
28
34
|
* candidate page. Always available when a frontend service is configured.
|
|
35
|
+
*
|
|
36
|
+
* When no frontend baseUrl can be resolved, strategies 1/1b still run and their
|
|
37
|
+
* results are returned as bare URL paths (`baseUrlResolved: false`) so the agent
|
|
38
|
+
* can prefix the base URL it discovers; strategies 2–3 need a host and are skipped.
|
|
29
39
|
*/
|
|
30
|
-
|
|
40
|
+
import type { FrontendFileIntegration } from "../types/FrontendIntegration.js";
|
|
41
|
+
export type CandidateUiPageStrategy = "framework-route-grep" | "import-graph" | "source-grounded-routes" | "dart-go-router" | "root-fallback";
|
|
31
42
|
export interface CandidateUiPage {
|
|
32
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Fully qualified URL (workspace baseUrl + framework-derived path), or a bare
|
|
45
|
+
* URL path when `baseUrlResolved` is false.
|
|
46
|
+
*/
|
|
33
47
|
url: string;
|
|
34
48
|
/** Source files that suggested this page (for traceability). */
|
|
35
49
|
sourcedFrom: string[];
|
|
36
50
|
/** Which strategy enumerated it. */
|
|
37
51
|
strategy: CandidateUiPageStrategy;
|
|
52
|
+
/**
|
|
53
|
+
* Present (false) only when no frontend baseUrl could be resolved and `url`
|
|
54
|
+
* is a bare path the agent must prefix. Absent means `url` is complete.
|
|
55
|
+
*/
|
|
56
|
+
baseUrlResolved?: false;
|
|
57
|
+
/** import-graph only: the route files that import `sourcedFrom` and produced this path. */
|
|
58
|
+
via?: string[];
|
|
38
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Upper bound on candidate pages. Strategy 1b is bounded by changed files ×
|
|
62
|
+
* importers, and every candidate costs the agent a navigate + a capture, so the
|
|
63
|
+
* list is cut here — direct route matches first, then import-graph pages that
|
|
64
|
+
* render the most changed files.
|
|
65
|
+
*/
|
|
66
|
+
export declare const MAX_CANDIDATE_PAGES = 10;
|
|
39
67
|
/**
|
|
40
68
|
* File-system → URL mapping for filesystem-routed frameworks.
|
|
41
69
|
*
|
|
@@ -63,15 +91,7 @@ export declare function frameworkFileToUrlPath(filePath: string): string | null;
|
|
|
63
91
|
* and the `frontendFiles[]`-derived candidate frontend roots.
|
|
64
92
|
*/
|
|
65
93
|
export declare function detectsFilesystemRouting(repositoryPath: string): boolean;
|
|
66
|
-
/**
|
|
67
|
-
* Strategy 1 implementation. Filters frontend files through the framework
|
|
68
|
-
* route mapper; collects the URL paths that mapped successfully.
|
|
69
|
-
*
|
|
70
|
-
* Suppressed when the repo doesn't have a recognized filesystem-routing
|
|
71
|
-
* framework config — see `detectsFilesystemRouting`. Without that guard,
|
|
72
|
-
* repos with conventional `pages/` directories but code-defined routes
|
|
73
|
-
* (e.g. React with React Router) produce false-positive URLs.
|
|
74
|
-
*/
|
|
94
|
+
/** Strategy 1 only (no import graph), joined to `baseUrl`. */
|
|
75
95
|
export declare function findCandidatePagesByFrameworkRoute(frontendFiles: string[], baseUrl: string, repositoryPath?: string): CandidateUiPage[];
|
|
76
96
|
/**
|
|
77
97
|
* Strategy 2 implementation. Walks router/App files with the TS Compiler API
|
|
@@ -126,21 +146,27 @@ export declare function findRootFallbackPage(repositoryPath: string): Promise<Ca
|
|
|
126
146
|
* is the picked frontend service's serviceName, sanitized via
|
|
127
147
|
* `sanitizeServiceName`)
|
|
128
148
|
* 2. Global override: `SKYRAMP_TEST_BASE_URL` (single service or all
|
|
129
|
-
* services share one URL)
|
|
149
|
+
* services share one URL). Also honored when the workspace declares no
|
|
150
|
+
* services at all — the action can know the URL without a workspace entry.
|
|
130
151
|
* 3. Static workspace.yml value: `api.baseUrl` from the picked frontend
|
|
131
152
|
* service
|
|
132
153
|
*
|
|
133
|
-
* Returns null when
|
|
154
|
+
* Returns null when neither the environment nor any service has a usable URL.
|
|
134
155
|
*
|
|
135
156
|
* Exported (vs private) so analyze_changes and future tools can reuse the
|
|
136
157
|
* same selection without re-implementing it.
|
|
137
158
|
*/
|
|
138
159
|
export declare function pickFrontendBaseUrl(repositoryPath: string): Promise<string | null>;
|
|
139
160
|
/**
|
|
140
|
-
* Run the strategy ladder
|
|
141
|
-
* routes (2) → root fallback (3). Each
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
161
|
+
* Run the strategy ladder: framework route grep + import graph (1/1b) →
|
|
162
|
+
* source-grounded routes (2) → Dart GoRouter (2.5) → root fallback (3). Each
|
|
163
|
+
* later strategy is consulted only if the earlier ones produced no candidates.
|
|
164
|
+
*
|
|
165
|
+
* `integration` is the production-importer check for the same files; when given,
|
|
166
|
+
* changed non-route files resolve to the pages that import them.
|
|
167
|
+
*
|
|
168
|
+
* Without a resolvable frontend baseUrl only strategies 1/1b can run; their
|
|
169
|
+
* paths are returned with `baseUrlResolved: false` for the agent to prefix.
|
|
170
|
+
* Returns empty only when nothing resolves at all.
|
|
145
171
|
*/
|
|
146
|
-
export declare function enumerateCandidateUiPages(repositoryPath: string, frontendFiles: string[]): Promise<CandidateUiPage[]>;
|
|
172
|
+
export declare function enumerateCandidateUiPages(repositoryPath: string, frontendFiles: string[], integration?: FrontendFileIntegration[]): Promise<CandidateUiPage[]>;
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
* Next.js (app/ and pages/), Nuxt / Vue Router file-based, and SvelteKit.
|
|
15
15
|
* Gated on detection of a framework config file in the repo.
|
|
16
16
|
*
|
|
17
|
+
* Strategy 1b — Import graph
|
|
18
|
+
* For a changed file that is not itself a route (a component, a constants
|
|
19
|
+
* module), map the production files that import it through the same
|
|
20
|
+
* filesystem-route mapper. Uses the importer list the production-importer integration
|
|
21
|
+
* check already computed. Merged with strategy 1 rather than tried after it.
|
|
22
|
+
*
|
|
17
23
|
* Strategy 2 — Source-grounded routes
|
|
18
24
|
* Walks router/App files with the TS Compiler API for code-defined
|
|
19
25
|
* route declarations like `<Route path="/cart" element={<Cart/>}>`.
|
|
@@ -26,6 +32,10 @@
|
|
|
26
32
|
* Strategy 3 — Root fallback
|
|
27
33
|
* Read the workspace's frontend baseUrl and treat it as the single
|
|
28
34
|
* candidate page. Always available when a frontend service is configured.
|
|
35
|
+
*
|
|
36
|
+
* When no frontend baseUrl can be resolved, strategies 1/1b still run and their
|
|
37
|
+
* results are returned as bare URL paths (`baseUrlResolved: false`) so the agent
|
|
38
|
+
* can prefix the base URL it discovers; strategies 2–3 need a host and are skipped.
|
|
29
39
|
*/
|
|
30
40
|
import * as fs from "fs";
|
|
31
41
|
import * as path from "path";
|
|
@@ -33,6 +43,13 @@ import { extractDartRoutes } from "./dartRouteExtractor.js";
|
|
|
33
43
|
import { extractSourceRoutes } from "./sourceRouteExtractor.js";
|
|
34
44
|
import { hasFlutterSdkDep } from "../prompts/test-recommendation/scopeAssessment.js";
|
|
35
45
|
import { readWorkspaceConfigRaw } from "./workspaceAuth.js";
|
|
46
|
+
/**
|
|
47
|
+
* Upper bound on candidate pages. Strategy 1b is bounded by changed files ×
|
|
48
|
+
* importers, and every candidate costs the agent a navigate + a capture, so the
|
|
49
|
+
* list is cut here — direct route matches first, then import-graph pages that
|
|
50
|
+
* render the most changed files.
|
|
51
|
+
*/
|
|
52
|
+
export const MAX_CANDIDATE_PAGES = 10;
|
|
36
53
|
// ── Strategy 1: framework route grep ────────────────────────────────────────
|
|
37
54
|
/**
|
|
38
55
|
* File-system → URL mapping for filesystem-routed frameworks.
|
|
@@ -61,8 +78,9 @@ export function frameworkFileToUrlPath(filePath) {
|
|
|
61
78
|
return "/" + stripRouteGroups(nextAppMatch[1]).replace(/\[(\.\.\.)?(\w+)\]/g, ":$2");
|
|
62
79
|
}
|
|
63
80
|
// Next.js Pages Router: pages/foo.tsx → /foo, pages/index.tsx → /
|
|
64
|
-
// Excludes pages/api/* (
|
|
65
|
-
|
|
81
|
+
// Excludes pages/api/* (API handlers) and the framework files _app, _document,
|
|
82
|
+
// _error (they wrap every page and are not routes of their own).
|
|
83
|
+
const nextPagesMatch = normalized.match(/(?:^|\/)pages\/(?!api\/)(?!_(?:app|document|error)\.)(.+?)\.(tsx|jsx|ts|js)$/);
|
|
66
84
|
if (nextPagesMatch) {
|
|
67
85
|
let urlPath = nextPagesMatch[1];
|
|
68
86
|
// /index → /, /foo/index → /foo
|
|
@@ -140,43 +158,64 @@ export function detectsFilesystemRouting(repositoryPath) {
|
|
|
140
158
|
return false;
|
|
141
159
|
}
|
|
142
160
|
/**
|
|
143
|
-
*
|
|
144
|
-
* route
|
|
161
|
+
* Strategies 1 + 1b: map changed route files (1) and the route files that import
|
|
162
|
+
* changed non-route files (1b) to URL paths. Deduped by path; when both name the
|
|
163
|
+
* same path the strategy-1 entry wins and the extra source is appended.
|
|
145
164
|
*
|
|
146
165
|
* Suppressed when the repo doesn't have a recognized filesystem-routing
|
|
147
166
|
* framework config — see `detectsFilesystemRouting`. Without that guard,
|
|
148
167
|
* repos with conventional `pages/` directories but code-defined routes
|
|
149
168
|
* (e.g. React with React Router) produce false-positive URLs.
|
|
150
169
|
*/
|
|
151
|
-
|
|
170
|
+
function collectRoutePaths(frontendFiles, integration, repositoryPath) {
|
|
152
171
|
// When repositoryPath is provided, gate on framework detection. When
|
|
153
172
|
// omitted (e.g. older callers, unit tests), preserve original behavior.
|
|
154
173
|
if (repositoryPath !== undefined && !detectsFilesystemRouting(repositoryPath)) {
|
|
155
174
|
return [];
|
|
156
175
|
}
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
if (urlPath === null)
|
|
161
|
-
continue;
|
|
162
|
-
const normalizedBase = baseUrl.replace(/\/$/, "");
|
|
163
|
-
const fullUrl = normalizedBase + urlPath;
|
|
164
|
-
const existing = byUrl.get(fullUrl);
|
|
176
|
+
const byPath = new Map();
|
|
177
|
+
const add = (urlPath, sourceFile, strategy, via) => {
|
|
178
|
+
const existing = byPath.get(urlPath);
|
|
165
179
|
if (existing) {
|
|
166
|
-
// Same
|
|
167
|
-
if (!existing.sourcedFrom.includes(
|
|
168
|
-
existing.sourcedFrom.push(
|
|
169
|
-
|
|
180
|
+
// Same path surfaced by multiple files — track all sources.
|
|
181
|
+
if (!existing.sourcedFrom.includes(sourceFile))
|
|
182
|
+
existing.sourcedFrom.push(sourceFile);
|
|
183
|
+
if (via && existing.via && !existing.via.includes(via))
|
|
184
|
+
existing.via.push(via);
|
|
170
185
|
}
|
|
171
186
|
else {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
187
|
+
byPath.set(urlPath, { path: urlPath, sourcedFrom: [sourceFile], strategy, ...(via ? { via: [via] } : {}) });
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
for (const file of frontendFiles) {
|
|
191
|
+
const urlPath = frameworkFileToUrlPath(file);
|
|
192
|
+
if (urlPath !== null)
|
|
193
|
+
add(urlPath, file, "framework-route-grep");
|
|
194
|
+
}
|
|
195
|
+
for (const entry of integration ?? []) {
|
|
196
|
+
if (!entry.integrated)
|
|
197
|
+
continue;
|
|
198
|
+
for (const importer of entry.importers) {
|
|
199
|
+
const urlPath = frameworkFileToUrlPath(importer);
|
|
200
|
+
if (urlPath !== null)
|
|
201
|
+
add(urlPath, entry.file, "import-graph", importer);
|
|
177
202
|
}
|
|
178
203
|
}
|
|
179
|
-
|
|
204
|
+
const all = Array.from(byPath.values());
|
|
205
|
+
const rank = (c) => (c.strategy === "framework-route-grep" ? 1 : 0) * 1000 + c.sourcedFrom.length;
|
|
206
|
+
return all.sort((a, b) => rank(b) - rank(a)).slice(0, MAX_CANDIDATE_PAGES);
|
|
207
|
+
}
|
|
208
|
+
function joinBaseUrl(baseUrl, candidate) {
|
|
209
|
+
return {
|
|
210
|
+
url: baseUrl.replace(/\/$/, "") + candidate.path,
|
|
211
|
+
sourcedFrom: candidate.sourcedFrom,
|
|
212
|
+
strategy: candidate.strategy,
|
|
213
|
+
...(candidate.via ? { via: candidate.via } : {}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/** Strategy 1 only (no import graph), joined to `baseUrl`. */
|
|
217
|
+
export function findCandidatePagesByFrameworkRoute(frontendFiles, baseUrl, repositoryPath) {
|
|
218
|
+
return collectRoutePaths(frontendFiles, undefined, repositoryPath).map((c) => joinBaseUrl(baseUrl, c));
|
|
180
219
|
}
|
|
181
220
|
// ── Strategy 2: source-grounded routes ──────────────────────────────────────
|
|
182
221
|
/**
|
|
@@ -302,13 +341,10 @@ export function findCandidatePagesByDartRoute(repositoryPath, baseUrl, frontendF
|
|
|
302
341
|
*/
|
|
303
342
|
export async function findRootFallbackPage(repositoryPath) {
|
|
304
343
|
const baseUrl = await pickFrontendBaseUrl(repositoryPath);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
sourcedFrom: [],
|
|
310
|
-
strategy: "root-fallback",
|
|
311
|
-
};
|
|
344
|
+
return baseUrl ? rootFallbackPage(baseUrl) : null;
|
|
345
|
+
}
|
|
346
|
+
function rootFallbackPage(baseUrl) {
|
|
347
|
+
return { url: baseUrl, sourcedFrom: [], strategy: "root-fallback" };
|
|
312
348
|
}
|
|
313
349
|
/**
|
|
314
350
|
* Mirror of testbot.git src/services.ts exportServiceBaseUrlEnvVars sanitize().
|
|
@@ -369,19 +405,21 @@ function pickFrontendService(services) {
|
|
|
369
405
|
* is the picked frontend service's serviceName, sanitized via
|
|
370
406
|
* `sanitizeServiceName`)
|
|
371
407
|
* 2. Global override: `SKYRAMP_TEST_BASE_URL` (single service or all
|
|
372
|
-
* services share one URL)
|
|
408
|
+
* services share one URL). Also honored when the workspace declares no
|
|
409
|
+
* services at all — the action can know the URL without a workspace entry.
|
|
373
410
|
* 3. Static workspace.yml value: `api.baseUrl` from the picked frontend
|
|
374
411
|
* service
|
|
375
412
|
*
|
|
376
|
-
* Returns null when
|
|
413
|
+
* Returns null when neither the environment nor any service has a usable URL.
|
|
377
414
|
*
|
|
378
415
|
* Exported (vs private) so analyze_changes and future tools can reuse the
|
|
379
416
|
* same selection without re-implementing it.
|
|
380
417
|
*/
|
|
381
418
|
export async function pickFrontendBaseUrl(repositoryPath) {
|
|
382
419
|
const config = await readWorkspaceConfigRaw(repositoryPath);
|
|
420
|
+
const global = process.env.SKYRAMP_TEST_BASE_URL?.trim();
|
|
383
421
|
if (!config?.services || !Array.isArray(config.services))
|
|
384
|
-
return null;
|
|
422
|
+
return global || null;
|
|
385
423
|
const picked = pickFrontendService(config.services);
|
|
386
424
|
// 1. Per-service override (multi-service, distinct URLs).
|
|
387
425
|
// readWorkspaceConfigRaw may fall back to unvalidated YAML, so serviceName
|
|
@@ -392,7 +430,6 @@ export async function pickFrontendBaseUrl(repositoryPath) {
|
|
|
392
430
|
return perSvc;
|
|
393
431
|
}
|
|
394
432
|
// 2. Global override (single service / all services share a URL).
|
|
395
|
-
const global = process.env.SKYRAMP_TEST_BASE_URL?.trim();
|
|
396
433
|
if (global)
|
|
397
434
|
return global;
|
|
398
435
|
// 3. Static workspace.yml value.
|
|
@@ -400,31 +437,38 @@ export async function pickFrontendBaseUrl(repositoryPath) {
|
|
|
400
437
|
}
|
|
401
438
|
// ── Composer ────────────────────────────────────────────────────────────────
|
|
402
439
|
/**
|
|
403
|
-
* Run the strategy ladder
|
|
404
|
-
* routes (2) → root fallback (3). Each
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
440
|
+
* Run the strategy ladder: framework route grep + import graph (1/1b) →
|
|
441
|
+
* source-grounded routes (2) → Dart GoRouter (2.5) → root fallback (3). Each
|
|
442
|
+
* later strategy is consulted only if the earlier ones produced no candidates.
|
|
443
|
+
*
|
|
444
|
+
* `integration` is the production-importer check for the same files; when given,
|
|
445
|
+
* changed non-route files resolve to the pages that import them.
|
|
446
|
+
*
|
|
447
|
+
* Without a resolvable frontend baseUrl only strategies 1/1b can run; their
|
|
448
|
+
* paths are returned with `baseUrlResolved: false` for the agent to prefix.
|
|
449
|
+
* Returns empty only when nothing resolves at all.
|
|
408
450
|
*/
|
|
409
|
-
export async function enumerateCandidateUiPages(repositoryPath, frontendFiles) {
|
|
451
|
+
export async function enumerateCandidateUiPages(repositoryPath, frontendFiles, integration) {
|
|
410
452
|
if (frontendFiles.length === 0)
|
|
411
453
|
return [];
|
|
412
454
|
const baseUrl = await pickFrontendBaseUrl(repositoryPath);
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
455
|
+
const routePaths = collectRoutePaths(frontendFiles, integration, repositoryPath);
|
|
456
|
+
if (!baseUrl) {
|
|
457
|
+
return routePaths.map((c) => ({
|
|
458
|
+
url: c.path,
|
|
459
|
+
sourcedFrom: c.sourcedFrom,
|
|
460
|
+
strategy: c.strategy,
|
|
461
|
+
baseUrlResolved: false,
|
|
462
|
+
...(c.via ? { via: c.via } : {}),
|
|
463
|
+
}));
|
|
464
|
+
}
|
|
465
|
+
if (routePaths.length > 0)
|
|
466
|
+
return routePaths.map((c) => joinBaseUrl(baseUrl, c));
|
|
422
467
|
const fromSource = findCandidatePagesBySourceRoute(frontendFiles, baseUrl, repositoryPath);
|
|
423
468
|
if (fromSource.length > 0)
|
|
424
469
|
return fromSource;
|
|
425
470
|
const fromDart = findCandidatePagesByDartRoute(repositoryPath, baseUrl, frontendFiles);
|
|
426
471
|
if (fromDart.length > 0)
|
|
427
472
|
return fromDart;
|
|
428
|
-
|
|
429
|
-
return fallback ? [fallback] : [];
|
|
473
|
+
return [rootFallbackPage(baseUrl)];
|
|
430
474
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { UtilsLanguageSpec } from "./language-spec.js";
|
|
2
|
+
export type UtilsViolationKind = "duplicate-helper" | "body-assertion" | "scenario-name";
|
|
3
|
+
export declare const UTILS_VIOLATION_KINDS: readonly UtilsViolationKind[];
|
|
4
|
+
export interface UtilsAllow {
|
|
5
|
+
kind: UtilsViolationKind;
|
|
6
|
+
helper: string;
|
|
7
|
+
reason: string;
|
|
8
|
+
}
|
|
9
|
+
/** The exact line to paste for a violation — what the failure text hands the agent. */
|
|
10
|
+
export declare function allowMarkerLine(spec: UtilsLanguageSpec, kind: UtilsViolationKind, helper: string): string;
|
|
11
|
+
export declare function parseUtilsAllows(content: string, spec: UtilsLanguageSpec): {
|
|
12
|
+
allows: UtilsAllow[];
|
|
13
|
+
/** Marker lines the loose pattern recognises but the grammar cannot read — a decline
|
|
14
|
+
* the gate would otherwise drop silently. Reported back so the agent fixes the line. */
|
|
15
|
+
malformed: string[];
|
|
16
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { blankStrings } from "./parse.js";
|
|
2
|
+
export const UTILS_VIOLATION_KINDS = [
|
|
3
|
+
"duplicate-helper",
|
|
4
|
+
"body-assertion",
|
|
5
|
+
"scenario-name",
|
|
6
|
+
];
|
|
7
|
+
/**
|
|
8
|
+
* The documented-decline marker for a utils-file invariant, written in the utils file:
|
|
9
|
+
*
|
|
10
|
+
* # reuse-verify: allow <kind> <helper> — <reason> (Python)
|
|
11
|
+
* // reuse-verify: allow <kind> <helper> — <reason> (TS/JS)
|
|
12
|
+
*
|
|
13
|
+
* ONE grammar, used by both the gate and the report. The POM path's `// kept inline:`
|
|
14
|
+
* marker had two — the gate's regex and the report parser disagreed on the separator
|
|
15
|
+
* and on the file extension — and the gap was a run where the gate blocked execution
|
|
16
|
+
* on declines the report listed correctly (mcp#716, run 30965653402). Here the gate
|
|
17
|
+
* and the report call this one function.
|
|
18
|
+
*
|
|
19
|
+
* The separator is an em/en dash or a hyphen with whitespace on BOTH sides, so a
|
|
20
|
+
* hyphen inside a helper name cannot be mistaken for it. `(?!<reason)` rejects the
|
|
21
|
+
* paste-ready line from the failure text when it was pasted unedited: publishing the
|
|
22
|
+
* placeholder as the stated reason is worse than no marker.
|
|
23
|
+
*/
|
|
24
|
+
const ALLOW_RE = /reuse-verify:\s*allow\s+(duplicate-helper|body-assertion|scenario-name)\s+([A-Za-z_$][\w$]*)\s*(?:[—–]+|\s+-+\s+)\s*(?!<reason)(\S.*?)\s*$/;
|
|
25
|
+
const ALLOW_LOOSE_RE = /reuse-verify:\s*allow\b/;
|
|
26
|
+
/** The exact line to paste for a violation — what the failure text hands the agent. */
|
|
27
|
+
export function allowMarkerLine(spec, kind, helper) {
|
|
28
|
+
return `${spec.commentPrefix} reuse-verify: allow ${kind} ${helper} — <reason this helper must stay as it is>`;
|
|
29
|
+
}
|
|
30
|
+
export function parseUtilsAllows(content, spec) {
|
|
31
|
+
const allows = [];
|
|
32
|
+
const malformed = [];
|
|
33
|
+
// String contents are blanked first, so a marker (or a comment token) quoted in
|
|
34
|
+
// code can neither be read as a decline nor make one look like a comment.
|
|
35
|
+
const rawLines = content.split("\n");
|
|
36
|
+
const codeLines = blankStrings(content, spec).split("\n");
|
|
37
|
+
for (let n = 0; n < rawLines.length; n++) {
|
|
38
|
+
const raw = rawLines[n];
|
|
39
|
+
const code = codeLines[n] ?? "";
|
|
40
|
+
// The marker may sit in a trailing comment, a block comment or a JSDoc line — any
|
|
41
|
+
// comment form. Match from the marker itself, so the comment syntax around it does
|
|
42
|
+
// not decide whether a decline is read.
|
|
43
|
+
const at = code.search(ALLOW_LOOSE_RE);
|
|
44
|
+
if (at === -1)
|
|
45
|
+
continue;
|
|
46
|
+
const before = code.slice(0, at);
|
|
47
|
+
const inComment = spec.commentPrefix === "#"
|
|
48
|
+
? /#/.test(before)
|
|
49
|
+
: /\/\/|\/\*|^\s*\*/.test(before);
|
|
50
|
+
if (!inComment)
|
|
51
|
+
continue;
|
|
52
|
+
const line = raw
|
|
53
|
+
.slice(at)
|
|
54
|
+
.replace(/\s*\*\/\s*$/, "")
|
|
55
|
+
.trim();
|
|
56
|
+
const m = ALLOW_RE.exec(line);
|
|
57
|
+
if (!m) {
|
|
58
|
+
malformed.push(raw.trim());
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
allows.push({
|
|
62
|
+
kind: m[1],
|
|
63
|
+
helper: m[2],
|
|
64
|
+
reason: m[3],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return { allows, malformed };
|
|
68
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type UtilsHelper } from "./parse.js";
|
|
2
|
+
import { type UtilsLanguageSpec } from "./language-spec.js";
|
|
3
|
+
/** The codegen marker every Skyramp-GENERATED TEST carries on line 1 (`Generated by
|
|
4
|
+
* Skyramp v<version>`), as opposed to the utils header (`Generated by Skyramp on …`).
|
|
5
|
+
* The same discriminator STEP 4 of the reuse prompt hands the agent. */
|
|
6
|
+
export declare const CODEGEN_MARKER_RE: RegExp;
|
|
7
|
+
export interface InlineCallSite {
|
|
8
|
+
/** Absolute path of the sibling test. */
|
|
9
|
+
file: string;
|
|
10
|
+
/** 1-based line of the request call. */
|
|
11
|
+
line: number;
|
|
12
|
+
method: string;
|
|
13
|
+
path: string;
|
|
14
|
+
/** The utils helper that wraps the same method+path. */
|
|
15
|
+
helper: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Inline request calls in OTHER Skyramp-generated tests beside `testFile` that a
|
|
19
|
+
* utils helper already wraps — the reuse the prompt asks for and the agent does not
|
|
20
|
+
* perform (measured 0/2 seeded eval runs with the rule as its own mandatory step).
|
|
21
|
+
*
|
|
22
|
+
* A call is INLINE when it sits in a test function, not in a module-level helper of
|
|
23
|
+
* the sibling's own: a sibling that defines `create_order` locally is STEP 4b's
|
|
24
|
+
* helper-vs-helper case, not a call site. Equivalence is `c3b9157b`'s: same method
|
|
25
|
+
* and same normalised path, nothing else — literals are what the call's arguments
|
|
26
|
+
* are for.
|
|
27
|
+
*
|
|
28
|
+
* Advisory by design: it names a change to a PRE-EXISTING test, which the maintenance
|
|
29
|
+
* flow owns, so it informs rather than gates. Never throws.
|
|
30
|
+
*/
|
|
31
|
+
export declare function findSiblingInlineCallSites(testFile: string, utilsFiles: string[], _helpers: UtilsHelper[], spec: UtilsLanguageSpec): Promise<InlineCallSite[]>;
|
|
32
|
+
/** The keyword names of a request call (`path=`, `method=`, `query_params=` … / TS
|
|
33
|
+
* object keys), less the liftable ones — a cheap normalised call shape. */
|
|
34
|
+
export declare function shapeKeys(call: string): Set<string>;
|