@skyramp/mcp 0.3.1 → 0.3.2-rc.pom-2

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 (177) hide show
  1. package/build/index.js +2 -1
  2. package/build/prompts/code-reuse.d.ts +7 -1
  3. package/build/prompts/code-reuse.js +8 -5
  4. package/build/prompts/code-reuse.test.d.ts +1 -0
  5. package/build/prompts/code-reuse.test.js +62 -0
  6. package/build/prompts/pom-aware-code-reuse.d.ts +6 -1
  7. package/build/prompts/pom-aware-code-reuse.js +100 -53
  8. package/build/prompts/pom-aware-code-reuse.test.d.ts +1 -0
  9. package/build/prompts/pom-aware-code-reuse.test.js +11 -0
  10. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +4 -2
  11. package/build/prompts/test-recommendation/diffExecutionPlan.js +11 -65
  12. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +2 -2
  13. package/build/prompts/test-recommendation/recommendationSections.js +5 -2
  14. package/build/prompts/test-recommendation/scopeAssessment.js +1 -1
  15. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +26 -1
  16. package/build/prompts/test-recommendation/test-recommendation-prompt.js +68 -56
  17. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +48 -11
  18. package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
  19. package/build/prompts/testbot/testbot-prompts.js +67 -21
  20. package/build/prompts/testbot/testbot-prompts.test.js +44 -0
  21. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +7 -0
  22. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +71 -0
  23. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +1 -0
  24. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +75 -0
  25. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +7 -0
  26. package/build/recommendation/budgeters/fixedNBudgeter.js +11 -0
  27. package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +1 -0
  28. package/build/recommendation/budgeters/fixedNBudgeter.test.js +66 -0
  29. package/build/recommendation/budgeters/shared.d.ts +19 -0
  30. package/build/recommendation/budgeters/shared.js +66 -0
  31. package/build/recommendation/discriminators.d.ts +31 -0
  32. package/build/recommendation/discriminators.js +355 -0
  33. package/build/recommendation/discriminators.test.d.ts +1 -0
  34. package/build/recommendation/discriminators.test.js +324 -0
  35. package/build/recommendation/diversity.d.ts +47 -0
  36. package/build/recommendation/diversity.js +101 -0
  37. package/build/recommendation/diversity.test.d.ts +1 -0
  38. package/build/recommendation/diversity.test.js +77 -0
  39. package/build/recommendation/planRanker.d.ts +50 -0
  40. package/build/recommendation/planRanker.js +67 -0
  41. package/build/recommendation/planRanker.test.d.ts +1 -0
  42. package/build/recommendation/planRanker.test.js +110 -0
  43. package/build/recommendation/testFixtures.d.ts +25 -0
  44. package/build/recommendation/testFixtures.js +45 -0
  45. package/build/resources/testbotResource.js +4 -1
  46. package/build/services/ScenarioGenerationService.d.ts +5 -0
  47. package/build/services/ScenarioGenerationService.js +16 -1
  48. package/build/services/ScenarioGenerationService.test.js +44 -0
  49. package/build/services/TestExecutionService.d.ts +15 -1
  50. package/build/services/TestExecutionService.js +210 -55
  51. package/build/services/TestExecutionService.test.js +397 -0
  52. package/build/services/TestGenerationService.js +19 -1
  53. package/build/services/TestGenerationService.test.js +58 -0
  54. package/build/tool-phases.js +1 -0
  55. package/build/toolNames.d.ts +19 -0
  56. package/build/toolNames.js +19 -0
  57. package/build/tools/code-refactor/codeReuseTool.d.ts +7 -0
  58. package/build/tools/code-refactor/codeReuseTool.js +130 -4
  59. package/build/tools/code-refactor/codeReuseTool.test.d.ts +1 -0
  60. package/build/tools/code-refactor/codeReuseTool.test.js +290 -0
  61. package/build/tools/executeSkyrampTestTool.js +8 -2
  62. package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +6 -1
  63. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +110 -17
  64. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +147 -0
  65. package/build/tools/generate-tests/generateContractRestTool.js +11 -1
  66. package/build/tools/generate-tests/generateIntegrationRestTool.js +24 -1
  67. package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +1 -0
  68. package/build/tools/generate-tests/generateIntegrationRestTool.test.js +159 -0
  69. package/build/tools/generate-tests/planGuard.d.ts +13 -0
  70. package/build/tools/generate-tests/planGuard.js +78 -0
  71. package/build/tools/generate-tests/planGuard.test.d.ts +1 -0
  72. package/build/tools/generate-tests/planGuard.test.js +185 -0
  73. package/build/tools/generate-tests/scenarioFileIdentity.d.ts +10 -0
  74. package/build/tools/generate-tests/scenarioFileIdentity.js +46 -0
  75. package/build/tools/generate-tests/scenarioLint.d.ts +30 -0
  76. package/build/tools/generate-tests/scenarioLint.js +150 -0
  77. package/build/tools/generate-tests/scenarioLint.test.d.ts +1 -0
  78. package/build/tools/generate-tests/scenarioLint.test.js +100 -0
  79. package/build/tools/submitReportTool.js +78 -0
  80. package/build/tools/submitReportTool.test.js +255 -0
  81. package/build/tools/test-management/analyzeChangesTool.js +55 -2
  82. package/build/tools/test-management/analyzeChangesTool.test.js +12 -0
  83. package/build/tools/test-management/index.d.ts +1 -0
  84. package/build/tools/test-management/index.js +1 -0
  85. package/build/tools/test-management/registerTestPlanTool.d.ts +2 -0
  86. package/build/tools/test-management/registerTestPlanTool.js +329 -0
  87. package/build/tools/test-management/registerTestPlanTool.test.d.ts +1 -0
  88. package/build/tools/test-management/registerTestPlanTool.test.js +296 -0
  89. package/build/types/Recommendation.d.ts +97 -0
  90. package/build/types/Recommendation.js +48 -0
  91. package/build/types/RepositoryAnalysis.d.ts +14 -14
  92. package/build/types/TestExecution.d.ts +2 -0
  93. package/build/types/TestRecommendation.d.ts +12 -1
  94. package/build/types/TestRecommendation.js +26 -11
  95. package/build/types/TestTypes.js +1 -1
  96. package/build/utils/AnalysisStateManager.d.ts +47 -0
  97. package/build/utils/docker.test.js +1 -1
  98. package/build/utils/planMatchKeys.d.ts +61 -0
  99. package/build/utils/planMatchKeys.js +125 -0
  100. package/build/utils/pom-scope/import-expansion.d.ts +5 -0
  101. package/build/utils/pom-scope/import-expansion.js +32 -0
  102. package/build/utils/pom-scope/index.d.ts +39 -0
  103. package/build/utils/pom-scope/index.js +120 -0
  104. package/build/utils/pom-scope/index.test.d.ts +1 -0
  105. package/build/utils/pom-scope/index.test.js +239 -0
  106. package/build/utils/pom-scope/pom-files.d.ts +3 -0
  107. package/build/utils/pom-scope/pom-files.js +48 -0
  108. package/build/utils/pom-scope/pom-files.test.d.ts +1 -0
  109. package/build/utils/pom-scope/pom-files.test.js +29 -0
  110. package/build/utils/pom-scope/scoring.d.ts +20 -0
  111. package/build/utils/pom-scope/scoring.js +45 -0
  112. package/build/utils/pom-scope/scoring.test.d.ts +1 -0
  113. package/build/utils/pom-scope/scoring.test.js +39 -0
  114. package/build/utils/pom-scope/selector-extractor.d.ts +7 -0
  115. package/build/utils/pom-scope/selector-extractor.js +57 -0
  116. package/build/utils/pom-scope/selector-extractor.test.d.ts +1 -0
  117. package/build/utils/pom-scope/selector-extractor.test.js +67 -0
  118. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.d.ts +5 -0
  119. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.js +5 -0
  120. package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.d.ts +5 -0
  121. package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.js +9 -0
  122. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.d.ts +4 -0
  123. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.js +4 -0
  124. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.d.ts +4 -0
  125. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.js +4 -0
  126. package/build/utils/pom-verify/bindings.d.ts +19 -0
  127. package/build/utils/pom-verify/bindings.js +161 -0
  128. package/build/utils/pom-verify/bindings.test.d.ts +1 -0
  129. package/build/utils/pom-verify/bindings.test.js +164 -0
  130. package/build/utils/pom-verify/calls.d.ts +16 -0
  131. package/build/utils/pom-verify/calls.js +42 -0
  132. package/build/utils/pom-verify/calls.test.d.ts +1 -0
  133. package/build/utils/pom-verify/calls.test.js +61 -0
  134. package/build/utils/pom-verify/index.d.ts +4 -0
  135. package/build/utils/pom-verify/index.js +4 -0
  136. package/build/utils/pom-verify/resolve.d.ts +7 -0
  137. package/build/utils/pom-verify/resolve.js +27 -0
  138. package/build/utils/pom-verify/resolve.test.d.ts +1 -0
  139. package/build/utils/pom-verify/resolve.test.js +68 -0
  140. package/build/utils/pom-verify/strip.d.ts +9 -0
  141. package/build/utils/pom-verify/strip.js +89 -0
  142. package/build/utils/pom-verify/verify.d.ts +14 -0
  143. package/build/utils/pom-verify/verify.js +158 -0
  144. package/build/utils/pom-verify/verify.test.d.ts +1 -0
  145. package/build/utils/pom-verify/verify.test.js +325 -0
  146. package/build/utils/reportVerification.d.ts +61 -0
  147. package/build/utils/reportVerification.js +104 -0
  148. package/build/utils/reportVerification.test.d.ts +1 -0
  149. package/build/utils/reportVerification.test.js +185 -0
  150. package/build/utils/scenarioDrafting.js +5 -5
  151. package/build/utils/versions.d.ts +3 -3
  152. package/build/utils/versions.js +1 -1
  153. package/build/utils/workspaceAuth.d.ts +9 -1
  154. package/build/utils/workspaceAuth.js +25 -5
  155. package/build/utils/workspaceAuth.test.js +48 -0
  156. package/build/workspace/workspace.d.ts +20 -0
  157. package/build/workspace/workspace.js +4 -0
  158. package/build/workspace/workspace.test.js +10 -0
  159. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +77 -8
  160. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  161. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  162. package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +101 -0
  163. package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-aszq5EdG.js → codeMirrorModule-Bzd72-bG.js} +1 -1
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-BxS7Jm4s.js → defaultSettingsView-DzxTioTK.js} +101 -101
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.D4JTTy4R.js → index.BGc30U3S.js} +1 -1
  167. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  168. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.DaRMQKOI.js → uiMode.IaDrb29A.js} +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  170. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  171. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  172. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  173. package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +131 -0
  174. package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
  175. package/node_modules/playwright/package.json +1 -1
  176. package/package.json +3 -3
  177. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
@@ -0,0 +1,61 @@
1
+ import { extractPomCalls } from "./calls.js";
2
+ describe("extractPomCalls -- oo-style", () => {
3
+ const loginBinding = { kind: "instance", name: "login", classFile: "/fake/loginPage.js" };
4
+ const fnBinding = { kind: "function", name: "openNavFlyout", sourceFile: "/fake/commonActions.js" };
5
+ it("first-hop method call -> strict:true", () => {
6
+ const spec = `await login.signInAs(user);`;
7
+ expect(extractPomCalls(spec, [loginBinding])).toEqual([{ binding: "login", chain: ["signInAs"], strict: true }]);
8
+ });
9
+ it("property-only use (e.g. inside expect()) counts as a call too", () => {
10
+ const spec = `expect(login.toast).toBeVisible();`;
11
+ expect(extractPomCalls(spec, [loginBinding])).toEqual([{ binding: "login", chain: ["toast"], strict: true }]);
12
+ });
13
+ it("chains beyond the first hop are strict:false", () => {
14
+ const spec = `await login.signIn.click();`;
15
+ expect(extractPomCalls(spec, [loginBinding])).toEqual([{ binding: "login", chain: ["signIn", "click"], strict: false }]);
16
+ });
17
+ it("a free-function binding's direct call has an empty chain and is strict", () => {
18
+ const spec = `await openNavFlyout(page);`;
19
+ expect(extractPomCalls(spec, [fnBinding])).toEqual([{ binding: "openNavFlyout", chain: [], strict: true }]);
20
+ });
21
+ it("dedupes identical (binding, chain) occurrences", () => {
22
+ const spec = `
23
+ await login.signIn.click();
24
+ await login.signIn.click();
25
+ `;
26
+ expect(extractPomCalls(spec, [loginBinding])).toEqual([{ binding: "login", chain: ["signIn", "click"], strict: false }]);
27
+ });
28
+ it("a page-manager-style dotted binding name doesn't double-count against a shorter binding sharing its suffix", () => {
29
+ const pmLoginBinding = { kind: "instance", name: "pm.loginPage", classFile: "/fake/loginPage.js" };
30
+ const plainLoginBinding = { kind: "instance", name: "loginPage", classFile: "/fake/loginPage.js" };
31
+ const spec = `await pm.loginPage.signIn.click();`;
32
+ const calls = extractPomCalls(spec, [pmLoginBinding, plainLoginBinding]);
33
+ expect(calls).toEqual([{ binding: "pm.loginPage", chain: ["signIn", "click"], strict: false }]);
34
+ });
35
+ it("does not let quotes inside regex literals swallow real code", () => {
36
+ const spec = `page.getByText(/Don't have an account/).click();\nawait login.signIn.click();\nawait login.logout();`;
37
+ const calls = extractPomCalls(spec, [{ kind: "instance", name: "login", classFile: "/x/login.js" }]);
38
+ expect(calls).toEqual(expect.arrayContaining([
39
+ { binding: "login", chain: ["signIn", "click"], strict: false },
40
+ { binding: "login", chain: ["logout"], strict: true },
41
+ ]));
42
+ });
43
+ it("ignores calls appearing in comments and string/template literals", () => {
44
+ const spec = `// setup: login.signIn.click() manually
45
+ /* login.signIn.click() */
46
+ const doc = "call login.signIn.click()";
47
+ const t = \`use login.signIn.click()\`;
48
+ login.realCall();`;
49
+ const calls = extractPomCalls(spec, [{ kind: "instance", name: "login", classFile: "/x/login.js" }]);
50
+ expect(calls).toEqual([{ binding: "login", chain: ["realCall"], strict: true }]);
51
+ });
52
+ });
53
+ describe("extractPomCalls -- af-style", () => {
54
+ it("a deep component-property chain (list.footer.okBtn) is strict:false", () => {
55
+ const listBinding = { kind: "instance", name: "list", classFile: "/fake/asset-list.page.ts" };
56
+ const spec = `await list.footer.okBtn.click();`;
57
+ expect(extractPomCalls(spec, [listBinding])).toEqual([
58
+ { binding: "list", chain: ["footer", "okBtn", "click"], strict: false },
59
+ ]);
60
+ });
61
+ });
@@ -0,0 +1,4 @@
1
+ export * from "./bindings.js";
2
+ export * from "./calls.js";
3
+ export * from "./resolve.js";
4
+ export * from "./verify.js";
@@ -0,0 +1,4 @@
1
+ export * from "./bindings.js";
2
+ export * from "./calls.js";
3
+ export * from "./resolve.js";
4
+ export * from "./verify.js";
@@ -0,0 +1,7 @@
1
+ import type { PomBinding } from "./bindings.js";
2
+ /** Greps `member` (as its own token, e.g. followed by `(`, `:`, `=`, or a plain word boundary) in
3
+ * the binding's class/source file and its one-hop relative imports that are themselves POM files. */
4
+ export declare function resolveMemberSource(binding: PomBinding, member: string, pomFiles: string[]): Promise<{
5
+ found: boolean;
6
+ searched: string[];
7
+ }>;
@@ -0,0 +1,27 @@
1
+ import { readFile } from "fs/promises";
2
+ import { expandImports } from "../pom-scope/import-expansion.js";
3
+ import { stripCommentsAndStrings } from "./strip.js";
4
+ function escapeRegExp(s) {
5
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6
+ }
7
+ /** Greps `member` (as its own token, e.g. followed by `(`, `:`, `=`, or a plain word boundary) in
8
+ * the binding's class/source file and its one-hop relative imports that are themselves POM files. */
9
+ export async function resolveMemberSource(binding, member, pomFiles) {
10
+ const primaryFile = binding.kind === "instance" ? binding.classFile : binding.sourceFile;
11
+ const candidates = await expandImports([primaryFile], pomFiles);
12
+ const memberRe = new RegExp(`\\b${escapeRegExp(member)}\\b`);
13
+ const searched = [];
14
+ for (const { file } of candidates) {
15
+ searched.push(file);
16
+ let content;
17
+ try {
18
+ content = stripCommentsAndStrings(await readFile(file, "utf8"));
19
+ }
20
+ catch {
21
+ continue;
22
+ }
23
+ if (memberRe.test(content))
24
+ return { found: true, searched };
25
+ }
26
+ return { found: false, searched };
27
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { resolveMemberSource } from "./resolve.js";
6
+ const OO = path.join(path.dirname(fileURLToPath(import.meta.url)), "__fixtures__", "oo-style");
7
+ const AF = path.join(path.dirname(fileURLToPath(import.meta.url)), "__fixtures__", "af-style");
8
+ const OO_POM_FILES = [
9
+ path.join(OO, "page-manager.js"),
10
+ path.join(OO, "loginPage.js"),
11
+ path.join(OO, "widgetsPage.js"),
12
+ path.join(OO, "widgetHelpers.js"),
13
+ path.join(OO, "commonActions.js"),
14
+ ];
15
+ const AF_POM_FILES = [
16
+ path.join(AF, "asset-list.page.ts"),
17
+ path.join(AF, "workflow-footer.page.ts"),
18
+ path.join(AF, "report.iframe.page.ts"),
19
+ ];
20
+ describe("resolveMemberSource -- oo-style", () => {
21
+ it("finds a member defined directly in the binding's own class file", async () => {
22
+ const binding = { kind: "instance", name: "login", classFile: path.join(OO, "loginPage.js") };
23
+ const result = await resolveMemberSource(binding, "signInAs", OO_POM_FILES);
24
+ expect(result).toEqual({ found: true, searched: [path.join(OO, "loginPage.js")] });
25
+ });
26
+ it("falls through to a one-hop imported submodule when the member isn't in the class file itself", async () => {
27
+ // "dragWidget" is defined only in widgetHelpers.js -- widgetsPage.js neither imports nor
28
+ // mentions it, so a hit here can only come from the one-hop expansion, not the primary file.
29
+ const binding = { kind: "instance", name: "widgets", classFile: path.join(OO, "widgetsPage.js") };
30
+ const result = await resolveMemberSource(binding, "dragWidget", OO_POM_FILES);
31
+ expect(result.found).toBe(true);
32
+ expect(result.searched).toEqual([path.join(OO, "widgetsPage.js"), path.join(OO, "widgetHelpers.js")]);
33
+ });
34
+ it("resolves a free-function binding directly in its source file", async () => {
35
+ const binding = { kind: "function", name: "openNavFlyout", sourceFile: path.join(OO, "commonActions.js") };
36
+ const result = await resolveMemberSource(binding, "openNavFlyout", OO_POM_FILES);
37
+ expect(result).toEqual({ found: true, searched: [path.join(OO, "commonActions.js")] });
38
+ });
39
+ it("reports not-found with the full searched list when the member exists nowhere", async () => {
40
+ const binding = { kind: "instance", name: "login", classFile: path.join(OO, "loginPage.js") };
41
+ const result = await resolveMemberSource(binding, "doesNotExistAnywhere", OO_POM_FILES);
42
+ expect(result.found).toBe(false);
43
+ expect(result.searched).toContain(path.join(OO, "loginPage.js"));
44
+ });
45
+ });
46
+ describe("resolveMemberSource -- comment stripping", () => {
47
+ it("does not count a member name that appears only in a comment", async () => {
48
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pom-resolve-comment-"));
49
+ try {
50
+ const file = path.join(dir, "widgetsPage.js");
51
+ fs.writeFileSync(file, `// TODO: add dragWidget\nexport class WidgetsPage {}\n`);
52
+ const binding = { kind: "instance", name: "widgets", classFile: file };
53
+ const result = await resolveMemberSource(binding, "dragWidget", [file]);
54
+ expect(result).toEqual({ found: false, searched: [file] });
55
+ }
56
+ finally {
57
+ fs.rmSync(dir, { recursive: true, force: true });
58
+ }
59
+ });
60
+ });
61
+ describe("resolveMemberSource -- af-style", () => {
62
+ it("finds a component-chain member via the one-hop imported component file", async () => {
63
+ const binding = { kind: "instance", name: "list", classFile: path.join(AF, "asset-list.page.ts") };
64
+ const result = await resolveMemberSource(binding, "okBtn", AF_POM_FILES);
65
+ expect(result.found).toBe(true);
66
+ expect(result.searched).toEqual([path.join(AF, "asset-list.page.ts"), path.join(AF, "workflow-footer.page.ts")]);
67
+ });
68
+ });
@@ -0,0 +1,9 @@
1
+ /** Blanks line/block comments only, preserving string/template literal contents (e.g. so an
2
+ * import specifier's quoted path stays readable). Each blanked character becomes a space
3
+ * (newlines preserved) so remaining code characters keep their original offset. */
4
+ export declare function stripComments(src: string): string;
5
+ /** Blanks string/template literal contents only, preserving comments as-is. */
6
+ export declare function stripStrings(src: string): string;
7
+ /** Blanks both comments and string/template literals -- for scanners (like call-chain matching)
8
+ * that never need to read quoted text and want prose in either kind fully inert. */
9
+ export declare function stripCommentsAndStrings(src: string): string;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Single lexer-lite pass over the source classifying every line comment, block comment, and
3
+ * string/template literal span (escaped quotes handled; a template literal's `${...}`
4
+ * interpolation is treated as part of the literal, not parsed separately). Callers blank out
5
+ * whichever kinds they need -- comments only, strings only, or both -- without re-scanning.
6
+ */
7
+ function classifySpans(src) {
8
+ const spans = [];
9
+ let i = 0;
10
+ const n = src.length;
11
+ while (i < n) {
12
+ const c = src[i];
13
+ const c2 = i + 1 < n ? src[i + 1] : "";
14
+ if (c === "/" && c2 === "/") {
15
+ const start = i;
16
+ while (i < n && src[i] !== "\n")
17
+ i++;
18
+ spans.push({ kind: "comment", start, end: i });
19
+ continue;
20
+ }
21
+ if (c === "/" && c2 === "*") {
22
+ const start = i;
23
+ i += 2;
24
+ while (i < n && !(src[i] === "*" && i + 1 < n && src[i + 1] === "/"))
25
+ i++;
26
+ i = Math.min(i + 2, n);
27
+ spans.push({ kind: "comment", start, end: i });
28
+ continue;
29
+ }
30
+ if (c === "'" || c === '"' || c === "`") {
31
+ const quote = c;
32
+ const start = i;
33
+ i++;
34
+ let brokeOnNewline = false;
35
+ while (i < n) {
36
+ if (src[i] === "\\" && i + 1 < n) {
37
+ i += 2;
38
+ continue;
39
+ }
40
+ if (src[i] === quote) {
41
+ i++;
42
+ break;
43
+ }
44
+ // A plain '...' or "..." string can't legally span a raw newline (only backtick templates
45
+ // can). A quote char that never closes before a newline is almost always a stray apostrophe
46
+ // (e.g. inside a regex literal: /Don't have an account/) rather than a real string opener --
47
+ // treat it as plain code instead of blanking everything up to the next matching quote.
48
+ if (src[i] === "\n" && quote !== "`") {
49
+ brokeOnNewline = true;
50
+ break;
51
+ }
52
+ i++;
53
+ }
54
+ if (brokeOnNewline) {
55
+ i = start + 1;
56
+ continue;
57
+ }
58
+ spans.push({ kind: "string", start, end: i });
59
+ continue;
60
+ }
61
+ i++;
62
+ }
63
+ return spans;
64
+ }
65
+ function blankSpans(src, spans) {
66
+ if (spans.length === 0)
67
+ return src;
68
+ const out = src.split("");
69
+ for (const { start, end } of spans) {
70
+ for (let i = start; i < end; i++)
71
+ out[i] = src[i] === "\n" ? "\n" : " ";
72
+ }
73
+ return out.join("");
74
+ }
75
+ /** Blanks line/block comments only, preserving string/template literal contents (e.g. so an
76
+ * import specifier's quoted path stays readable). Each blanked character becomes a space
77
+ * (newlines preserved) so remaining code characters keep their original offset. */
78
+ export function stripComments(src) {
79
+ return blankSpans(src, classifySpans(src).filter((s) => s.kind === "comment"));
80
+ }
81
+ /** Blanks string/template literal contents only, preserving comments as-is. */
82
+ export function stripStrings(src) {
83
+ return blankSpans(src, classifySpans(src).filter((s) => s.kind === "string"));
84
+ }
85
+ /** Blanks both comments and string/template literals -- for scanners (like call-chain matching)
86
+ * that never need to read quoted text and want prose in either kind fully inert. */
87
+ export function stripCommentsAndStrings(src) {
88
+ return blankSpans(src, classifySpans(src));
89
+ }
@@ -0,0 +1,14 @@
1
+ export type ReuseViolation = {
2
+ binding: string;
3
+ member: string;
4
+ searched: string[];
5
+ availableMembers: string[];
6
+ };
7
+ export type VerifyResult = {
8
+ ok: boolean;
9
+ violations: ReuseViolation[];
10
+ unverifiable: string[];
11
+ parseError?: string;
12
+ checkedCalls: number;
13
+ };
14
+ export declare function verifyReuse(testFile: string, language: string): Promise<VerifyResult>;
@@ -0,0 +1,158 @@
1
+ import { execFile } from "child_process";
2
+ import { readFile } from "fs/promises";
3
+ import * as path from "path";
4
+ import { promisify } from "util";
5
+ import { findRoot } from "../pom-scope/index.js";
6
+ import { globPomFiles, globAllSourceFiles } from "../pom-scope/pom-files.js";
7
+ import { extractPomBindings } from "./bindings.js";
8
+ import { extractPomCalls } from "./calls.js";
9
+ import { resolveMemberSource } from "./resolve.js";
10
+ import { stripComments, stripCommentsAndStrings } from "./strip.js";
11
+ const pExecFile = promisify(execFile);
12
+ const PAGE_MANAGER_RE = /^page-?manager\.(ts|js)$/i;
13
+ const MAX_PARSE_ERROR_LEN = 500;
14
+ const NODE_CHECK_TIMEOUT_MS = 10_000;
15
+ /**
16
+ * Module-private, deliberately rough member lister: strips comments/strings then greps three
17
+ * simple patterns (leading-of-line function decls, `this.member =` assignments, getters). This is
18
+ * LLM-evidence quality by design -- a plausible-members list for a violation report, NOT a parser,
19
+ * so it will miss real members (e.g. a method declared mid-line in minified/compact source) and
20
+ * that's an accepted tradeoff, not a bug to fix.
21
+ */
22
+ async function listMembers(file) {
23
+ let content;
24
+ try {
25
+ content = stripCommentsAndStrings(await readFile(file, "utf8"));
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ const members = new Set();
31
+ for (const re of [/^\s*(?:async\s+)?(\w+)\s*\(/gm, /this\.(\w+)\s*=/g, /get\s+(\w+)\s*\(/g]) {
32
+ for (const m of content.matchAll(re))
33
+ members.add(m[1]);
34
+ }
35
+ return [...members];
36
+ }
37
+ function primaryFile(binding) {
38
+ return binding.kind === "instance" ? binding.classFile : binding.sourceFile;
39
+ }
40
+ /** Reads `compilerOptions.paths` out of a tsconfig/jsconfig JSON string. Tolerates JSONC comments
41
+ * and trailing commas via a strip-then-parse pass; falls back to a bounded regex scan of the
42
+ * `"paths"` block if that still doesn't parse. Never throws -- returns undefined on any failure. */
43
+ function extractCompilerPaths(raw) {
44
+ const tryParse = (text) => {
45
+ try {
46
+ const parsed = JSON.parse(text);
47
+ return parsed?.compilerOptions?.paths;
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ };
53
+ const direct = tryParse(raw);
54
+ if (direct)
55
+ return direct;
56
+ const relaxed = tryParse(stripComments(raw).replace(/,(\s*[}\]])/g, "$1"));
57
+ if (relaxed)
58
+ return relaxed;
59
+ // Last-resort bounded regex scan, scoped to the paths block if one is findable.
60
+ try {
61
+ const pathsBlock = raw.match(/"paths"\s*:\s*\{([\s\S]*?)\}/)?.[1] ?? raw;
62
+ const result = {};
63
+ for (const m of pathsBlock.matchAll(/"([^"]+)"\s*:\s*\[\s*"([^"]+)"/g))
64
+ result[m[1]] = [m[2]];
65
+ return Object.keys(result).length > 0 ? result : undefined;
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ }
71
+ /** Builds a prefix -> absolute-dir alias map from `<root>/tsconfig.json` and `<root>/jsconfig.json`
72
+ * `compilerOptions.paths` entries of the shape `"@x/*": ["./src/y/*"]`. Never throws. */
73
+ async function loadAliasMap(root) {
74
+ let aliasMap;
75
+ for (const fname of ["tsconfig.json", "jsconfig.json"]) {
76
+ let raw;
77
+ try {
78
+ raw = await readFile(path.join(root, fname), "utf8");
79
+ }
80
+ catch {
81
+ continue;
82
+ }
83
+ const paths = extractCompilerPaths(raw);
84
+ if (!paths)
85
+ continue;
86
+ for (const [key, targets] of Object.entries(paths)) {
87
+ const target = targets?.[0];
88
+ if (typeof target !== "string")
89
+ continue;
90
+ if (!target)
91
+ continue;
92
+ aliasMap ??= {};
93
+ aliasMap[key.replace(/\/\*$/, "")] = path.resolve(root, target.replace(/\/\*$/, ""));
94
+ }
95
+ }
96
+ return aliasMap;
97
+ }
98
+ export async function verifyReuse(testFile, language) {
99
+ const spec = await readFile(testFile, "utf8"); // throws on unreadable testFile -- caller handles
100
+ const root = await findRoot(path.dirname(testFile));
101
+ let pomFiles = await globPomFiles(root);
102
+ if (pomFiles.length === 0)
103
+ pomFiles = await globAllSourceFiles(root);
104
+ const pageManagerFile = pomFiles.find((f) => PAGE_MANAGER_RE.test(path.basename(f)));
105
+ const aliasMap = await loadAliasMap(root);
106
+ const { bindings, unrecognized } = await extractPomBindings(spec, {
107
+ specDir: path.dirname(testFile),
108
+ pomFiles,
109
+ pageManagerFile,
110
+ aliasMap,
111
+ });
112
+ const calls = extractPomCalls(spec, bindings);
113
+ const bindingsByName = new Map(bindings.map((b) => [b.name, b]));
114
+ const violations = [];
115
+ const unverifiable = [];
116
+ let checkedCalls = 0;
117
+ for (const call of calls) {
118
+ const binding = bindingsByName.get(call.binding);
119
+ if (!binding) {
120
+ // Shouldn't happen -- extractPomCalls only matches against the bindings it was given --
121
+ // but degrade to unverifiable rather than throw if the invariant ever breaks.
122
+ unverifiable.push([call.binding, ...call.chain].join("."));
123
+ continue;
124
+ }
125
+ if (!call.strict) {
126
+ unverifiable.push([call.binding, ...call.chain].join("."));
127
+ continue;
128
+ }
129
+ checkedCalls++;
130
+ // A free-function binding's direct call has an empty chain (see calls.ts) -- there's no
131
+ // member to hop to, so resolve the binding's own name against its source file instead.
132
+ const member = call.chain[0] ?? binding.name;
133
+ const { found, searched } = await resolveMemberSource(binding, member, pomFiles);
134
+ if (!found) {
135
+ violations.push({ binding: call.binding, member, searched, availableMembers: await listMembers(primaryFile(binding)) });
136
+ }
137
+ }
138
+ for (const u of unrecognized)
139
+ unverifiable.push(u);
140
+ let parseError;
141
+ if (language === "javascript") {
142
+ try {
143
+ await pExecFile("node", ["--check", testFile], { timeout: NODE_CHECK_TIMEOUT_MS });
144
+ }
145
+ catch (err) {
146
+ const e = err;
147
+ const stderr = e?.killed ? `node --check timed out after ${NODE_CHECK_TIMEOUT_MS}ms` : (e?.stderr ?? String(err));
148
+ parseError = stderr.slice(0, MAX_PARSE_ERROR_LEN);
149
+ }
150
+ }
151
+ return {
152
+ ok: violations.length === 0 && parseError === undefined,
153
+ violations,
154
+ unverifiable,
155
+ parseError,
156
+ checkedCalls,
157
+ };
158
+ }
@@ -0,0 +1 @@
1
+ export {};