@skyramp/mcp 0.3.6 → 0.3.8
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/prompts/code-reuse.js +17 -2
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +1 -1
- package/build/prompts/modularization/ui-test-modularization.js +9 -6
- package/build/prompts/pom-aware-code-reuse.js +1 -1
- package/build/prompts/shared-helper-policy.js +5 -5
- package/build/prompts/test-recommendation/diffExecutionPlan.js +40 -9
- package/build/prompts/test-recommendation/fullRepoCatalog.js +3 -2
- package/build/prompts/test-recommendation/recommendationSections.js +3 -3
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +2 -2
- package/build/prompts/test-recommendation/scopeAssessment.js +58 -76
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +14 -1
- package/build/prompts/testbot/testbot-prompts.js +25 -8
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +39 -5
- package/build/recommendation/budgeters/shared.d.ts +21 -8
- package/build/recommendation/budgeters/shared.js +134 -56
- package/build/recommendation/planRanker.d.ts +5 -3
- package/build/recommendation/planRanker.js +1 -1
- package/build/services/TestGenerationService.js +15 -1
- package/build/tools/code-refactor/assertion-state.d.ts +91 -0
- package/build/tools/code-refactor/assertion-state.js +375 -0
- package/build/tools/code-refactor/codeReuseTool.js +6 -4
- package/build/tools/code-refactor/enhanceAssertionsTool.js +73 -18
- package/build/tools/code-refactor/retrofit-state.d.ts +53 -0
- package/build/tools/code-refactor/retrofit-state.js +162 -0
- package/build/tools/code-refactor/reuse-outcome.d.ts +7 -0
- package/build/tools/code-refactor/reuse-state.d.ts +9 -0
- package/build/tools/code-refactor/reuse-state.js +42 -4
- package/build/tools/code-refactor/utils-verify-gates.js +69 -15
- package/build/tools/executeSkyrampTestTool.js +19 -14
- package/build/tools/submitReportTool.js +87 -9
- package/build/tools/test-management/registerTestPlanTool.d.ts +3 -3
- package/build/tools/test-management/registerTestPlanTool.js +29 -8
- package/build/types/AssertionOutcome.d.ts +68 -0
- package/build/types/AssertionOutcome.js +1 -0
- package/build/types/RepositoryAnalysis.d.ts +10 -10
- package/build/types/ReuseOutcome.d.ts +16 -0
- package/build/types/TestRecommendation.d.ts +11 -1
- package/build/types/TestRecommendation.js +34 -0
- package/build/types/TestTypes.d.ts +4 -0
- package/build/types/TestTypes.js +8 -0
- package/build/types/TestbotReport.d.ts +13 -0
- package/build/types/index.d.ts +1 -1
- package/build/utils/AnalysisStateManager.d.ts +20 -7
- package/build/utils/assertion-verify/api-shared-lints.d.ts +5 -0
- package/build/utils/assertion-verify/api-shared-lints.js +315 -0
- package/build/utils/assertion-verify/contract-lints.d.ts +3 -0
- package/build/utils/assertion-verify/contract-lints.js +87 -0
- package/build/utils/assertion-verify/format.d.ts +5 -0
- package/build/utils/assertion-verify/format.js +65 -0
- package/build/utils/assertion-verify/helper-imports.d.ts +6 -0
- package/build/utils/assertion-verify/helper-imports.js +178 -0
- package/build/utils/assertion-verify/index.d.ts +3 -0
- package/build/utils/assertion-verify/index.js +7 -0
- package/build/utils/assertion-verify/integration-lints.d.ts +3 -0
- package/build/utils/assertion-verify/integration-lints.js +36 -0
- package/build/utils/assertion-verify/js-regex-blank.d.ts +1 -0
- package/build/utils/assertion-verify/js-regex-blank.js +153 -0
- package/build/utils/assertion-verify/lint-types.d.ts +33 -0
- package/build/utils/assertion-verify/lint-types.js +57 -0
- package/build/utils/assertion-verify/marker.d.ts +27 -0
- package/build/utils/assertion-verify/marker.js +61 -0
- package/build/utils/assertion-verify/metrics.d.ts +30 -0
- package/build/utils/assertion-verify/metrics.js +341 -0
- package/build/utils/assertion-verify/python-strip.d.ts +6 -0
- package/build/utils/assertion-verify/python-strip.js +75 -0
- package/build/utils/assertion-verify/strip-dispatch.d.ts +19 -0
- package/build/utils/assertion-verify/strip-dispatch.js +42 -0
- package/build/utils/assertion-verify/ui-lints.d.ts +8 -0
- package/build/utils/assertion-verify/ui-lints.js +244 -0
- package/build/utils/assertion-verify/verify.d.ts +61 -0
- package/build/utils/assertion-verify/verify.js +215 -0
- package/build/utils/featureFlags.d.ts +12 -2
- package/build/utils/featureFlags.js +33 -3
- package/build/utils/pathSignatures.d.ts +4 -1
- package/build/utils/pathSignatures.js +14 -2
- package/build/utils/planOnlyMode.d.ts +33 -0
- package/build/utils/planOnlyMode.js +40 -0
- package/build/utils/reportVerification.d.ts +9 -0
- package/build/utils/reportVerification.js +39 -6
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +1 -1
- package/build/utils/utils-verify/allow.d.ts +22 -4
- package/build/utils/utils-verify/allow.js +8 -2
- package/build/utils/utils-verify/call-sites.d.ts +40 -1
- package/build/utils/utils-verify/call-sites.js +196 -30
- package/build/utils/utils-verify/importers.d.ts +31 -0
- package/build/utils/utils-verify/importers.js +78 -0
- package/build/utils/utils-verify/index.d.ts +1 -0
- package/build/utils/utils-verify/index.js +1 -0
- package/build/utils/utils-verify/language-spec.d.ts +13 -2
- package/build/utils/utils-verify/language-spec.js +12 -2
- package/build/utils/utils-verify/parse.d.ts +31 -3
- package/build/utils/utils-verify/parse.js +190 -9
- package/build/utils/utils-verify/retrofit-equivalence.d.ts +43 -0
- package/build/utils/utils-verify/retrofit-equivalence.js +218 -0
- package/build/utils/utils-verify/stage.d.ts +6 -0
- package/build/utils/utils-verify/stage.js +12 -2
- package/build/utils/utils-verify/verify.d.ts +54 -4
- package/build/utils/utils-verify/verify.js +224 -12
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-CZfp96qZ.js → codeMirrorModule-LNgEKtdV.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-gpLo02E0.js → defaultSettingsView-Bwr1eMKC.js} +135 -135
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Bq1r1URj.js → index.-Id052Lr.js} +1 -1
- 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 → uiMode.BPopbasy.js} +1 -1
- 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/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/build/tools/code-refactor/enhance-state.d.ts +0 -49
- package/build/tools/code-refactor/enhance-state.js +0 -109
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-only mode capture for `skyramp_submit_report` (SKYR-4250).
|
|
3
|
+
*
|
|
4
|
+
* A plan-only eval run (SKYR-3879) writes no test files: `newTestsCreated`
|
|
5
|
+
* DECLARES the approved GENERATE selection instead of recording generated
|
|
6
|
+
* files, and the declaration itself is the deliverable. The SKYR-3883 guard
|
|
7
|
+
* (`findUnchangedFileClaims`) verifies every `newTestsCreated` claim against
|
|
8
|
+
* the working tree, so in that lane EVERY declaration is unbacked by
|
|
9
|
+
* construction and the report is rejected.
|
|
10
|
+
*
|
|
11
|
+
* That rejection is not evenly distributed. `findUnchangedFileClaims` exempts a
|
|
12
|
+
* claim attributed to a non-primary repo, so on a cross-repo fixture the
|
|
13
|
+
* related repo's declarations pass and only the primary repo's are checked.
|
|
14
|
+
* On a frontend-primary pair that makes the mandatory UI item (SKYR-4204's
|
|
15
|
+
* floor) the one claim that always fails, and the agent's only compliant move
|
|
16
|
+
* is to demote it to `additionalRecommendations` — which is exactly the
|
|
17
|
+
* under-declaration SKYR-4250 exists to fix (run 32903633247).
|
|
18
|
+
*
|
|
19
|
+
* `planOnly` reaches the server only as a testbot prompt/resource arg, so this
|
|
20
|
+
* mirrors the SKYR-4185 report-language capture: the prompt render is the
|
|
21
|
+
* single capture point, and the report tool reads the captured mode later in
|
|
22
|
+
* the same process.
|
|
23
|
+
*
|
|
24
|
+
* Capture is fail-open by design: if the testbot prompt was never served, the
|
|
25
|
+
* mode stays false and the guard behaves exactly as it does today.
|
|
26
|
+
*/
|
|
27
|
+
/** Capture plan-only mode at prompt-serve time. Last render wins: a later
|
|
28
|
+
* non-plan-only render disarms a mode captured earlier in a long-lived server
|
|
29
|
+
* process, so an ordinary run never silently skips the guard. */
|
|
30
|
+
export declare function setPlanOnlyMode(planOnly: boolean): void;
|
|
31
|
+
export declare function isPlanOnlyMode(): boolean;
|
|
32
|
+
/** Test isolation only — module state persists across tests in one process. */
|
|
33
|
+
export declare function resetPlanOnlyMode(): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-only mode capture for `skyramp_submit_report` (SKYR-4250).
|
|
3
|
+
*
|
|
4
|
+
* A plan-only eval run (SKYR-3879) writes no test files: `newTestsCreated`
|
|
5
|
+
* DECLARES the approved GENERATE selection instead of recording generated
|
|
6
|
+
* files, and the declaration itself is the deliverable. The SKYR-3883 guard
|
|
7
|
+
* (`findUnchangedFileClaims`) verifies every `newTestsCreated` claim against
|
|
8
|
+
* the working tree, so in that lane EVERY declaration is unbacked by
|
|
9
|
+
* construction and the report is rejected.
|
|
10
|
+
*
|
|
11
|
+
* That rejection is not evenly distributed. `findUnchangedFileClaims` exempts a
|
|
12
|
+
* claim attributed to a non-primary repo, so on a cross-repo fixture the
|
|
13
|
+
* related repo's declarations pass and only the primary repo's are checked.
|
|
14
|
+
* On a frontend-primary pair that makes the mandatory UI item (SKYR-4204's
|
|
15
|
+
* floor) the one claim that always fails, and the agent's only compliant move
|
|
16
|
+
* is to demote it to `additionalRecommendations` — which is exactly the
|
|
17
|
+
* under-declaration SKYR-4250 exists to fix (run 32903633247).
|
|
18
|
+
*
|
|
19
|
+
* `planOnly` reaches the server only as a testbot prompt/resource arg, so this
|
|
20
|
+
* mirrors the SKYR-4185 report-language capture: the prompt render is the
|
|
21
|
+
* single capture point, and the report tool reads the captured mode later in
|
|
22
|
+
* the same process.
|
|
23
|
+
*
|
|
24
|
+
* Capture is fail-open by design: if the testbot prompt was never served, the
|
|
25
|
+
* mode stays false and the guard behaves exactly as it does today.
|
|
26
|
+
*/
|
|
27
|
+
let sessionPlanOnly = false;
|
|
28
|
+
/** Capture plan-only mode at prompt-serve time. Last render wins: a later
|
|
29
|
+
* non-plan-only render disarms a mode captured earlier in a long-lived server
|
|
30
|
+
* process, so an ordinary run never silently skips the guard. */
|
|
31
|
+
export function setPlanOnlyMode(planOnly) {
|
|
32
|
+
sessionPlanOnly = planOnly;
|
|
33
|
+
}
|
|
34
|
+
export function isPlanOnlyMode() {
|
|
35
|
+
return sessionPlanOnly;
|
|
36
|
+
}
|
|
37
|
+
/** Test isolation only — module state persists across tests in one process. */
|
|
38
|
+
export function resetPlanOnlyMode() {
|
|
39
|
+
sessionPlanOnly = false;
|
|
40
|
+
}
|
|
@@ -24,6 +24,10 @@ import { DriftAction } from "../types/TestAnalysis.js";
|
|
|
24
24
|
* nothing rather than failing the whole check.
|
|
25
25
|
*/
|
|
26
26
|
export declare function listChangedFilesAcross(roots: Array<string | undefined>): Promise<string[]>;
|
|
27
|
+
/** Like listChangedFilesAcross, but ABSOLUTE: each changed path resolved against
|
|
28
|
+
* the root it came from, so a consumer never has to guess which checkout a
|
|
29
|
+
* repo-relative path belongs to. */
|
|
30
|
+
export declare function listChangedFilesAbs(roots: Array<string | undefined>): Promise<string[]>;
|
|
27
31
|
export declare function listChangedFiles(repoRoot: string): Promise<string[]>;
|
|
28
32
|
export interface UnchangedFileClaimsInput {
|
|
29
33
|
/** Absolute path of the primary repo checkout the report pertains to. */
|
|
@@ -46,6 +50,11 @@ export interface UnchangedFileClaimsInput {
|
|
|
46
50
|
/** owner/repo of the primary repo — used to exempt cross-repo newTest claims
|
|
47
51
|
* whose files live in a different checkout. */
|
|
48
52
|
primaryRepository?: string;
|
|
53
|
+
/** SKYR-4250: a plan-only eval run generates nothing — `newTestsCreated`
|
|
54
|
+
* declares the approved GENERATE list — so no declaration can be backed by
|
|
55
|
+
* a working-tree change. Exempts the declaration half only; a maintenance
|
|
56
|
+
* UPDATE still claims a real edit and is still verified. */
|
|
57
|
+
planOnly?: boolean;
|
|
49
58
|
}
|
|
50
59
|
/**
|
|
51
60
|
* Cross-check report claims against files that actually changed in the working
|
|
@@ -51,6 +51,26 @@ export async function listChangedFilesAcross(roots) {
|
|
|
51
51
|
}
|
|
52
52
|
return files;
|
|
53
53
|
}
|
|
54
|
+
/** Like listChangedFilesAcross, but ABSOLUTE: each changed path resolved against
|
|
55
|
+
* the root it came from, so a consumer never has to guess which checkout a
|
|
56
|
+
* repo-relative path belongs to. */
|
|
57
|
+
export async function listChangedFilesAbs(roots) {
|
|
58
|
+
const seenRoots = new Set();
|
|
59
|
+
const files = [];
|
|
60
|
+
for (const root of roots) {
|
|
61
|
+
if (!root || seenRoots.has(root))
|
|
62
|
+
continue;
|
|
63
|
+
seenRoots.add(root);
|
|
64
|
+
try {
|
|
65
|
+
for (const f of await listChangedFiles(root))
|
|
66
|
+
files.push(path.resolve(root, f));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Non-git or missing tree — nothing to contribute.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
54
74
|
export async function listChangedFiles(repoRoot) {
|
|
55
75
|
const { stdout } = await execFileAsync("git", ["status", "--porcelain", "-z", "--untracked-files=all"], { cwd: repoRoot });
|
|
56
76
|
const files = [];
|
|
@@ -104,7 +124,7 @@ export async function listChangedFiles(repoRoot) {
|
|
|
104
124
|
* absolute path resolving outside `repoRoot`) — a different checkout owns them.
|
|
105
125
|
*/
|
|
106
126
|
export function findUnchangedFileClaims(input) {
|
|
107
|
-
const { repoRoot, changedFiles, newTests, verdicts, primaryRepository } = input;
|
|
127
|
+
const { repoRoot, changedFiles, newTests, verdicts, primaryRepository, planOnly, } = input;
|
|
108
128
|
const unbacked = [];
|
|
109
129
|
const isBacked = (claimedPath) => {
|
|
110
130
|
let candidate = claimedPath;
|
|
@@ -117,7 +137,12 @@ export function findUnchangedFileClaims(input) {
|
|
|
117
137
|
}
|
|
118
138
|
return changedFiles.some((c) => testFileMatches(c, candidate));
|
|
119
139
|
};
|
|
120
|
-
|
|
140
|
+
// A plan-only run declares its GENERATE list instead of writing files, so
|
|
141
|
+
// skip the claim scan wholesale rather than per-claim: checking it rejects
|
|
142
|
+
// every declaration the guard can see, and the ones it CANNOT see (cross-repo
|
|
143
|
+
// claims, exempted below) would pass — demoting the primary repo's items
|
|
144
|
+
// alone. Uneven enforcement is worse than none here.
|
|
145
|
+
for (const t of planOnly ? [] : newTests) {
|
|
121
146
|
if (!t.fileName)
|
|
122
147
|
continue;
|
|
123
148
|
// Cross-repo finding — its file lives in a different checkout, not this tree.
|
|
@@ -273,7 +298,9 @@ export async function findInvalidSourceCitations(input) {
|
|
|
273
298
|
continue; // Could not look — accept.
|
|
274
299
|
const suggestions = await suggestSimilarPaths(roots, cited);
|
|
275
300
|
invalid.push(`issuesFound[${i}] cites ${cited} but that file does not exist in the repository` +
|
|
276
|
-
(suggestions.length > 0
|
|
301
|
+
(suggestions.length > 0
|
|
302
|
+
? ` (closest matches: ${suggestions.join(", ")})`
|
|
303
|
+
: "") +
|
|
277
304
|
`. ${CITATION_REMEDY}`);
|
|
278
305
|
continue;
|
|
279
306
|
}
|
|
@@ -322,7 +349,9 @@ export async function findInvalidSourceCitations(input) {
|
|
|
322
349
|
* A checkout the run could not name contributes nothing: it can be neither
|
|
323
350
|
* stamped nor offered as a candidate. */
|
|
324
351
|
function namesOf(resolved) {
|
|
325
|
-
return [
|
|
352
|
+
return [
|
|
353
|
+
...new Set(resolved.map((c) => c.repository).filter((r) => !!r)),
|
|
354
|
+
];
|
|
326
355
|
}
|
|
327
356
|
/**
|
|
328
357
|
* Reconcile where a citation resolved with what the entry says about it, giving
|
|
@@ -339,7 +368,9 @@ function attributeCitation(args) {
|
|
|
339
368
|
// than sent back for a rewrite that would change nothing.
|
|
340
369
|
const match = resolved.find((c) => c.repository && c.repository.toLowerCase() === declared.toLowerCase());
|
|
341
370
|
if (match)
|
|
342
|
-
return match.repository === declared
|
|
371
|
+
return match.repository === declared
|
|
372
|
+
? {}
|
|
373
|
+
: { repository: match.repository };
|
|
343
374
|
// It resolved somewhere else. Only say so when the run can name where —
|
|
344
375
|
// otherwise there is nothing to correct the attribution to.
|
|
345
376
|
if (names.length === 0)
|
|
@@ -362,5 +393,7 @@ function attributeCitation(args) {
|
|
|
362
393
|
// Exactly one checkout. Stamp it unless it is the primary, whose findings are
|
|
363
394
|
// identified downstream by having NO repository — see SourceCitationCheck.
|
|
364
395
|
const [only] = resolved;
|
|
365
|
-
return only.primary || !only.repository
|
|
396
|
+
return only.primary || !only.repository
|
|
397
|
+
? {}
|
|
398
|
+
: { repository: only.repository };
|
|
366
399
|
}
|
|
@@ -46,7 +46,7 @@ export declare function draftScenariosFromEndpoints(endpoints: Array<EndpointInp
|
|
|
46
46
|
/**
|
|
47
47
|
* Enforce a global cap on drafted scenarios while preserving category diversity.
|
|
48
48
|
*
|
|
49
|
-
* 1. CRITICAL (bug_caught) scenarios prioritized first.
|
|
49
|
+
* 1. CRITICAL (bug_caught, requirement_conflict) scenarios prioritized first.
|
|
50
50
|
* 2. One scenario per non-empty category guaranteed (breadth).
|
|
51
51
|
* 3. Remaining budget filled by priority tier (HIGH > MEDIUM > LOW).
|
|
52
52
|
* 4. Hard cap at MAX_TOTAL_SCENARIOS — applied to the combined output.
|
|
@@ -226,7 +226,7 @@ const TIER_ORDER = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
|
|
|
226
226
|
/**
|
|
227
227
|
* Enforce a global cap on drafted scenarios while preserving category diversity.
|
|
228
228
|
*
|
|
229
|
-
* 1. CRITICAL (bug_caught) scenarios prioritized first.
|
|
229
|
+
* 1. CRITICAL (bug_caught, requirement_conflict) scenarios prioritized first.
|
|
230
230
|
* 2. One scenario per non-empty category guaranteed (breadth).
|
|
231
231
|
* 3. Remaining budget filled by priority tier (HIGH > MEDIUM > LOW).
|
|
232
232
|
* 4. Hard cap at MAX_TOTAL_SCENARIOS — applied to the combined output.
|
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import type { UtilsLanguageSpec } from "./language-spec.js";
|
|
2
|
-
export type UtilsViolationKind = "duplicate-helper" | "body-assertion" | "scenario-name"
|
|
3
|
-
|
|
2
|
+
export type UtilsViolationKind = "duplicate-helper" | "body-assertion" | "scenario-name"
|
|
3
|
+
/** The delivered spec (plus the helpers it imports) holds fewer assertions than at
|
|
4
|
+
* the modularization hand-out. Marker-eligible: a deliberate consolidation is
|
|
5
|
+
* legitimate once documented. */
|
|
6
|
+
| "assertion-loss"
|
|
7
|
+
/** A page-error/console guard present at hand-out is gone. Not marker-eligible. */
|
|
8
|
+
| "guard-removed"
|
|
9
|
+
/** Python only: the spec imports the shared module in a form that cannot resolve
|
|
10
|
+
* in the customer's repo. Not marker-eligible — a broken import has no reason to
|
|
11
|
+
* stay. */
|
|
12
|
+
| "import-form";
|
|
13
|
+
/** Kinds that only ever advise. Kept OUT of UtilsViolationKind so that type keeps
|
|
14
|
+
* meaning "a kind the allow grammar and the gate know about" — an advisory can
|
|
15
|
+
* never fail a verify and never needs (or accepts) an allow marker. */
|
|
16
|
+
export type UtilsAdvisoryKind = "untyped-parameter";
|
|
17
|
+
export declare const MARKERABLE_UTILS_KINDS: readonly ["duplicate-helper", "body-assertion", "scenario-name", "assertion-loss"];
|
|
18
|
+
/** A kind the allow grammar accepts. `guard-removed` is a gate kind but never
|
|
19
|
+
* markerable — a guard is restored, not documented away — so the grammar itself
|
|
20
|
+
* cannot spell it; the regex below is built from this list. */
|
|
21
|
+
export type MarkerableUtilsKind = (typeof MARKERABLE_UTILS_KINDS)[number];
|
|
4
22
|
export interface UtilsAllow {
|
|
5
|
-
kind:
|
|
23
|
+
kind: MarkerableUtilsKind;
|
|
6
24
|
helper: string;
|
|
7
25
|
reason: string;
|
|
8
26
|
}
|
|
9
27
|
/** The exact line to paste for a violation — what the failure text hands the agent. */
|
|
10
|
-
export declare function allowMarkerLine(spec: UtilsLanguageSpec, kind:
|
|
28
|
+
export declare function allowMarkerLine(spec: UtilsLanguageSpec, kind: MarkerableUtilsKind, helper: string): string;
|
|
11
29
|
export declare function parseUtilsAllows(content: string, spec: UtilsLanguageSpec): {
|
|
12
30
|
allows: UtilsAllow[];
|
|
13
31
|
/** Marker lines the loose pattern recognises but the grammar cannot read — a decline
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { blankStrings } from "./parse.js";
|
|
2
|
-
export const
|
|
2
|
+
export const MARKERABLE_UTILS_KINDS = [
|
|
3
3
|
"duplicate-helper",
|
|
4
4
|
"body-assertion",
|
|
5
5
|
"scenario-name",
|
|
6
|
+
"assertion-loss",
|
|
6
7
|
];
|
|
7
8
|
/**
|
|
8
9
|
* The documented-decline marker for a utils-file invariant, written in the utils file:
|
|
@@ -21,7 +22,7 @@ export const UTILS_VIOLATION_KINDS = [
|
|
|
21
22
|
* paste-ready line from the failure text when it was pasted unedited: publishing the
|
|
22
23
|
* placeholder as the stated reason is worse than no marker.
|
|
23
24
|
*/
|
|
24
|
-
const ALLOW_RE =
|
|
25
|
+
const ALLOW_RE = new RegExp(`reuse-verify:\\s*allow\\s+(${MARKERABLE_UTILS_KINDS.join("|")})\\s+([A-Za-z_$][\\w$]*)\\s*(?:[—–]+|\\s+-+\\s+)\\s*(?!<reason)(\\S.*?)\\s*$`);
|
|
25
26
|
const ALLOW_LOOSE_RE = /reuse-verify:\s*allow\b/;
|
|
26
27
|
/** The exact line to paste for a violation — what the failure text hands the agent. */
|
|
27
28
|
export function allowMarkerLine(spec, kind, helper) {
|
|
@@ -55,6 +56,11 @@ export function parseUtilsAllows(content, spec) {
|
|
|
55
56
|
.trim();
|
|
56
57
|
const m = ALLOW_RE.exec(line);
|
|
57
58
|
if (!m) {
|
|
59
|
+
// A marker naming an advisory kind is inert, not malformed: advisories never
|
|
60
|
+
// block, so "documenting" one must not fail the gate either (the agent sees
|
|
61
|
+
// the kind in a kind-shaped slot and reasonably tries).
|
|
62
|
+
if (/allow\s+untyped-parameter\b/.test(code))
|
|
63
|
+
continue;
|
|
58
64
|
malformed.push(raw.trim());
|
|
59
65
|
continue;
|
|
60
66
|
}
|
|
@@ -4,6 +4,23 @@ import { type UtilsLanguageSpec } from "./language-spec.js";
|
|
|
4
4
|
* Skyramp v<version>`), as opposed to the utils header (`Generated by Skyramp on …`).
|
|
5
5
|
* The same discriminator STEP 4 of the reuse prompt hands the agent. */
|
|
6
6
|
export declare const CODEGEN_MARKER_RE: RegExp;
|
|
7
|
+
/** One Skyramp-generated test beside the spec, read once. */
|
|
8
|
+
export interface SiblingFile {
|
|
9
|
+
/** Absolute path. */
|
|
10
|
+
file: string;
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The Skyramp-generated tests in the spec's directory, each read once — THE sibling
|
|
15
|
+
* walk, shared by the inline-call-site scan (which excludes the spec itself) and the
|
|
16
|
+
* importer count (which includes it). Utils files carry no codegen version marker,
|
|
17
|
+
* so the marker check alone excludes them. Fails open to [] on any read error.
|
|
18
|
+
*/
|
|
19
|
+
export declare function generatedSiblings(testFile: string, spec: UtilsLanguageSpec, opts: {
|
|
20
|
+
includeSelf: boolean;
|
|
21
|
+
}): Promise<SiblingFile[]>;
|
|
22
|
+
/** An SDK request call. Global: clone with `new RegExp(...)` before `matchAll`. */
|
|
23
|
+
export declare const REQUEST_CALL_RE: RegExp;
|
|
7
24
|
export interface InlineCallSite {
|
|
8
25
|
/** Absolute path of the sibling test. */
|
|
9
26
|
file: string;
|
|
@@ -28,7 +45,29 @@ export interface InlineCallSite {
|
|
|
28
45
|
* Advisory by design: it names a change to a PRE-EXISTING test, which the maintenance
|
|
29
46
|
* flow owns, so it informs rather than gates. Never throws.
|
|
30
47
|
*/
|
|
31
|
-
export declare function findSiblingInlineCallSites(testFile: string, utilsFiles: string[], _helpers: UtilsHelper[], spec: UtilsLanguageSpec): Promise<InlineCallSite[]>;
|
|
48
|
+
export declare function findSiblingInlineCallSites(testFile: string, utilsFiles: string[], _helpers: UtilsHelper[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingFile[]): Promise<InlineCallSite[]>;
|
|
49
|
+
/** Lines after a call scanned for its schema assertion — the generated shape puts it
|
|
50
|
+
* immediately after the status-code assertion. */
|
|
51
|
+
export declare const SCHEMA_SCAN_LINES = 12;
|
|
52
|
+
/** True when a `check_schema` / `checkSchema` assertion sits in the given 1-based line
|
|
53
|
+
* range — the response-shape contract a status-only helper does not carry. */
|
|
54
|
+
export declare function hasSchemaCheck(content: string, from: number, to: number): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* The request's body/override FIELD names — leaf names only, so a dotted override key
|
|
57
|
+
* (`posts.0.publish_at`) and the body literal it targets (`"publish_at":`) are one
|
|
58
|
+
* field. Reads the body through its variable: generated code assigns the template to
|
|
59
|
+
* an identifier and passes `body=<ident>`, so the fields live in the assignment, not
|
|
60
|
+
* in the call.
|
|
61
|
+
*
|
|
62
|
+
* `callAt` is the call's offset in `content`. The body variable resolves to the
|
|
63
|
+
* assignment NEAREST BEFORE it, because generated code reuses the name `body` per
|
|
64
|
+
* request: a file-wide first match gives every later request the first one's fields,
|
|
65
|
+
* so two different payloads compare equal. Omit it only when `content` holds a single
|
|
66
|
+
* request.
|
|
67
|
+
*/
|
|
68
|
+
export declare function requestFields(content: string, call: string, callAt?: number): Set<string>;
|
|
32
69
|
/** The keyword names of a request call (`path=`, `method=`, `query_params=` … / TS
|
|
33
70
|
* object keys), less the liftable ones — a cheap normalised call shape. */
|
|
34
71
|
export declare function shapeKeys(call: string): Set<string>;
|
|
72
|
+
/** Text of the parenthesised argument list starting at the `(` at `open`. */
|
|
73
|
+
export declare function callText(content: string, open: number): string;
|
|
@@ -4,12 +4,52 @@ import { SKYRAMP_UTILS_HEADER } from "../utils.js";
|
|
|
4
4
|
import { parseUtilsFile } from "./parse.js";
|
|
5
5
|
import { normalizePath } from "./language-spec.js";
|
|
6
6
|
import { realpath } from "./locate.js";
|
|
7
|
+
import { escapeRegExp } from "../regex.js";
|
|
7
8
|
/** The codegen marker every Skyramp-GENERATED TEST carries on line 1 (`Generated by
|
|
8
9
|
* Skyramp v<version>`), as opposed to the utils header (`Generated by Skyramp on …`).
|
|
9
10
|
* The same discriminator STEP 4 of the reuse prompt hands the agent. */
|
|
10
11
|
export const CODEGEN_MARKER_RE = new RegExp(`${SKYRAMP_UTILS_HEADER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+v\\d`);
|
|
11
12
|
const HEADER_SCAN_LINES = 5;
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The Skyramp-generated tests in the spec's directory, each read once — THE sibling
|
|
15
|
+
* walk, shared by the inline-call-site scan (which excludes the spec itself) and the
|
|
16
|
+
* importer count (which includes it). Utils files carry no codegen version marker,
|
|
17
|
+
* so the marker check alone excludes them. Fails open to [] on any read error.
|
|
18
|
+
*/
|
|
19
|
+
export async function generatedSiblings(testFile, spec, opts) {
|
|
20
|
+
let dir;
|
|
21
|
+
let entries;
|
|
22
|
+
let self;
|
|
23
|
+
try {
|
|
24
|
+
self = await realpath(testFile);
|
|
25
|
+
dir = path.dirname(self);
|
|
26
|
+
entries = await fs.promises.readdir(dir);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const name of entries.sort()) {
|
|
33
|
+
const file = path.join(dir, name);
|
|
34
|
+
if (!spec.extensions.some((e) => name.toLowerCase().endsWith(e)))
|
|
35
|
+
continue;
|
|
36
|
+
if (!opts.includeSelf && path.resolve(file) === self)
|
|
37
|
+
continue;
|
|
38
|
+
let content;
|
|
39
|
+
try {
|
|
40
|
+
content = await fs.promises.readFile(file, "utf8");
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!CODEGEN_MARKER_RE.test(content.split("\n").slice(0, HEADER_SCAN_LINES).join("\n")))
|
|
46
|
+
continue;
|
|
47
|
+
out.push({ file, content });
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/** An SDK request call. Global: clone with `new RegExp(...)` before `matchAll`. */
|
|
52
|
+
export const REQUEST_CALL_RE = /\b(?:send_request|sendRequest)\s*\(/g;
|
|
13
53
|
/**
|
|
14
54
|
* Inline request calls in OTHER Skyramp-generated tests beside `testFile` that a
|
|
15
55
|
* utils helper already wraps — the reuse the prompt asks for and the agent does not
|
|
@@ -24,10 +64,12 @@ const REQUEST_CALL_RE = /\b(?:send_request|sendRequest)\s*\(/g;
|
|
|
24
64
|
* Advisory by design: it names a change to a PRE-EXISTING test, which the maintenance
|
|
25
65
|
* flow owns, so it informs rather than gates. Never throws.
|
|
26
66
|
*/
|
|
27
|
-
export async function findSiblingInlineCallSites(testFile, utilsFiles, _helpers, spec) {
|
|
67
|
+
export async function findSiblingInlineCallSites(testFile, utilsFiles, _helpers, spec, preReadSiblings) {
|
|
28
68
|
// route → helper name + the keyword set of the helper's own request call, so a
|
|
29
69
|
// sibling call is reported only when its SHAPE matches too (same keywords once the
|
|
30
70
|
// liftable ones are set aside) — criterion 2 of the API near-duplicate definition.
|
|
71
|
+
// `fields` and `schemaChecked` carry the two things keyword shape cannot see; see
|
|
72
|
+
// substitutable().
|
|
31
73
|
const byRoute = new Map();
|
|
32
74
|
for (const utilsFile of utilsFiles) {
|
|
33
75
|
let content;
|
|
@@ -50,39 +92,20 @@ export async function findSiblingInlineCallSites(testFile, utilsFiles, _helpers,
|
|
|
50
92
|
byRoute.set(`${owner.method} ${owner.normalizedPath}`, {
|
|
51
93
|
helper: owner.name,
|
|
52
94
|
keys: shapeKeys(call),
|
|
95
|
+
fields: requestFields(content, call, start),
|
|
96
|
+
schemaChecked: hasSchemaCheck(content, owner.line, owner.endLine),
|
|
53
97
|
});
|
|
54
98
|
}
|
|
55
99
|
}
|
|
56
100
|
if (byRoute.size === 0)
|
|
57
101
|
return [];
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// own sibling.
|
|
63
|
-
const exclude = new Set(await Promise.all([testFile, ...utilsFiles].map((f) => realpath(f))));
|
|
64
|
-
let entries;
|
|
65
|
-
try {
|
|
66
|
-
entries = await fs.promises.readdir(dir);
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
102
|
+
const utilsSet = new Set(await Promise.all(utilsFiles.map((f) => realpath(f))));
|
|
103
|
+
const siblings = (preReadSiblings ??
|
|
104
|
+
(await generatedSiblings(testFile, spec, { includeSelf: false }))).filter((sib) => !utilsSet.has(path.resolve(sib.file)));
|
|
105
|
+
const self = await realpath(testFile);
|
|
71
106
|
const out = [];
|
|
72
|
-
for (const
|
|
73
|
-
|
|
74
|
-
if (exclude.has(path.resolve(file)))
|
|
75
|
-
continue;
|
|
76
|
-
if (!spec.extensions.some((e) => name.toLowerCase().endsWith(e)))
|
|
77
|
-
continue;
|
|
78
|
-
let content;
|
|
79
|
-
try {
|
|
80
|
-
content = await fs.promises.readFile(file, "utf8");
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (!CODEGEN_MARKER_RE.test(content.split("\n").slice(0, HEADER_SCAN_LINES).join("\n")))
|
|
107
|
+
for (const { file, content } of siblings) {
|
|
108
|
+
if (path.resolve(file) === self)
|
|
86
109
|
continue;
|
|
87
110
|
// Line ranges of the sibling's own non-test helpers: calls inside them are not inline.
|
|
88
111
|
const localHelpers = parseUtilsFile(content, spec).filter((h) => !/^test/i.test(h.name));
|
|
@@ -102,11 +125,154 @@ export async function findSiblingInlineCallSites(testFile, utilsFiles, _helpers,
|
|
|
102
125
|
continue;
|
|
103
126
|
if (!sameShape(shapeKeys(call), match.keys))
|
|
104
127
|
continue;
|
|
128
|
+
if (!substitutable(match, {
|
|
129
|
+
fields: requestFields(content, call, start),
|
|
130
|
+
schemaChecked: hasSchemaCheck(content, line, line + SCHEMA_SCAN_LINES),
|
|
131
|
+
}))
|
|
132
|
+
continue;
|
|
105
133
|
out.push({ file, line, method, path: p, helper: match.helper });
|
|
106
134
|
}
|
|
107
135
|
}
|
|
108
136
|
return out;
|
|
109
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Whether the helper can stand in for this call site with LITERALS as the only
|
|
140
|
+
* difference — the condition the advisory itself states. Keyword shape (`sameShape`)
|
|
141
|
+
* cannot see either of these, and both were measured naming sites an agent then
|
|
142
|
+
* correctly declined, which trains it to ignore the advisory:
|
|
143
|
+
*
|
|
144
|
+
* - **Fields the site does not carry.** A helper whose body/override sets
|
|
145
|
+
* `publish_at` cannot replace a POST whose body has only `status` and `title`:
|
|
146
|
+
* substituting ADDS a field, changing the sibling's request. `body` and
|
|
147
|
+
* `dataOverride` are liftable *keywords*, so their mere presence matched; their
|
|
148
|
+
* contents never got compared. (ghost `pr-scheduled-publishing`: a scheduled-post
|
|
149
|
+
* helper offered for a draft-post call, declined for exactly this reason.)
|
|
150
|
+
* - **A schema-checked site.** When the site feeds its response to `check_schema`
|
|
151
|
+
* against a per-file expected body and the helper wraps no such check, replacing
|
|
152
|
+
* the block either drops the contract assertion or redesigns the file. That is a
|
|
153
|
+
* structural difference, not a literal one. (demoshop `P08`: the contract-file
|
|
154
|
+
* sites, declined as "a design change beyond this PR's scope".)
|
|
155
|
+
*
|
|
156
|
+
* Both directions are conservative — they only ever drop a candidate — so precision
|
|
157
|
+
* rises and the advisory cannot start over-reporting because of them.
|
|
158
|
+
*/
|
|
159
|
+
function substitutable(helper, site) {
|
|
160
|
+
for (const f of helper.fields)
|
|
161
|
+
if (!site.fields.has(f))
|
|
162
|
+
return false;
|
|
163
|
+
return helper.schemaChecked === site.schemaChecked;
|
|
164
|
+
}
|
|
165
|
+
/** Lines after a call scanned for its schema assertion — the generated shape puts it
|
|
166
|
+
* immediately after the status-code assertion. */
|
|
167
|
+
export const SCHEMA_SCAN_LINES = 12;
|
|
168
|
+
/** True when a `check_schema` / `checkSchema` assertion sits in the given 1-based line
|
|
169
|
+
* range — the response-shape contract a status-only helper does not carry. */
|
|
170
|
+
export function hasSchemaCheck(content, from, to) {
|
|
171
|
+
const lines = content.split("\n").slice(Math.max(0, from - 1), to);
|
|
172
|
+
return /\bcheck_?[Ss]chema\s*\(/.test(lines.join("\n"));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The request's body/override FIELD names — leaf names only, so a dotted override key
|
|
176
|
+
* (`posts.0.publish_at`) and the body literal it targets (`"publish_at":`) are one
|
|
177
|
+
* field. Reads the body through its variable: generated code assigns the template to
|
|
178
|
+
* an identifier and passes `body=<ident>`, so the fields live in the assignment, not
|
|
179
|
+
* in the call.
|
|
180
|
+
*
|
|
181
|
+
* `callAt` is the call's offset in `content`. The body variable resolves to the
|
|
182
|
+
* assignment NEAREST BEFORE it, because generated code reuses the name `body` per
|
|
183
|
+
* request: a file-wide first match gives every later request the first one's fields,
|
|
184
|
+
* so two different payloads compare equal. Omit it only when `content` holds a single
|
|
185
|
+
* request.
|
|
186
|
+
*/
|
|
187
|
+
export function requestFields(content, call, callAt) {
|
|
188
|
+
const out = new Set();
|
|
189
|
+
const add = (raw) => {
|
|
190
|
+
const leaf = raw.split(".").pop() ?? "";
|
|
191
|
+
// Array indices are positions, not fields.
|
|
192
|
+
if (leaf && !/^\d+$/.test(leaf))
|
|
193
|
+
out.add(leaf);
|
|
194
|
+
};
|
|
195
|
+
// FIELDS COME FROM THE PAYLOAD ONLY. Every other liftable argument carries quoted
|
|
196
|
+
// keys of its own that are not body fields: `path_params={"orders": id}` names a
|
|
197
|
+
// placeholder (the same `{}` in the same normalised path), `headers={"Authorization":
|
|
198
|
+
// t}` names a header, `query_params={"page": 1}` a query key. Reading any of them as
|
|
199
|
+
// a field reports a correct retrofit as changing the request — measured on a demoshop
|
|
200
|
+
// DELETE whose helper labelled the path parameter `orders` where the baseline said
|
|
201
|
+
// `order_id`. shapeKeys already ignores these keywords; ignoring the keyword while
|
|
202
|
+
// reading its contents was the incoherence. `body` and `data_override` stay, because
|
|
203
|
+
// their keys ARE payload paths.
|
|
204
|
+
const body = call.replace(/\b(?:path_params|pathParams|query_params|queryParams|headers|url)\s*[=:]\s*\{[^{}]*\}/g, " ");
|
|
205
|
+
// Dotted override keys, and any body literal written inline in the call.
|
|
206
|
+
for (const m of body.matchAll(/["'`]([A-Za-z_][\w.]*)["'`]\s*:/g))
|
|
207
|
+
add(m[1]);
|
|
208
|
+
// TS shorthand inside an override object — `dataOverride: { status, title }` names
|
|
209
|
+
// the same fields as `{ "status": status }` but carries neither quotes nor a colon,
|
|
210
|
+
// so the pattern above cannot see it and the field would compare as absent.
|
|
211
|
+
for (const region of overrideObjects(body))
|
|
212
|
+
for (const m of region.matchAll(/(?:^|[{,])\s*([A-Za-z_$][\w$]*)\s*(?=[,}])/g))
|
|
213
|
+
add(m[1]);
|
|
214
|
+
// `body=IDENT` / `body: IDENT` → the identifier's assignment elsewhere in the file.
|
|
215
|
+
const ref = /\bbody\s*[=:]\s*([A-Za-z_$][\w$]*)/.exec(call);
|
|
216
|
+
if (ref) {
|
|
217
|
+
const assignRe = new RegExp(`\\b${escapeRegExp(ref[1])}\\b\\s*(?::[^=]*)?=`, "g");
|
|
218
|
+
// The last assignment before the call; the first in the file when the offset is
|
|
219
|
+
// unknown or nothing precedes it.
|
|
220
|
+
let assign = null;
|
|
221
|
+
for (const m of content.matchAll(assignRe)) {
|
|
222
|
+
if (callAt !== undefined && (m.index ?? 0) > callAt)
|
|
223
|
+
break;
|
|
224
|
+
assign = m;
|
|
225
|
+
if (callAt === undefined)
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
if (assign) {
|
|
229
|
+
// From the assignment to the end of its braced literal — the template is one
|
|
230
|
+
// string, so the first balanced `{`…`}` after it is the body.
|
|
231
|
+
const tail = content.slice(assign.index + assign[0].length);
|
|
232
|
+
const open = tail.indexOf("{");
|
|
233
|
+
if (open !== -1) {
|
|
234
|
+
let depth = 0;
|
|
235
|
+
let end = tail.length;
|
|
236
|
+
for (let i = open; i < tail.length; i++) {
|
|
237
|
+
if (tail[i] === "{")
|
|
238
|
+
depth++;
|
|
239
|
+
else if (tail[i] === "}") {
|
|
240
|
+
depth--;
|
|
241
|
+
if (depth === 0) {
|
|
242
|
+
end = i + 1;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const m of tail
|
|
248
|
+
.slice(open, end)
|
|
249
|
+
.matchAll(/["'`]([A-Za-z_][\w.]*)["'`]\s*:/g))
|
|
250
|
+
add(m[1]);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
/** The `{…}` text of each `data_override` / `dataOverride` argument in a call. */
|
|
257
|
+
function overrideObjects(call) {
|
|
258
|
+
const out = [];
|
|
259
|
+
for (const m of call.matchAll(/\b(?:data_override|dataOverride)\s*[=:]\s*\{/g)) {
|
|
260
|
+
const open = (m.index ?? 0) + m[0].length - 1;
|
|
261
|
+
let depth = 0;
|
|
262
|
+
for (let i = open; i < call.length; i++) {
|
|
263
|
+
if (call[i] === "{")
|
|
264
|
+
depth++;
|
|
265
|
+
else if (call[i] === "}") {
|
|
266
|
+
depth--;
|
|
267
|
+
if (depth === 0) {
|
|
268
|
+
out.push(call.slice(open, i + 1));
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
110
276
|
/** Keywords whose presence differs legitimately between two versions of one request:
|
|
111
277
|
* the literals the merge rule lifts to parameters. Everything else is call shape. */
|
|
112
278
|
const LIFTABLE_KEYS = new Set([
|
|
@@ -138,7 +304,7 @@ function sameShape(a, b) {
|
|
|
138
304
|
return true;
|
|
139
305
|
}
|
|
140
306
|
/** Text of the parenthesised argument list starting at the `(` at `open`. */
|
|
141
|
-
function callText(content, open) {
|
|
307
|
+
export function callText(content, open) {
|
|
142
308
|
let depth = 0;
|
|
143
309
|
for (let i = open; i < content.length; i++) {
|
|
144
310
|
const ch = content[i];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type SiblingFile } from "./call-sites.js";
|
|
2
|
+
import type { UtilsHelper } from "./parse.js";
|
|
3
|
+
import type { UtilsLanguageSpec } from "./language-spec.js";
|
|
4
|
+
/** A helper imported by at most one delivered test. */
|
|
5
|
+
export interface SingleImporterHelper {
|
|
6
|
+
/** Absolute path of the utils file. */
|
|
7
|
+
file: string;
|
|
8
|
+
helper: string;
|
|
9
|
+
line: number;
|
|
10
|
+
/** Delivered tests importing it (0 or 1). */
|
|
11
|
+
importers: number;
|
|
12
|
+
}
|
|
13
|
+
/** The strings a sibling's source would contain if it imports the module at
|
|
14
|
+
* `modulePath` (relative to the sibling's directory): the path form for TS/JS
|
|
15
|
+
* specifiers, and the dotted Python form — `../shared/X` → `..shared.X`,
|
|
16
|
+
* `shared/X` → `shared.X` / `.shared.X`. */
|
|
17
|
+
export declare function moduleMentionCandidates(modulePath: string): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Count, per utils helper, how many Skyramp-generated tests in the spec's directory
|
|
20
|
+
* import it — the spec under verification included.
|
|
21
|
+
*
|
|
22
|
+
* Findings:
|
|
23
|
+
* - 0 importers: nothing imports the helper, not even the spec that wrote it — a
|
|
24
|
+
* dead helper, actionable on any run.
|
|
25
|
+
* - 1 importer: not (yet) shared. Reported only when the directory holds at least
|
|
26
|
+
* TWO delivered tests: on a first run every helper has exactly one importer by
|
|
27
|
+
* construction (it seeds the utils file), so listing them would be guaranteed
|
|
28
|
+
* noise on the most common path.
|
|
29
|
+
* Fails open to "no findings" on any read error.
|
|
30
|
+
*/
|
|
31
|
+
export declare function singleImporterHelpers(testFile: string, utilsFiles: string[], helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings?: SiblingFile[]): Promise<SingleImporterHelper[]>;
|