@skyramp/mcp 0.4.2 → 0.4.3-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.
Files changed (119) hide show
  1. package/build/execution/wrapperConfig.d.ts +4 -0
  2. package/build/execution/wrapperConfig.js +14 -2
  3. package/build/prompts/code-reuse.d.ts +5 -1
  4. package/build/prompts/code-reuse.js +16 -10
  5. package/build/prompts/modularization/ui-test-modularization.js +9 -2
  6. package/build/prompts/pom-aware-code-reuse.d.ts +4 -1
  7. package/build/prompts/pom-aware-code-reuse.js +38 -6
  8. package/build/prompts/reuse-hand-off.d.ts +4 -0
  9. package/build/prompts/reuse-hand-off.js +10 -5
  10. package/build/prompts/shared-helper-policy.d.ts +5 -0
  11. package/build/prompts/shared-helper-policy.js +5 -0
  12. package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
  13. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +2 -1
  14. package/build/prompts/testbot/testbot-prompts.js +33 -16
  15. package/build/recommendation/fakeVerifyContext.d.ts +3 -0
  16. package/build/recommendation/fakeVerifyContext.js +16 -0
  17. package/build/recommendation/runVerifiers.js +2 -0
  18. package/build/recommendation/types.d.ts +12 -0
  19. package/build/recommendation/verifierContracts.d.ts +24 -2
  20. package/build/recommendation/verifierContracts.js +32 -4
  21. package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
  22. package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
  23. package/build/recommendation/verifiers/expectedValueSourced.js +8 -0
  24. package/build/recommendation/verifiers/requirementSourced.js +141 -11
  25. package/build/services/TestGenerationService.js +4 -0
  26. package/build/tools/code-refactor/caller-gate.d.ts +105 -0
  27. package/build/tools/code-refactor/caller-gate.js +174 -0
  28. package/build/tools/code-refactor/changed-helper-report.d.ts +62 -0
  29. package/build/tools/code-refactor/changed-helper-report.js +114 -0
  30. package/build/tools/code-refactor/codeReuseTool.js +131 -62
  31. package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -2
  32. package/build/tools/code-refactor/helper-callers.d.ts +65 -0
  33. package/build/tools/code-refactor/helper-callers.js +85 -0
  34. package/build/tools/code-refactor/modularizationTool.js +10 -0
  35. package/build/tools/code-refactor/pom-pass-state.d.ts +51 -0
  36. package/build/tools/code-refactor/pom-pass-state.js +115 -0
  37. package/build/tools/code-refactor/retrofit-state.d.ts +3 -2
  38. package/build/tools/code-refactor/retrofit-state.js +11 -12
  39. package/build/tools/code-refactor/reuse-outcome.d.ts +6 -0
  40. package/build/tools/code-refactor/reuse-record-store.d.ts +52 -0
  41. package/build/tools/code-refactor/reuse-record-store.js +126 -0
  42. package/build/tools/code-refactor/reuse-state.d.ts +18 -6
  43. package/build/tools/code-refactor/reuse-state.js +78 -131
  44. package/build/tools/code-refactor/utils-verify-gates.js +3 -3
  45. package/build/tools/code-refactor/verify-gates.d.ts +4 -0
  46. package/build/tools/code-refactor/verify-gates.js +8 -1
  47. package/build/tools/generate-tests/generateContractRestTool.js +7 -7
  48. package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -8
  49. package/build/tools/generate-tests/generateUIRestTool.js +3 -3
  50. package/build/tools/submitReportTool.js +91 -29
  51. package/build/tools/test-management/actionsTool.js +7 -0
  52. package/build/tools/test-management/registerTestPlanTool.d.ts +1 -0
  53. package/build/tools/test-management/registerTestPlanTool.js +70 -22
  54. package/build/types/RepositoryAnalysis.d.ts +20 -20
  55. package/build/types/ReuseOutcome.d.ts +96 -0
  56. package/build/types/TestExecution.d.ts +11 -0
  57. package/build/types/TestExecution.js +19 -0
  58. package/build/types/index.d.ts +1 -1
  59. package/build/utils/branchDiff.d.ts +1 -1
  60. package/build/utils/branchDiff.js +1 -1
  61. package/build/utils/changedRuns.d.ts +13 -0
  62. package/build/utils/changedRuns.js +56 -0
  63. package/build/utils/featureFlags.d.ts +8 -10
  64. package/build/utils/featureFlags.js +23 -21
  65. package/build/utils/normalizeSkyrampImports.d.ts +4 -1
  66. package/build/utils/normalizeSkyrampImports.js +24 -17
  67. package/build/utils/pom-scope/ownership.d.ts +19 -0
  68. package/build/utils/pom-scope/ownership.js +31 -0
  69. package/build/utils/pom-scope/pom-files.d.ts +8 -0
  70. package/build/utils/pom-scope/pom-files.js +8 -0
  71. package/build/utils/pom-scope/scoring.js +13 -1
  72. package/build/utils/pom-scope/strip.d.ts +10 -0
  73. package/build/utils/pom-scope/strip.js +9 -0
  74. package/build/utils/pom-verify/verify.js +13 -2
  75. package/build/utils/reuseRouting.d.ts +30 -2
  76. package/build/utils/reuseRouting.js +36 -15
  77. package/build/utils/testDependencyPolicy.js +4 -16
  78. package/build/utils/utils-verify/action-sites.d.ts +2 -2
  79. package/build/utils/utils-verify/body-reach.d.ts +2 -2
  80. package/build/utils/utils-verify/body-reach.js +4 -1
  81. package/build/utils/utils-verify/call-sites.d.ts +16 -6
  82. package/build/utils/utils-verify/call-sites.js +12 -6
  83. package/build/utils/utils-verify/changed-helpers.d.ts +38 -0
  84. package/build/utils/utils-verify/changed-helpers.js +91 -0
  85. package/build/utils/utils-verify/head.d.ts +48 -0
  86. package/build/utils/utils-verify/head.js +115 -0
  87. package/build/utils/utils-verify/importers.d.ts +59 -10
  88. package/build/utils/utils-verify/importers.js +121 -31
  89. package/build/utils/utils-verify/in-house.js +26 -31
  90. package/build/utils/utils-verify/index.d.ts +3 -0
  91. package/build/utils/utils-verify/index.js +3 -0
  92. package/build/utils/utils-verify/language-spec.js +10 -2
  93. package/build/utils/utils-verify/locate.d.ts +3 -0
  94. package/build/utils/utils-verify/locate.js +6 -6
  95. package/build/utils/utils-verify/parse.d.ts +13 -1
  96. package/build/utils/utils-verify/parse.js +122 -49
  97. package/build/utils/utils-verify/retrofit-equivalence.d.ts +0 -5
  98. package/build/utils/utils-verify/retrofit-equivalence.js +2 -34
  99. package/build/utils/utils-verify/stage.js +4 -16
  100. package/build/utils/utils-verify/verify.d.ts +9 -3
  101. package/build/utils/utils-verify/verify.js +26 -12
  102. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1 -0
  103. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +1 -0
  104. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +6 -0
  105. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +303 -150
  106. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  107. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-LNgEKtdV.js → codeMirrorModule-Bou8kKzE.js} +1 -1
  108. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-Bwr1eMKC.js → defaultSettingsView-DTp2-8Si.js} +3 -3
  109. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.BAkLd5DX.js → index.ZYuEFHsi.js} +1 -1
  110. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  111. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.BPopbasy.js → uiMode.GLqsx5cI.js} +1 -1
  112. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  113. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  114. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  115. package/node_modules/playwright/package.json +1 -1
  116. package/package.json +2 -2
  117. package/plugin/prompts/code-reuse/hand-off.md +13 -1
  118. package/plugin/prompts/plan-tests.md +2 -2
  119. package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -0
@@ -1,14 +1,30 @@
1
- import { type SiblingFile } from "./call-sites.js";
2
- import type { UtilsHelper } from "./parse.js";
1
+ import { type SiblingScan } from "./call-sites.js";
2
+ import { type UtilsHelper } from "./parse.js";
3
3
  import type { UtilsLanguageSpec } from "./language-spec.js";
4
- /** A helper imported by at most one delivered test. */
5
- export interface SingleImporterHelper {
4
+ /** One helper with the Skyramp-generated tests that import it. */
5
+ export interface HelperImporters {
6
6
  /** Absolute path of the utils file. */
7
7
  file: string;
8
8
  helper: string;
9
9
  line: number;
10
- /** Delivered tests importing it (0 or 1). */
11
- importers: number;
10
+ /** ABSOLUTE PATHS of the delivered tests importing it, in directory order — the
11
+ * spec under verification included. The count alone answers "is this helper
12
+ * shared"; the paths answer "which tests does a change to it reach", which is
13
+ * what a caller has to be named and run. */
14
+ importers: string[];
15
+ }
16
+ /** The importer walk's result. `read` is the walk's OWN verdict on whether it ran:
17
+ * an empty `helpers` means "no helper imports anything" only when `read` is true,
18
+ * and means nothing at all when it is false. A caller that blocks or asserts on
19
+ * the absence of an importer must consult it. */
20
+ export interface HelperImporterScan {
21
+ helpers: HelperImporters[];
22
+ /** Absolute paths of the generated tests the walk read. */
23
+ siblings: string[];
24
+ /** The sibling walk saw the whole directory: the listing succeeded and every
25
+ * candidate file was read. False means the importer lists are short by an unknown
26
+ * amount, not that they are empty. */
27
+ read: boolean;
12
28
  }
13
29
  /** The strings a sibling's source would contain if it imports the module at
14
30
  * `modulePath` (relative to the sibling's directory): the path form for TS/JS
@@ -16,8 +32,21 @@ export interface SingleImporterHelper {
16
32
  * `shared/X` → `shared.X` / `.shared.X`. */
17
33
  export declare function moduleMentionCandidates(modulePath: string): string[];
18
34
  /**
19
- * Count, per utils helper, how many Skyramp-generated tests in the spec's directory
20
- * import it — the spec under verification included.
35
+ * The Skyramp-generated tests in the spec's directory that import each utils helper —
36
+ * the spec under verification included. THE importer walk: the single-importer
37
+ * advisory and the changed-helper caller set both read it, so one definition decides
38
+ * what counts as an importing test.
39
+ *
40
+ * Fails open to an unread scan on any error. That is a silence, not an empty answer;
41
+ * `read` is what tells the two apart.
42
+ *
43
+ * SCOPE, which every caller inherits: siblings are the spec's OWN DIRECTORY. A
44
+ * generated test importing the module from another directory is not found here, so
45
+ * the importer list is a LOWER BOUND on the callers, never the complete set.
46
+ */
47
+ export declare function helperImporters(testFile: string, utilsFiles: string[], helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings?: SiblingScan): Promise<HelperImporterScan>;
48
+ /**
49
+ * Helpers with 0 or 1 importing test — the advisory slice of {@link helperImporters}.
21
50
  *
22
51
  * Findings:
23
52
  * - 0 importers: nothing imports the helper, not even the spec that wrote it — a
@@ -26,6 +55,26 @@ export declare function moduleMentionCandidates(modulePath: string): string[];
26
55
  * TWO delivered tests: on a first run every helper has exactly one importer by
27
56
  * construction (it seeds the utils file), so listing them would be guaranteed
28
57
  * noise on the most common path.
29
- * Fails open to "no findings" on any read error.
58
+ *
59
+ * Reports NOTHING from an incomplete walk. This is a claim, not a gate — it tells the
60
+ * agent a helper is dead and to call it or delete it — and a walk that could not read
61
+ * every candidate cannot support it: the file it failed to read may be the importer.
62
+ * Silence on doubt, so the finding stays trustworthy when it is made.
63
+ */
64
+ export declare function singleImporterHelpers(testFile: string, utilsFiles: string[], helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings?: SiblingScan): Promise<HelperImporters[]>;
65
+ /**
66
+ * `content` prepared for an import scan: the copy `importedNames` should read.
67
+ *
68
+ * Comments go in both languages — an import quoted in prose is not an import, and
69
+ * counting one makes a test a "caller" of a helper it cannot reach, which a gate then
70
+ * blocks on. Python string CONTENTS go too, since a Python import names no string, so
71
+ * the usage example in a docstring stops counting.
72
+ *
73
+ * TS/JS cannot blank every string, because the module specifier IS a string
74
+ * (`from "./skyrampUtils"`). Only the specifiers are kept: a string that directly
75
+ * follows `from` or `require(` stays, and every other string is blanked. Requiring
76
+ * the import to start a line is not enough on its own — a template literal holds
77
+ * whole lines, and the `require` form is bounded by a declaration keyword rather than
78
+ * by the line start — so quoted code kept reading as executable code.
30
79
  */
31
- export declare function singleImporterHelpers(testFile: string, utilsFiles: string[], helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings?: SiblingFile[]): Promise<SingleImporterHelper[]>;
80
+ export declare function importScanSource(content: string, spec: UtilsLanguageSpec): string;
@@ -1,5 +1,8 @@
1
1
  import * as path from "path";
2
2
  import { generatedSiblings } from "./call-sites.js";
3
+ import { realpath } from "./locate.js";
4
+ import { blankComments, blankCommentsAndStrings, } from "./parse.js";
5
+ import { stringSpans } from "../pom-scope/strip.js";
3
6
  /** The strings a sibling's source would contain if it imports the module at
4
7
  * `modulePath` (relative to the sibling's directory): the path form for TS/JS
5
8
  * specifiers, and the dotted Python form — `../shared/X` → `..shared.X`,
@@ -13,29 +16,36 @@ export function moduleMentionCandidates(modulePath) {
13
16
  return [posix, "./" + posix, rest, dotted, "." + rest];
14
17
  }
15
18
  /**
16
- * Count, per utils helper, how many Skyramp-generated tests in the spec's directory
17
- * import it — the spec under verification included.
19
+ * The Skyramp-generated tests in the spec's directory that import each utils helper —
20
+ * the spec under verification included. THE importer walk: the single-importer
21
+ * advisory and the changed-helper caller set both read it, so one definition decides
22
+ * what counts as an importing test.
18
23
  *
19
- * Findings:
20
- * - 0 importers: nothing imports the helper, not even the spec that wrote it — a
21
- * dead helper, actionable on any run.
22
- * - 1 importer: not (yet) shared. Reported only when the directory holds at least
23
- * TWO delivered tests: on a first run every helper has exactly one importer by
24
- * construction (it seeds the utils file), so listing them would be guaranteed
25
- * noise on the most common path.
26
- * Fails open to "no findings" on any read error.
24
+ * Fails open to an unread scan on any error. That is a silence, not an empty answer;
25
+ * `read` is what tells the two apart.
26
+ *
27
+ * SCOPE, which every caller inherits: siblings are the spec's OWN DIRECTORY. A
28
+ * generated test importing the module from another directory is not found here, so
29
+ * the importer list is a LOWER BOUND on the callers, never the complete set.
27
30
  */
28
- export async function singleImporterHelpers(testFile, utilsFiles, helpersByFile, spec, preReadSiblings) {
31
+ export async function helperImporters(testFile, utilsFiles, helpersByFile, spec, preReadSiblings) {
29
32
  let dir;
30
33
  let siblings;
31
34
  try {
32
- dir = path.dirname(testFile);
35
+ // CANONICAL, because the module paths this is compared against are: they come
36
+ // from locateUtilsFiles, which realpaths everything. A spec named through a
37
+ // symlinked parent (macOS `/var` → `/private/var`, and every worktree under a
38
+ // linked temporary directory) otherwise relativises to a `../../…` path no
39
+ // sibling's import can contain, so every helper in the module read as imported
40
+ // by nothing — silently, since an empty importer list is also the honest answer
41
+ // for a helper nobody calls.
42
+ dir = path.dirname(await realpath(testFile));
33
43
  siblings =
34
44
  preReadSiblings ??
35
45
  (await generatedSiblings(testFile, spec, { includeSelf: true }));
36
46
  }
37
47
  catch {
38
- return [];
48
+ return { helpers: [], siblings: [], read: false };
39
49
  }
40
50
  // `importedNames` matches by basename stem, so a bare `from SkyrampUtils import …`
41
51
  // in a sibling would also be credited to `../shared/SkyrampUtils`. A sibling's
@@ -49,30 +59,110 @@ export async function singleImporterHelpers(testFile, utilsFiles, helpersByFile,
49
59
  inDir: path.dirname(f) === dir,
50
60
  mentions: moduleMentionCandidates(path.relative(dir, f).replace(/\.[^.]+$/, "")),
51
61
  }));
52
- // helper key = `${utilsFile}\0${name}` → number of importing tests
53
- const counts = new Map();
62
+ // helper key = `${utilsFile}\0${name}` → the tests importing it
63
+ const importers = new Map();
54
64
  for (const [file, helpers] of helpersByFile)
55
65
  for (const h of helpers)
56
- counts.set(`${file}\0${h.name}`, 0);
57
- for (const { content } of siblings) {
66
+ importers.set(`${file}\0${h.name}`, []);
67
+ for (const { file: sibling, content } of siblings.files) {
68
+ // Imports are read from the CODE, never from prose. A docstring or a comment
69
+ // quoting `from SkyrampUtils import …` would otherwise make that test a caller of
70
+ // a helper it cannot reach, and the gate would refuse the report over it — a
71
+ // false block, which this design treats as the worst outcome available to it.
72
+ const code = importScanSource(content, spec);
58
73
  for (const { file: utilsFile, stem, inDir, mentions } of stems) {
59
74
  if (!inDir && !mentions.some((c) => content.includes(c)))
60
75
  continue;
61
- const imported = new Set(spec.importedNames(content, stem));
76
+ const imported = new Set(spec.importedNames(code, stem));
62
77
  for (const h of helpersByFile.get(utilsFile) ?? [])
63
- if (imported.has(h.name)) {
64
- const k = `${utilsFile}\0${h.name}`;
65
- counts.set(k, (counts.get(k) ?? 0) + 1);
66
- }
78
+ if (imported.has(h.name))
79
+ importers.get(`${utilsFile}\0${h.name}`)?.push(sibling);
67
80
  }
68
81
  }
69
- const secondRunExists = siblings.length >= 2;
70
- const out = [];
71
- for (const [file, helpers] of helpersByFile)
72
- for (const h of helpers) {
73
- const n = counts.get(`${file}\0${h.name}`) ?? 0;
74
- if (n === 0 || (n === 1 && secondRunExists))
75
- out.push({ file, helper: h.name, line: h.line, importers: n });
76
- }
77
- return out;
82
+ const helpers = [];
83
+ for (const [file, hs] of helpersByFile)
84
+ for (const h of hs)
85
+ helpers.push({
86
+ file,
87
+ helper: h.name,
88
+ line: h.line,
89
+ importers: importers.get(`${file}\0${h.name}`) ?? [],
90
+ });
91
+ return {
92
+ helpers,
93
+ siblings: siblings.files.map((s) => s.file),
94
+ read: siblings.complete,
95
+ };
96
+ }
97
+ /**
98
+ * Helpers with 0 or 1 importing test — the advisory slice of {@link helperImporters}.
99
+ *
100
+ * Findings:
101
+ * - 0 importers: nothing imports the helper, not even the spec that wrote it — a
102
+ * dead helper, actionable on any run.
103
+ * - 1 importer: not (yet) shared. Reported only when the directory holds at least
104
+ * TWO delivered tests: on a first run every helper has exactly one importer by
105
+ * construction (it seeds the utils file), so listing them would be guaranteed
106
+ * noise on the most common path.
107
+ *
108
+ * Reports NOTHING from an incomplete walk. This is a claim, not a gate — it tells the
109
+ * agent a helper is dead and to call it or delete it — and a walk that could not read
110
+ * every candidate cannot support it: the file it failed to read may be the importer.
111
+ * Silence on doubt, so the finding stays trustworthy when it is made.
112
+ */
113
+ export async function singleImporterHelpers(testFile, utilsFiles, helpersByFile, spec, preReadSiblings) {
114
+ const scan = await helperImporters(testFile, utilsFiles, helpersByFile, spec, preReadSiblings);
115
+ if (!scan.read)
116
+ return [];
117
+ const secondRunExists = scan.siblings.length >= 2;
118
+ return scan.helpers.filter((h) => h.importers.length === 0 || (h.importers.length === 1 && secondRunExists));
119
+ }
120
+ /**
121
+ * `content` prepared for an import scan: the copy `importedNames` should read.
122
+ *
123
+ * Comments go in both languages — an import quoted in prose is not an import, and
124
+ * counting one makes a test a "caller" of a helper it cannot reach, which a gate then
125
+ * blocks on. Python string CONTENTS go too, since a Python import names no string, so
126
+ * the usage example in a docstring stops counting.
127
+ *
128
+ * TS/JS cannot blank every string, because the module specifier IS a string
129
+ * (`from "./skyrampUtils"`). Only the specifiers are kept: a string that directly
130
+ * follows `from` or `require(` stays, and every other string is blanked. Requiring
131
+ * the import to start a line is not enough on its own — a template literal holds
132
+ * whole lines, and the `require` form is bounded by a declaration keyword rather than
133
+ * by the line start — so quoted code kept reading as executable code.
134
+ */
135
+ export function importScanSource(content, spec) {
136
+ if (spec.language === "python")
137
+ return blankCommentsAndStrings(content, spec);
138
+ const noComments = blankComments(content, spec);
139
+ const out = noComments.split("");
140
+ for (const { start, end } of stringSpans(noComments)) {
141
+ // A module specifier is a string in exactly one position: straight after `from`
142
+ // or after `require(`. Anything else is data, whatever it spells.
143
+ if (isSpecifierPosition(noComments, start))
144
+ continue;
145
+ for (let i = start; i < end; i++)
146
+ out[i] = noComments[i] === "\n" ? "\n" : " ";
147
+ }
148
+ return out.join("");
149
+ }
150
+ /** Whether the string opening at `at` sits where a module specifier does: after
151
+ * `from`, or after `require(`, with only whitespace between. Read backwards over a
152
+ * fixed window rather than by matching a regex against `src.slice(0, at)` — that
153
+ * copied the file once per literal, quadratic in a spec with thousands of them. */
154
+ function isSpecifierPosition(src, at) {
155
+ let i = at;
156
+ while (i > 0 && /\s/.test(src[i - 1]))
157
+ i--;
158
+ const word = (w) => src.slice(Math.max(0, i - w.length), i) === w &&
159
+ !/[\w$]/.test(src[i - w.length - 1] ?? "");
160
+ if (word("from"))
161
+ return true;
162
+ if (src[i - 1] !== "(")
163
+ return false;
164
+ i--;
165
+ while (i > 0 && /\s/.test(src[i - 1]))
166
+ i--;
167
+ return word("require");
78
168
  }
@@ -1,14 +1,12 @@
1
- import { execFile } from "child_process";
2
- import { promisify } from "util";
3
1
  import * as fs from "fs";
4
2
  import * as path from "path";
5
3
  import { detectGitRoot } from "../gitStaging.js";
6
4
  import { logger } from "../logger.js";
7
5
  import { escapeRegExp } from "../regex.js";
8
- import { importedModules, realpath, UTILS_FILE_HEADER_RE } from "./locate.js";
6
+ import { hasUtilsHeaderContent, importedModules, realpath } from "./locate.js";
7
+ import { contentAtHead } from "./head.js";
9
8
  import { blankComments, blankCommentsAndStrings, defaultAt, hasDefault, parseUtilsFile, } from "./parse.js";
10
9
  import { blankStringContents, matchingClose } from "./action-key.js";
11
- const execFileAsync = promisify(execFile);
12
10
  export async function locateInHouseModules(params) {
13
11
  const root = params.cwd ?? detectGitRoot(params.testFile);
14
12
  if (!root)
@@ -26,7 +24,7 @@ export async function locateInHouseModules(params) {
26
24
  const rel = path.relative(realRoot, real);
27
25
  if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel))
28
26
  continue;
29
- const head = await headContent(realRoot, rel.split(path.sep).join("/"));
27
+ const head = await contentAtHead(real, realRoot);
30
28
  if (head === undefined)
31
29
  continue; // not tracked: created this run, or never a file
32
30
  // Ownership is read at HEAD and nowhere else. A Skyramp module from an earlier
@@ -34,31 +32,12 @@ export async function locateInHouseModules(params) {
34
32
  // did to its first lines; a module headerless at HEAD is the customer's, and a
35
33
  // header the working copy gained since is a change the additive gate refuses —
36
34
  // reading the working copy here would let that stamp reclassify their file.
37
- if (hasHeader(head))
35
+ if (hasUtilsHeaderContent(head))
38
36
  continue;
39
37
  out.push({ file: real, relPath: rel.split(path.sep).join("/") });
40
38
  }
41
39
  return out.sort((a, b) => a.file.localeCompare(b.file));
42
40
  }
43
- /** Whether a module's first lines carry the Skyramp utils header. */
44
- function hasHeader(content) {
45
- return UTILS_FILE_HEADER_RE.test(content.split("\n").slice(0, 5).join("\n"));
46
- }
47
- /** The module's content at HEAD, or `undefined` when HEAD does not hold it (or git
48
- * cannot be asked). */
49
- async function headContent(root, rel) {
50
- try {
51
- const { stdout } = await execFileAsync("git", ["show", `HEAD:${rel}`], {
52
- cwd: root,
53
- encoding: "utf8",
54
- maxBuffer: 16 * 1024 * 1024,
55
- });
56
- return stdout;
57
- }
58
- catch {
59
- return undefined;
60
- }
61
- }
62
41
  /** The number of arguments a helper accepts: from the position of its last required
63
42
  * parameter to its total, a rest parameter opening the top. A parameter with a
64
43
  * default, an optional `x?`, or a Python `*args`/`**kw` is not required — but a
@@ -224,20 +203,36 @@ function definitionLineOf(moduleLines, name) {
224
203
  const re = new RegExp(`\\b(?:class|const|let|var|enum|interface|type|function|def)\\s+${escapeRegExp(name)}\\b`);
225
204
  return moduleLines.find((l) => re.test(l));
226
205
  }
227
- /** `export { name } from "…"`, or `export * from "…"` a binding of another module
228
- * exported here. Exists as an export; not readable as a helper of this module. */
206
+ /** `export { name } from "…"`, `export * from "…"`, or a binding this module IMPORTS
207
+ * and then lists in a local `export { }` a binding of another module exported
208
+ * here. Exists as an export; not readable as a helper of this module. The last form
209
+ * is how a fixtures module passes Playwright's `expect` through beside its extended
210
+ * `test`: `import { test as base, expect } from "@playwright/test"; … export { expect };`. */
229
211
  function isReExported(moduleCode, name) {
230
212
  // `export * from "./x"` may legally provide any name; nothing here can read it.
231
213
  if (/\bexport\s*\*\s*from\b/.test(moduleCode))
232
214
  return true;
233
- for (const m of moduleCode.matchAll(/\bexport\s*\{([^}]*)\}\s*from\b/g))
215
+ for (const m of moduleCode.matchAll(/\bexport\s*\{([^}]*)\}(\s*from\b)?/g))
234
216
  for (const raw of m[1].split(",")) {
235
217
  const [local, exported] = raw.trim().split(/\s+as\s+/);
236
- if ((exported ?? local) === name)
218
+ if ((exported ?? local) !== name)
219
+ continue;
220
+ // `export { x } from "…"` names the other module's binding directly; a local
221
+ // list exports this module's binding, which is another module's only when
222
+ // an import brought it in.
223
+ if (m[2] || isImportedBinding(moduleCode, local))
237
224
  return true;
238
225
  }
239
226
  return false;
240
227
  }
228
+ /** Whether an import statement or a CommonJS destructure binds `local` in this
229
+ * module: `import { local }`, `import { x as local }`, `import local from`,
230
+ * `import * as local from`, `const { local } = require(…)`. */
231
+ function isImportedBinding(moduleCode, local) {
232
+ const n = escapeRegExp(local);
233
+ return (new RegExp(`\\bimport\\s+(?:type\\s+)?[^;]*?(?:[{,]\\s*(?:\\w+\\s+as\\s+)?${n}\\s*[,}]|\\*\\s+as\\s+${n}\\b|\\b${n}\\s*(?:,|\\bfrom\\b))[^;]*?\\bfrom\\b`).test(moduleCode) ||
234
+ new RegExp(`\\b(?:const|let|var)\\s*\\{[^}]*\\b${n}\\b[^}]*\\}\\s*=\\s*require\\s*\\(`).test(moduleCode));
235
+ }
241
236
  function importLine(lines, name) {
242
237
  const re = new RegExp(`\\b${escapeRegExp(name)}\\b`);
243
238
  const i = lines.findIndex((l) => /\b(?:import|require|from)\b/.test(l) && re.test(l));
@@ -306,7 +301,7 @@ export async function additiveChanges(file, root, spec) {
306
301
  duplicates: [],
307
302
  };
308
303
  const rel = path.relative(root, file).split(path.sep).join("/");
309
- const head = await headContent(root, rel);
304
+ const head = await contentAtHead(file, root);
310
305
  if (head === undefined) {
311
306
  logger.warning("Could not read an in-house module at HEAD — the additive gate did not run", { file });
312
307
  return result;
@@ -335,7 +330,7 @@ export async function additiveChanges(file, root, spec) {
335
330
  }
336
331
  result.ran = true;
337
332
  // Rule 5 of the design, enforced: the file stays the customer's file.
338
- if (hasHeader(work) && !hasHeader(head))
333
+ if (hasUtilsHeaderContent(work) && !hasUtilsHeaderContent(head))
339
334
  result.violations.push({
340
335
  kind: "in-house-modified",
341
336
  file,
@@ -2,9 +2,12 @@ export * from "./language-spec.js";
2
2
  export * from "./parse.js";
3
3
  export * from "./allow.js";
4
4
  export * from "./locate.js";
5
+ export * from "./head.js";
5
6
  export * from "./verify.js";
6
7
  export * from "./stage.js";
7
8
  export * from "./call-sites.js";
9
+ export * from "./importers.js";
10
+ export * from "./changed-helpers.js";
8
11
  export * from "./action-sites.js";
9
12
  export * from "./retrofit-equivalence.js";
10
13
  export * from "./in-house.js";
@@ -2,9 +2,12 @@ export * from "./language-spec.js";
2
2
  export * from "./parse.js";
3
3
  export * from "./allow.js";
4
4
  export * from "./locate.js";
5
+ export * from "./head.js";
5
6
  export * from "./verify.js";
6
7
  export * from "./stage.js";
7
8
  export * from "./call-sites.js";
9
+ export * from "./importers.js";
10
+ export * from "./changed-helpers.js";
8
11
  export * from "./action-sites.js";
9
12
  export * from "./retrofit-equivalence.js";
10
13
  export * from "./in-house.js";
@@ -115,7 +115,10 @@ function tsModuleQualifiers(spec, stem) {
115
115
  const stemRe = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
116
116
  const from = `["'][^"']*\\/${stemRe}(?:\\.[cm]?[jt]s)?["']`;
117
117
  const out = new Set();
118
- for (const m of spec.matchAll(new RegExp(`import\\s*\\*\\s*as\\s+([A-Za-z_$][\\w$]*)\\s*from\\s*${from}`, "g")))
118
+ // Anchored to the START of a line, for the same reason the named-import scan is: a
119
+ // TypeScript specifier is itself a string, so the scan cannot blank strings to keep
120
+ // prose out — it has to require the import to BE a statement.
121
+ for (const m of spec.matchAll(new RegExp(`^\\s*import\\s*\\*\\s*as\\s+([A-Za-z_$][\\w$]*)\\s*from\\s*${from}`, "gm")))
119
122
  out.add(m[1]);
120
123
  for (const m of spec.matchAll(new RegExp(`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*${from}\\s*\\)`, "g")))
121
124
  out.add(m[1]);
@@ -125,7 +128,12 @@ function tsImportedNames(spec, stem) {
125
128
  const names = new Set();
126
129
  const stemRe = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
127
130
  const from = `["'][^"']*\\/${stemRe}(?:\\.[cm]?[jt]s)?["']`;
128
- for (const m of spec.matchAll(new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*${from}`, "g"))) {
131
+ // Anchored to the START of a line: a TypeScript specifier is itself a string, so
132
+ // the scan cannot blank strings to keep prose out — it has to require the import to
133
+ // BE a statement. Without this, ordinary data (`const sample = 'import { x } from
134
+ // "./skyrampUtils"'`) counted as an import, and a gate reading importers as callers
135
+ // would block a report over a test that cannot reach the helper.
136
+ for (const m of spec.matchAll(new RegExp(`^\\s*import\\s*\\{([^}]*)\\}\\s*from\\s*${from}`, "gm"))) {
129
137
  for (const raw of m[1].split(",")) {
130
138
  // The EXPORTED name (before `as`): the count checks what the utils file defines,
131
139
  // so an alias would under-count. Same choice as the Python `from … import x as y`.
@@ -3,6 +3,9 @@ import { type UtilsLanguageSpec } from "./language-spec.js";
3
3
  * <timestamp>`. Deliberately NOT the bare `SKYRAMP_UTILS_HEADER`, which every generated
4
4
  * TEST also carries as its codegen marker (`Generated by Skyramp v<version>`). */
5
5
  export declare const UTILS_FILE_HEADER_RE: RegExp;
6
+ /** Whether `content`'s first lines carry the utils header — the test that makes a
7
+ * file Skyramp's shared module, for a caller that already holds the content. */
8
+ export declare function hasUtilsHeaderContent(content: string): boolean;
6
9
  export interface LocateUtilsParams {
7
10
  /** Absolute path of the delivered test file. */
8
11
  testFile: string;
@@ -15,6 +15,11 @@ export const UTILS_FILE_HEADER_RE = new RegExp(`${SKYRAMP_UTILS_HEADER.replace(/
15
15
  const HEADER_SCAN_LINES = 5;
16
16
  /** Enough bytes for five header lines; a utils file is never read whole to find them. */
17
17
  const HEADER_SCAN_BYTES = 2048;
18
+ /** Whether `content`'s first lines carry the utils header — the test that makes a
19
+ * file Skyramp's shared module, for a caller that already holds the content. */
20
+ export function hasUtilsHeaderContent(content) {
21
+ return UTILS_FILE_HEADER_RE.test(content.split("\n").slice(0, HEADER_SCAN_LINES).join("\n"));
22
+ }
18
23
  /**
19
24
  * Every shared utils file relevant to `testFile`, by CONTENT, never by name.
20
25
  *
@@ -130,12 +135,7 @@ export async function hasUtilsHeader(file) {
130
135
  fh = await fs.promises.open(file, "r");
131
136
  const buf = Buffer.alloc(HEADER_SCAN_BYTES);
132
137
  const { bytesRead } = await fh.read(buf, 0, HEADER_SCAN_BYTES, 0);
133
- const head = buf
134
- .toString("utf8", 0, bytesRead)
135
- .split("\n")
136
- .slice(0, HEADER_SCAN_LINES)
137
- .join("\n");
138
- return UTILS_FILE_HEADER_RE.test(head);
138
+ return hasUtilsHeaderContent(buf.toString("utf8", 0, bytesRead));
139
139
  }
140
140
  catch (err) {
141
141
  // A missing file is the common, silent case. Anything else means a file exists
@@ -1,13 +1,21 @@
1
1
  import { type UtilsLanguageSpec } from "./language-spec.js";
2
2
  import { type HelperSleep } from "./fixed-sleep.js";
3
+ import { type SourceSpan } from "../pom-scope/strip.js";
3
4
  export interface HelperAssertion {
4
5
  line: number;
5
6
  text: string;
6
7
  }
7
8
  export interface UtilsHelper {
8
9
  name: string;
9
- /** 1-based line of the definition. */
10
+ /** 1-based line of the definition — the `def`/`function` line, which is where a
11
+ * finding is reported. */
10
12
  line: number;
13
+ /** 1-based line where the definition's SOURCE begins: the first line of its
14
+ * decorator stack, or `line` when it has none. A decorator is part of what the
15
+ * helper does — `@retry(3)` against `@retry(1)` changes every call site — so a
16
+ * reader comparing implementations has to start here, while a reader pointing at
17
+ * the helper still uses `line`. */
18
+ startLine: number;
11
19
  /** 1-based line of the last body line (inclusive). */
12
20
  endLine: number;
13
21
  method?: string;
@@ -104,6 +112,10 @@ export declare function parseUtilsFile(content: string, spec: UtilsLanguageSpec)
104
112
  /** True when the `path` value is a single string literal ending the argument — a
105
113
  * concatenation (`"/api/orders/" + str(id) + "/items"`) has no route this can name. */
106
114
  export declare function isSingleLiteral(line: string, m: RegExpExecArray): boolean;
115
+ /** The string literals in `src`, quotes included, in order. TS/JS go through the
116
+ * shared scanner; Python goes through the one below, so a caller sees one definition
117
+ * of a literal for each language. */
118
+ export declare function stringSpansOf(src: string, spec: UtilsLanguageSpec): SourceSpan[];
107
119
  /** Comments and string CONTENTS blanked (quotes kept, line count preserved). Python
108
120
  * comments are `#…`; TS/JS go through the shared stripper. */
109
121
  /** String CONTENTS blanked (quotes kept, comments kept, line count preserved). */