@skyramp/mcp 0.4.2 → 0.4.3-rc.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 (135) 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/answers.d.ts +5 -3
  16. package/build/recommendation/answers.js +5 -2
  17. package/build/recommendation/fakeVerifyContext.d.ts +3 -0
  18. package/build/recommendation/fakeVerifyContext.js +16 -0
  19. package/build/recommendation/runVerifiers.js +2 -0
  20. package/build/recommendation/types.d.ts +12 -0
  21. package/build/recommendation/verifierContracts.d.ts +32 -2
  22. package/build/recommendation/verifierContracts.js +45 -4
  23. package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
  24. package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
  25. package/build/recommendation/verifiers/coverage.js +143 -4
  26. package/build/recommendation/verifiers/expectedValueSourced.js +8 -0
  27. package/build/recommendation/verifiers/requirementSourced.js +141 -11
  28. package/build/services/TestGenerationService.js +4 -0
  29. package/build/tools/code-refactor/caller-gate.d.ts +105 -0
  30. package/build/tools/code-refactor/caller-gate.js +174 -0
  31. package/build/tools/code-refactor/changed-helper-report.d.ts +62 -0
  32. package/build/tools/code-refactor/changed-helper-report.js +114 -0
  33. package/build/tools/code-refactor/codeReuseTool.js +131 -62
  34. package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -2
  35. package/build/tools/code-refactor/helper-callers.d.ts +65 -0
  36. package/build/tools/code-refactor/helper-callers.js +85 -0
  37. package/build/tools/code-refactor/modularizationTool.js +10 -0
  38. package/build/tools/code-refactor/pom-pass-state.d.ts +51 -0
  39. package/build/tools/code-refactor/pom-pass-state.js +115 -0
  40. package/build/tools/code-refactor/removed-modules.d.ts +33 -0
  41. package/build/tools/code-refactor/removed-modules.js +61 -0
  42. package/build/tools/code-refactor/retrofit-state.d.ts +3 -2
  43. package/build/tools/code-refactor/retrofit-state.js +11 -12
  44. package/build/tools/code-refactor/reuse-outcome.d.ts +33 -1
  45. package/build/tools/code-refactor/reuse-record-store.d.ts +57 -0
  46. package/build/tools/code-refactor/reuse-record-store.js +140 -0
  47. package/build/tools/code-refactor/reuse-state.d.ts +34 -9
  48. package/build/tools/code-refactor/reuse-state.js +140 -135
  49. package/build/tools/code-refactor/utils-verify-gates.js +42 -18
  50. package/build/tools/code-refactor/verify-gates.d.ts +4 -0
  51. package/build/tools/code-refactor/verify-gates.js +8 -1
  52. package/build/tools/generate-tests/generateContractRestTool.js +7 -7
  53. package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -8
  54. package/build/tools/generate-tests/generateUIRestTool.js +3 -3
  55. package/build/tools/submitReportTool.js +121 -30
  56. package/build/tools/test-management/actionsTool.js +7 -0
  57. package/build/tools/test-management/registerTestPlanTool.d.ts +1 -0
  58. package/build/tools/test-management/registerTestPlanTool.js +71 -23
  59. package/build/types/RepositoryAnalysis.d.ts +20 -20
  60. package/build/types/ReuseOutcome.d.ts +123 -0
  61. package/build/types/TestExecution.d.ts +11 -0
  62. package/build/types/TestExecution.js +19 -0
  63. package/build/types/index.d.ts +1 -1
  64. package/build/utils/branchDiff.d.ts +1 -1
  65. package/build/utils/branchDiff.js +1 -1
  66. package/build/utils/changedRuns.d.ts +13 -0
  67. package/build/utils/changedRuns.js +56 -0
  68. package/build/utils/featureFlags.d.ts +8 -10
  69. package/build/utils/featureFlags.js +23 -21
  70. package/build/utils/normalizeSkyrampImports.d.ts +4 -1
  71. package/build/utils/normalizeSkyrampImports.js +24 -17
  72. package/build/utils/pom-scope/ownership.d.ts +19 -0
  73. package/build/utils/pom-scope/ownership.js +31 -0
  74. package/build/utils/pom-scope/pom-files.d.ts +8 -0
  75. package/build/utils/pom-scope/pom-files.js +8 -0
  76. package/build/utils/pom-scope/scoring.js +13 -1
  77. package/build/utils/pom-scope/strip.d.ts +10 -0
  78. package/build/utils/pom-scope/strip.js +9 -0
  79. package/build/utils/pom-verify/verify.js +13 -2
  80. package/build/utils/reuseRouting.d.ts +30 -2
  81. package/build/utils/reuseRouting.js +36 -15
  82. package/build/utils/testDependencyPolicy.js +4 -16
  83. package/build/utils/utils-verify/action-sites.d.ts +2 -2
  84. package/build/utils/utils-verify/allow.d.ts +1 -1
  85. package/build/utils/utils-verify/allow.js +15 -0
  86. package/build/utils/utils-verify/body-reach.d.ts +2 -2
  87. package/build/utils/utils-verify/body-reach.js +4 -1
  88. package/build/utils/utils-verify/call-sites.d.ts +16 -6
  89. package/build/utils/utils-verify/call-sites.js +12 -6
  90. package/build/utils/utils-verify/changed-helpers.d.ts +38 -0
  91. package/build/utils/utils-verify/changed-helpers.js +91 -0
  92. package/build/utils/utils-verify/changed-lines.d.ts +48 -0
  93. package/build/utils/utils-verify/changed-lines.js +106 -0
  94. package/build/utils/utils-verify/head.d.ts +73 -0
  95. package/build/utils/utils-verify/head.js +156 -0
  96. package/build/utils/utils-verify/importers.d.ts +59 -10
  97. package/build/utils/utils-verify/importers.js +121 -31
  98. package/build/utils/utils-verify/in-house.js +26 -31
  99. package/build/utils/utils-verify/index.d.ts +3 -0
  100. package/build/utils/utils-verify/index.js +3 -0
  101. package/build/utils/utils-verify/language-spec.js +10 -2
  102. package/build/utils/utils-verify/locate.d.ts +3 -0
  103. package/build/utils/utils-verify/locate.js +6 -6
  104. package/build/utils/utils-verify/parse.d.ts +13 -1
  105. package/build/utils/utils-verify/parse.js +122 -49
  106. package/build/utils/utils-verify/retrofit-equivalence.d.ts +0 -5
  107. package/build/utils/utils-verify/retrofit-equivalence.js +2 -34
  108. package/build/utils/utils-verify/stage.js +4 -16
  109. package/build/utils/utils-verify/typecheck.d.ts +25 -4
  110. package/build/utils/utils-verify/typecheck.js +95 -13
  111. package/build/utils/utils-verify/verify.d.ts +33 -8
  112. package/build/utils/utils-verify/verify.js +180 -18
  113. package/build/utils/versions.d.ts +3 -3
  114. package/build/utils/versions.js +1 -1
  115. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1 -0
  116. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +1 -0
  117. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +6 -0
  118. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +4 -2
  119. package/node_modules/playwright/lib/mcp/skyramp/snapshotLocators.js +246 -0
  120. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +378 -237
  121. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +21 -0
  122. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  123. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-LNgEKtdV.js → codeMirrorModule-Bou8kKzE.js} +1 -1
  124. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-Bwr1eMKC.js → defaultSettingsView-DTp2-8Si.js} +3 -3
  125. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  126. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.BAkLd5DX.js → index.uiPTFncY.js} +1 -1
  127. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.BPopbasy.js → uiMode.GLqsx5cI.js} +1 -1
  128. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  129. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  130. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  131. package/node_modules/playwright/package.json +1 -1
  132. package/package.json +3 -3
  133. package/plugin/prompts/code-reuse/hand-off.md +13 -1
  134. package/plugin/prompts/plan-tests.md +2 -2
  135. package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -0
@@ -3,7 +3,19 @@ import { logger } from "./logger.js";
3
3
  const PLAYWRIGHT_IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*['"]@playwright\/test['"]\s*;?[ \t]*\r?\n?/g;
4
4
  const SKYRAMP_IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*['"]@skyramp\/skyramp['"]\s*;?/;
5
5
  const EXPECT_USAGE_RE = /\bexpect\s*\(/;
6
- const LAST_IMPORT_RE = /^import\s[^;]*;[ \t]*$/gm;
6
+ const NAMED_IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g;
7
+ /** True when a module other than @playwright/test or @skyramp/skyramp imports
8
+ * `expect` (e.g. a repository's own harness); moving it would break that file. */
9
+ function expectFromOtherModule(source) {
10
+ for (const [, raw, from] of source.matchAll(NAMED_IMPORT_RE)) {
11
+ if (from === "@playwright/test" || from === "@skyramp/skyramp")
12
+ continue;
13
+ const names = splitImports(raw).map((item) => item.split(/\s+as\s+/).pop());
14
+ if (names.includes("expect"))
15
+ return true;
16
+ }
17
+ return false;
18
+ }
7
19
  function splitImports(raw) {
8
20
  return raw
9
21
  .split(",")
@@ -13,10 +25,16 @@ function splitImports(raw) {
13
25
  /**
14
26
  * Ensure `expect` is imported from `@skyramp/skyramp` rather than `@playwright/test`.
15
27
  * - Strips `expect` from any `@playwright/test` import (keeps other identifiers).
16
- * - If the file uses `expect(...)`, ensures an `@skyramp/skyramp` import carries `expect`.
28
+ * - If the file uses `expect(...)`, ensures its `@skyramp/skyramp` import carries `expect`.
29
+ * A file with no `@skyramp/skyramp` import is not a Skyramp test (a repository's
30
+ * own spec), so it is left alone, as is a file that takes `expect` from another
31
+ * module.
17
32
  * Returns the original string if no rewrite was needed.
18
33
  */
19
34
  export function normalizeSkyrampExpectImport(source) {
35
+ if (!SKYRAMP_IMPORT_RE.test(source) || expectFromOtherModule(source)) {
36
+ return source;
37
+ }
20
38
  let out = source;
21
39
  out = out.replace(PLAYWRIGHT_IMPORT_RE, (match, raw) => {
22
40
  const items = splitImports(raw);
@@ -31,22 +49,11 @@ export function normalizeSkyrampExpectImport(source) {
31
49
  if (!EXPECT_USAGE_RE.test(out))
32
50
  return out;
33
51
  const skyrampMatch = out.match(SKYRAMP_IMPORT_RE);
34
- if (skyrampMatch) {
35
- const items = splitImports(skyrampMatch[1]);
36
- if (items.includes("expect"))
37
- return out;
38
- items.push("expect");
39
- out = out.replace(SKYRAMP_IMPORT_RE, `import { ${items.join(", ")} } from '@skyramp/skyramp';`);
52
+ const items = splitImports(skyrampMatch[1]);
53
+ if (items.includes("expect"))
40
54
  return out;
41
- }
42
- const newImport = `import { expect } from '@skyramp/skyramp';`;
43
- const imports = [...out.matchAll(LAST_IMPORT_RE)];
44
- const last = imports[imports.length - 1];
45
- if (last && last.index !== undefined) {
46
- const insertAt = last.index + last[0].length;
47
- return out.slice(0, insertAt) + "\n" + newImport + out.slice(insertAt);
48
- }
49
- return newImport + "\n" + out;
55
+ items.push("expect");
56
+ return out.replace(SKYRAMP_IMPORT_RE, `import { ${items.join(", ")} } from '@skyramp/skyramp';`);
50
57
  }
51
58
  /**
52
59
  * Normalize the `expect` import in a file on disk. No-op if the file is missing,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Which reuse path owns a repository module when both reuse settings are on: the
3
+ * page-object path owns a module that declares a class, the in-house helper path
4
+ * (utils-verify/in-house.ts) owns one that does not. ONE predicate, read by both
5
+ * sides, so a module is never claimed by both and never by neither.
6
+ *
7
+ * Decided by shape because the in-house parser cannot read a class — it reports
8
+ * one as unverifiable — while page-object detection also globs free-function
9
+ * helper libraries (`support/`, `testUtils`). Without this split both paths would
10
+ * work on the same free-function module in one run.
11
+ */
12
+ export declare function declaresClass(content: string): boolean;
13
+ /** The file's working-copy content decides; an unreadable file is not a page
14
+ * object (fails toward the in-house path, whose additive gate reads HEAD). */
15
+ export declare function isPageObjectModule(file: string): Promise<boolean>;
16
+ /** Keep only the candidates the page-object path owns. */
17
+ export declare function pageObjectCandidates<T extends {
18
+ file: string;
19
+ }>(candidates: T[]): Promise<T[]>;
@@ -0,0 +1,31 @@
1
+ import { readFile } from "fs/promises";
2
+ import { stripCommentsAndStrings } from "./strip.js";
3
+ /**
4
+ * Which reuse path owns a repository module when both reuse settings are on: the
5
+ * page-object path owns a module that declares a class, the in-house helper path
6
+ * (utils-verify/in-house.ts) owns one that does not. ONE predicate, read by both
7
+ * sides, so a module is never claimed by both and never by neither.
8
+ *
9
+ * Decided by shape because the in-house parser cannot read a class — it reports
10
+ * one as unverifiable — while page-object detection also globs free-function
11
+ * helper libraries (`support/`, `testUtils`). Without this split both paths would
12
+ * work on the same free-function module in one run.
13
+ */
14
+ export function declaresClass(content) {
15
+ return /(?:^|[\s;}])class\s+[A-Za-z_$][\w$]*/.test(stripCommentsAndStrings(content));
16
+ }
17
+ /** The file's working-copy content decides; an unreadable file is not a page
18
+ * object (fails toward the in-house path, whose additive gate reads HEAD). */
19
+ export async function isPageObjectModule(file) {
20
+ try {
21
+ return declaresClass(await readFile(file, "utf8"));
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ /** Keep only the candidates the page-object path owns. */
28
+ export async function pageObjectCandidates(candidates) {
29
+ const owned = await Promise.all(candidates.map((c) => isPageObjectModule(c.file)));
30
+ return candidates.filter((_, i) => owned[i]);
31
+ }
@@ -3,6 +3,14 @@
3
3
  * targets them.
4
4
  */
5
5
  export declare const SPEC_BASENAME: RegExp;
6
+ /**
7
+ * Skyramp's own shared helper module is never a page object, wherever it lives — a
8
+ * customer layout such as `tests/lib/skyrampUtils.ts` matches the test-tree helper
9
+ * glob above. These globs only LIST files, and the whole-repository fallback lists
10
+ * every source file, so the module is not recognised here: the two readers that
11
+ * open candidates anyway — `scorePoms` for discovery, `verifyReuse` for the files a
12
+ * spec binds — drop a file whose head carries the shared-module header.
13
+ */
6
14
  export declare function globPomFiles(root: string): Promise<string[]>;
7
15
  /**
8
16
  * Spec files anywhere under `root`, by basename rather than by directory.
@@ -34,6 +34,14 @@ const BUILD_IGNORE = [
34
34
  /** Test trees a POM is not expected to live in. Not skipped when the tests themselves are the subject. */
35
35
  const TEST_TREE_IGNORE = ["**/cypress/**", "**/__tests__/**"];
36
36
  const ALL_SOURCE_IGNORE = [...BUILD_IGNORE, ...TEST_TREE_IGNORE];
37
+ /**
38
+ * Skyramp's own shared helper module is never a page object, wherever it lives — a
39
+ * customer layout such as `tests/lib/skyrampUtils.ts` matches the test-tree helper
40
+ * glob above. These globs only LIST files, and the whole-repository fallback lists
41
+ * every source file, so the module is not recognised here: the two readers that
42
+ * open candidates anyway — `scorePoms` for discovery, `verifyReuse` for the files a
43
+ * spec binds — drop a file whose head carries the shared-module header.
44
+ */
37
45
  export async function globPomFiles(root) {
38
46
  const matches = await fg(POM_GLOBS, {
39
47
  cwd: root,
@@ -1,4 +1,6 @@
1
1
  import { readFile } from "fs/promises";
2
+ import { hasUtilsHeaderContent } from "../utils-verify/locate.js";
3
+ import { logger } from "../logger.js";
2
4
  export const RARE_DF = 5;
3
5
  /**
4
6
  * IDF-style scoring: each selector contributes 1/df to every file containing it
@@ -18,7 +20,17 @@ export async function scorePoms(selectors, files) {
18
20
  const contents = new Map();
19
21
  await Promise.all(files.map(async (f) => {
20
22
  try {
21
- contents.set(f, (await readFile(f, "utf8")).replace(/\\/g, ""));
23
+ // The whole file: the scoring loop below counts a selector wherever it
24
+ // appears in the content, so a partial read would change the scores.
25
+ const text = await readFile(f, "utf8");
26
+ // Skyramp's own shared module is never a page-object candidate, wherever the
27
+ // globs found it (see pom-files.ts). Judged on the first lines of the content
28
+ // the scorer holds anyway, so the check adds no read.
29
+ if (hasUtilsHeaderContent(text)) {
30
+ logger.info("Page-object scoring skipped a Skyramp shared module the globs listed", { file: f });
31
+ return;
32
+ }
33
+ contents.set(f, text.replace(/\\/g, ""));
22
34
  }
23
35
  catch {
24
36
  /* unreadable file: skip */
@@ -1,3 +1,8 @@
1
+ /** Half-open `[start, end)` offsets of a region in the source. */
2
+ export interface SourceSpan {
3
+ start: number;
4
+ end: number;
5
+ }
1
6
  /** Every line- and block-comment span in `src`, in source order. Exported for callers
2
7
  * that must decide whether a `//` OPENS a comment rather than sits inside one: a `//`
3
8
  * on its own line inside `/* … *\/` opens nothing, and deleting that line can take the
@@ -11,6 +16,11 @@ export declare function commentSpans(src: string): {
11
16
  * (newlines preserved) so remaining code characters keep their original offset. */
12
17
  export declare function stripComments(src: string): string;
13
18
  /** Blanks string/template literal contents only, preserving comments as-is. */
19
+ /** The string literals in TS/JS source, quotes INCLUDED, in order. The same scan the
20
+ * blanking helpers use — exported so a caller that must treat literal contents
21
+ * differently from code (rather than blank them) has one definition of where a
22
+ * literal starts and ends, not a second scanner of its own. */
23
+ export declare function stringSpans(src: string): SourceSpan[];
14
24
  export declare function stripStrings(src: string): string;
15
25
  /** Blanks both comments and string/template literals -- for scanners (like call-chain matching)
16
26
  * that never need to read quoted text and want prose in either kind fully inert. */
@@ -317,6 +317,15 @@ export function stripComments(src) {
317
317
  return blankSpans(src, classifySpans(src).filter((s) => s.kind === "comment"));
318
318
  }
319
319
  /** Blanks string/template literal contents only, preserving comments as-is. */
320
+ /** The string literals in TS/JS source, quotes INCLUDED, in order. The same scan the
321
+ * blanking helpers use — exported so a caller that must treat literal contents
322
+ * differently from code (rather than blank them) has one definition of where a
323
+ * literal starts and ends, not a second scanner of its own. */
324
+ export function stringSpans(src) {
325
+ return classifySpans(src)
326
+ .filter((s) => s.kind === "string")
327
+ .map(({ start, end }) => ({ start, end }));
328
+ }
320
329
  export function stripStrings(src) {
321
330
  return blankSpans(src, classifySpans(src).filter((s) => s.kind === "string"));
322
331
  }
@@ -4,7 +4,8 @@ import * as path from "path";
4
4
  import { promisify } from "util";
5
5
  import { findRoot } from "../pom-scope/index.js";
6
6
  import { globPomFiles, globAllSourceFiles } from "../pom-scope/pom-files.js";
7
- import { extractPomBindings } from "./bindings.js";
7
+ import { extractPomBindings, primaryFile } from "./bindings.js";
8
+ import { hasUtilsHeader } from "../utils-verify/locate.js";
8
9
  import { extractPomCalls } from "./calls.js";
9
10
  import { resolveMemberSource } from "./resolve.js";
10
11
  import { stripComments, stripCommentsAndStrings } from "../pom-scope/strip.js";
@@ -128,12 +129,22 @@ export async function verifyReuse(testFile, language) {
128
129
  pomFiles = await globAllSourceFiles(root);
129
130
  const pageManagerFile = pomFiles.find((f) => PAGE_MANAGER_RE.test(path.basename(f)));
130
131
  const aliasMap = await loadAliasMap(root);
131
- const { bindings, unrecognized } = await extractPomBindings(spec, {
132
+ const extracted = await extractPomBindings(spec, {
132
133
  specDir: path.dirname(testFile),
133
134
  pomFiles,
134
135
  pageManagerFile,
135
136
  aliasMap,
136
137
  });
138
+ // Skyramp's own shared module can sit inside a page-object glob (`tests/lib/`), and
139
+ // a call to one of its helpers must not count as page-object reuse. The header is
140
+ // read on the few files the spec binds, never across the glob: the whole-repository
141
+ // fallback above lists every source file. Dropping the binding is the whole change:
142
+ // `unrecognized` fills only from a `new` of an unresolved class, which a dropped
143
+ // binding never was, so nothing needs a second extraction.
144
+ const bound = [...new Set(extracted.bindings.map(primaryFile))];
145
+ const ours = new Set((await Promise.all(bound.map(async (f) => ((await hasUtilsHeader(f)) ? f : undefined)))).filter((f) => f !== undefined));
146
+ const { unrecognized } = extracted;
147
+ const bindings = extracted.bindings.filter((b) => !ours.has(primaryFile(b)));
137
148
  const calls = extractPomCalls(spec, bindings);
138
149
  const bindingsByName = new Map(bindings.map((b) => [b.name, b]));
139
150
  const violations = [];
@@ -1,12 +1,40 @@
1
1
  export declare function isBrowserTestType(testType?: string): boolean;
2
2
  export declare function isModularizeFirstTarget(testType?: string, language?: string): boolean;
3
+ /**
4
+ * Which pass one reuse call serves, resolved ONCE per call and handed to every
5
+ * branch — discovery, verify, the prompt and the modularization check — so none
6
+ * of them re-derives it. `pass` is the page-object pass or the SkyrampUtils flow;
7
+ * `composed` says the target takes both (isPomFirstTarget), so the utils flow
8
+ * follows the page-object pass and one verify checks both halves.
9
+ */
10
+ export interface ReuseRoute {
11
+ pass: "pom" | "utils";
12
+ composed: boolean;
13
+ }
14
+ /** The route for a target. `pomPassFinished` is the one run-state fact it turns on:
15
+ * with both settings on, a finished page-object pass hands the spec to the utils flow. */
16
+ export declare function reuseRouteFor(language: string, framework: string | undefined, testType: string | undefined, pomPassFinished?: boolean): ReuseRoute;
17
+ /**
18
+ * Both reuse settings on, for a test both paths serve: a UI test in TypeScript or
19
+ * JavaScript. Such a test takes the page-object pass first — it owns every
20
+ * page-object class its detection returns — and then the seeded SkyrampUtils flow
21
+ * over what that pass left inline. `isPomAwareTarget` alone still routes the
22
+ * page-object pass; this names the composition, so e2e keeps its single path.
23
+ *
24
+ * No framework parameter: a UI test in TypeScript or JavaScript is a Playwright
25
+ * test — the generator derives that framework for it (deriveEffectiveFramework), and
26
+ * the tools that read this predicate after generation (modularization, the
27
+ * shared-helper verifier, the maintenance step) carry no framework to pass.
28
+ */
29
+ export declare function isPomFirstTarget(language: string | undefined, testType: string | undefined): boolean;
3
30
  export declare function isPomAwareTarget(language: string, framework?: string, testType?: string): boolean;
4
31
  /**
5
32
  * Which family's inline call-site rule and sibling scan a test gets — the ONE gate
6
33
  * for both. `api` for integration, whatever the flags (the rule is stated in API
7
34
  * terms and predates the flag); `browser` for the seeded UI flow, where a Skyramp
8
- * browser module exists to call into; `undefined` everywhere else (`contract`, `load`,
9
- * e2e, an absent type, UI with the flag off or page-object reuse on), where an
35
+ * browser module exists to call into — with page-object reuse on as well, since the
36
+ * seeded flow runs after the page-object pass; `undefined` everywhere else (`contract`,
37
+ * `load`, e2e, an absent type, UI with the flag off), where an
10
38
  * advisory would describe reuse the feature could not have taken. The prompt renders
11
39
  * the rule and STEP 5c on it; the verify pass runs the scan on it.
12
40
  */
@@ -19,30 +19,50 @@ export function isBrowserTestType(testType) {
19
19
  // maintenance step) read isModularizeFirstHandOffTarget in prompts/reuse-hand-off.ts,
20
20
  // which is THIS predicate plus a utils spec for the language: a language the
21
21
  // verifier does not cover has no payable reuse half, so no hand-off is owed for
22
- // it. Integration qualifies whenever the flag is on;
23
- // UI only while the POM path is off (POM refactoring supersedes modularization,
24
- // and the testbot UI flow asks for codeReuse unconditionally, so the flag is what
25
- // keeps the default-off behavior a no-op reuse pass). e2e and load are not
26
- // routed here: load has no modularization routing at all, e2e is deferred.
27
- // Deliberately keyed on the POM *flag*, not on isPomAwareTarget: with the flag
28
- // on, a UI target the POM path does not cover keeps today's plain reuse pass.
29
- // Composing utils reuse with the POM path is a separate change; this predicate
30
- // only decides the flag-off UI flow. The modularization prompts read it too, so
31
- // extraction and seeding agree for every codeReuse flow; a modularizeCode-only
32
- // call (no codeReuse) still receives the shared-helper rules — harmless without
33
- // a reuse pass, and the tool cannot see codeReuse.
22
+ // it. Integration qualifies whenever the flag is on; UI in TypeScript or
23
+ // JavaScript too, whatever the page-object flag says. With page-object reuse
24
+ // also on, the page-object pass runs FIRST and the seeded flow takes what it
25
+ // left inline (see isPomFirstTarget). e2e and load are not routed here: load has
26
+ // no modularization routing at all, e2e is deferred.
34
27
  export function isModularizeFirstTarget(testType, language) {
35
28
  if (!isUtilsReuseEnabled())
36
29
  return false;
37
30
  if (testType === "integration")
38
31
  return true;
39
- if (testType !== "ui" || isPomReuseEnabled())
32
+ if (testType !== "ui")
40
33
  return false;
41
34
  // Shared browser helpers are Playwright TS/JS; other UI languages have no
42
35
  // utils-file convention here and would fall through to a Python file name.
43
36
  const lang = language?.toLowerCase();
44
37
  return lang === "typescript" || lang === "javascript";
45
38
  }
39
+ /** The route for a target. `pomPassFinished` is the one run-state fact it turns on:
40
+ * with both settings on, a finished page-object pass hands the spec to the utils flow. */
41
+ export function reuseRouteFor(language, framework, testType, pomPassFinished = false) {
42
+ const composed = isPomFirstTarget(language, testType);
43
+ const pomAware = isPomAwareTarget(language, framework, testType);
44
+ return {
45
+ pass: pomAware && !(composed && pomPassFinished) ? "pom" : "utils",
46
+ composed,
47
+ };
48
+ }
49
+ /**
50
+ * Both reuse settings on, for a test both paths serve: a UI test in TypeScript or
51
+ * JavaScript. Such a test takes the page-object pass first — it owns every
52
+ * page-object class its detection returns — and then the seeded SkyrampUtils flow
53
+ * over what that pass left inline. `isPomAwareTarget` alone still routes the
54
+ * page-object pass; this names the composition, so e2e keeps its single path.
55
+ *
56
+ * No framework parameter: a UI test in TypeScript or JavaScript is a Playwright
57
+ * test — the generator derives that framework for it (deriveEffectiveFramework), and
58
+ * the tools that read this predicate after generation (modularization, the
59
+ * shared-helper verifier, the maintenance step) carry no framework to pass.
60
+ */
61
+ export function isPomFirstTarget(language, testType) {
62
+ return (isPomReuseEnabled() &&
63
+ testType === "ui" &&
64
+ isModularizeFirstTarget(testType, language));
65
+ }
46
66
  export function isPomAwareTarget(language, framework, testType) {
47
67
  if (!isPomReuseEnabled())
48
68
  return false;
@@ -56,8 +76,9 @@ export function isPomAwareTarget(language, framework, testType) {
56
76
  * Which family's inline call-site rule and sibling scan a test gets — the ONE gate
57
77
  * for both. `api` for integration, whatever the flags (the rule is stated in API
58
78
  * terms and predates the flag); `browser` for the seeded UI flow, where a Skyramp
59
- * browser module exists to call into; `undefined` everywhere else (`contract`, `load`,
60
- * e2e, an absent type, UI with the flag off or page-object reuse on), where an
79
+ * browser module exists to call into — with page-object reuse on as well, since the
80
+ * seeded flow runs after the page-object pass; `undefined` everywhere else (`contract`,
81
+ * `load`, e2e, an absent type, UI with the flag off), where an
61
82
  * advisory would describe reuse the feature could not have taken. The prompt renders
62
83
  * the rule and STEP 5c on it; the verify pass runs the scan on it.
63
84
  */
@@ -6,6 +6,7 @@ import { promisify } from "util";
6
6
  import fg from "fast-glob";
7
7
  import { canonicalJson as canonical, isPlainObject as isObject, } from "./canonicalJson.js";
8
8
  import { listChangedFiles } from "./reportVerification.js";
9
+ import { contentAtHead } from "./utils-verify/head.js";
9
10
  import { SKYRAMP_SDK_VERSION } from "./skyrampSdkVersion.js";
10
11
  const execFileAsync = promisify(execFile);
11
12
  export const NPM_TEST_PACKAGES = new Set([
@@ -96,7 +97,7 @@ export async function verifyTestDependencyChanges(repositoryPath) {
96
97
  }
97
98
  continue;
98
99
  }
99
- const committedBaseline = await baselineFile(repositoryPath, file);
100
+ const committedBaseline = await contentAtHead(path.join(repositoryPath, file), repositoryPath);
100
101
  const baseline = committedBaseline ??
101
102
  (await baselineForNewManifest(repositoryPath, file, name, violations));
102
103
  if (baseline === undefined)
@@ -203,19 +204,6 @@ function verifyJavaManifest(file, name, baseline, current, violations) {
203
204
  return verifyMaven(file, baseline, current, violations);
204
205
  return verifyGradle(file, baseline, current, violations);
205
206
  }
206
- async function baselineFile(repositoryPath, file) {
207
- try {
208
- const { stdout } = await execFileAsync("git", ["show", `HEAD:${file}`], {
209
- cwd: repositoryPath,
210
- encoding: "utf8",
211
- maxBuffer: 10 * 1024 * 1024,
212
- });
213
- return stdout;
214
- }
215
- catch {
216
- return undefined;
217
- }
218
- }
219
207
  async function baselineFileExists(repositoryPath, file) {
220
208
  try {
221
209
  await execFileAsync("git", ["cat-file", "-e", `HEAD:${file}`], {
@@ -540,7 +528,7 @@ async function correspondingManifests(repositoryPath, file, lockName) {
540
528
  const rootManifest = at("package.json");
541
529
  // Membership comes from the committed root manifest, not the edited lockfile.
542
530
  // A workspace addition is an unrelated manifest edit and remains disallowed.
543
- const baseline = await baselineFile(repositoryPath, rootManifest);
531
+ const baseline = await contentAtHead(path.join(repositoryPath, rootManifest), repositoryPath);
544
532
  if (baseline === undefined)
545
533
  return [rootManifest];
546
534
  const manifest = JSON.parse(baseline);
@@ -575,7 +563,7 @@ async function correspondingManifests(repositoryPath, file, lockName) {
575
563
  return [];
576
564
  }
577
565
  async function verifyLockfile(repositoryPath, file, name, pairedManifests, validManifests, violations) {
578
- const committedBaseline = await baselineFile(repositoryPath, file);
566
+ const committedBaseline = await contentAtHead(path.join(repositoryPath, file), repositoryPath);
579
567
  const baseline = committedBaseline ?? '{"packages":{}}\n';
580
568
  let current;
581
569
  try {
@@ -1,6 +1,6 @@
1
1
  import { type UtilsHelper } from "./parse.js";
2
2
  import { type UtilsLanguageSpec } from "./language-spec.js";
3
- import { type InlineCallSite, type SiblingFile } from "./call-sites.js";
3
+ import { type InlineCallSite, type SiblingScan } from "./call-sites.js";
4
4
  /**
5
5
  * The browser family's sibling scan: a contiguous run of actions in OTHER
6
6
  * Skyramp-generated tests beside `testFile` whose action sequence equals a utils
@@ -29,4 +29,4 @@ import { type InlineCallSite, type SiblingFile } from "./call-sites.js";
29
29
  * consumed and the scan continues after it. A run inside the sibling's own local
30
30
  * helper is reported with that helper's name, as the API scan does.
31
31
  */
32
- export declare function findSiblingInlineActionSites(testFile: string, helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings: SiblingFile[] | undefined): Promise<InlineCallSite[]>;
32
+ export declare function findSiblingInlineActionSites(testFile: string, helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings: SiblingScan | undefined): Promise<InlineCallSite[]>;
@@ -74,7 +74,7 @@ export type UtilsViolationKind = "duplicate-helper" | "body-assertion" | "scenar
74
74
  /** Kinds that only ever advise. Kept OUT of UtilsViolationKind so that type keeps
75
75
  * meaning "a kind the allow grammar and the gate know about" — an advisory can
76
76
  * never fail a verify and never needs (or accepts) an allow marker. */
77
- export declare const UTILS_ADVISORY_KINDS: readonly ["unusable-parameter-type", "fixed-sleep", "module-name", "duplicate-body-reach", "duplicate-action", "tsconfig-type-error", "ambient-type-error", "in-house-duplicate"];
77
+ export declare const UTILS_ADVISORY_KINDS: readonly ["unusable-parameter-type", "fixed-sleep", "module-name", "duplicate-body-reach", "duplicate-action", "tsconfig-type-error", "ambient-type-error", "inherited-type-error", "in-house-duplicate"];
78
78
  export type UtilsAdvisoryKind = (typeof UTILS_ADVISORY_KINDS)[number];
79
79
  export declare const MARKERABLE_UTILS_KINDS: readonly ["frozen-body", "duplicate-helper", "body-assertion", "scenario-name", "assertion-loss", "status-default", "status-restated"];
80
80
  /** A kind the allow grammar accepts. `guard-removed` is a gate kind but never
@@ -50,6 +50,21 @@ export const UTILS_ADVISORY_KINDS = [
50
50
  * never blocked: a block here made an agent rewrite `process.env` into a
51
51
  * `globalThis` cast to get past the gate. */
52
52
  "ambient-type-error",
53
+ /** A compiler error this run INHERITED: the line is unchanged since HEAD AND
54
+ * the file at HEAD already failed the compiler with the same code on that
55
+ * line. Whatever the error is, the customer or an earlier run wrote it, it is
56
+ * not the shared module's fault, and it must never be the reason a valid
57
+ * module is deleted: a run once destroyed its module over four assertion lines
58
+ * that pre-dated the run. Advised, never blocked.
59
+ *
60
+ * Attribution is by CAUSE, not by position: an error the run caused on a line
61
+ * it left alone — a helper rewritten to return nothing under an unchanged line
62
+ * that reads its result — was not there at HEAD and blocks. A file HEAD does
63
+ * not hold (a module or a spec this run generated) has nothing to inherit
64
+ * from, and every error in it blocks, the generator's included: a line any
65
+ * pass of this run wrote is the run's to fix. See inheritedTypeError in
66
+ * changed-lines.ts. */
67
+ "inherited-type-error",
53
68
  /** A helper this run added to one of the repository's own modules performs an
54
69
  * action sequence a helper already in that module performs — the duplicate the
55
70
  * extension exists to avoid, now inside the customer's file. Advisory for the
@@ -1,5 +1,5 @@
1
1
  import type { UtilsLanguageSpec } from "./language-spec.js";
2
- import { type SiblingFile } from "./call-sites.js";
2
+ import { type SiblingScan } from "./call-sites.js";
3
3
  /**
4
4
  * A shared helper whose request body no parameter of its signature reaches, beside a
5
5
  * spec that defines its OWN helper for the same route.
@@ -117,4 +117,4 @@ export declare function findDuplicateBodyReach(utilsFiles: string[], spec: Utils
117
117
  * Shared helpers whose body no parameter reaches, each paired with a sibling spec
118
118
  * that defines its own helper for the same route. Never throws; fails open to [].
119
119
  */
120
- export declare function findUnreachableBodies(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingFile[]): Promise<UnreachableBody[]>;
120
+ export declare function findUnreachableBodies(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingScan): Promise<UnreachableBody[]>;
@@ -319,7 +319,10 @@ export async function findUnreachableBodies(testFile, utilsFiles, spec, preReadS
319
319
  const siblings = preReadSiblings ??
320
320
  (await generatedSiblings(testFile, spec, { includeSelf: true }));
321
321
  const out = [];
322
- for (const { file, content } of siblings) {
322
+ // `.files`: the walk reports what it read alongside whether it read the whole
323
+ // directory. This scan acts only on siblings it HAS, so an incomplete walk costs it
324
+ // a finding and never invents one.
325
+ for (const { file, content } of siblings.files) {
323
326
  for (const h of localHelpersOf(content, spec)) {
324
327
  if (!h.method || !h.normalizedPath)
325
328
  continue;
@@ -10,16 +10,26 @@ export interface SiblingFile {
10
10
  file: string;
11
11
  content: string;
12
12
  }
13
+ /** The sibling walk's result. `complete` is the walk's own verdict on whether it saw
14
+ * the whole directory: the listing succeeded AND every candidate file was read. An
15
+ * incomplete walk still returns what it found — the findings stand — but a caller
16
+ * that blocks or asserts on the ABSENCE of a sibling must not treat the list as the
17
+ * full set. A silently short list is indistinguishable from an honest one. */
18
+ export interface SiblingScan {
19
+ files: SiblingFile[];
20
+ complete: boolean;
21
+ }
13
22
  /**
14
23
  * The Skyramp-generated tests in the spec's directory, each read once — THE sibling
15
24
  * walk, shared by the two sibling call-site scans and the importer count (the inline
16
25
  * scan excludes the spec itself; the other two include it). Utils files carry no
17
- * codegen version marker, so the marker check alone excludes them. Fails open to []
18
- * on any read error.
26
+ * codegen version marker, so the marker check alone excludes them. Fails open to an
27
+ * empty, INCOMPLETE scan on a listing failure, and drops an unreadable file from a
28
+ * scan it then reports as incomplete.
19
29
  */
20
30
  export declare function generatedSiblings(testFile: string, spec: UtilsLanguageSpec, opts: {
21
31
  includeSelf: boolean;
22
- }): Promise<SiblingFile[]>;
32
+ }): Promise<SiblingScan>;
23
33
  /** An SDK request call. Global: clone with `new RegExp(...)` before `matchAll`. */
24
34
  export declare const REQUEST_CALL_RE: RegExp;
25
35
  interface InlineSiteBase {
@@ -70,7 +80,7 @@ export type InlineCallSite = (InlineSiteBase & {
70
80
  * findSiblingInlineActionSites is the browser family's, and inlineCallSiteFamily
71
81
  * decides which runs.
72
82
  */
73
- export declare function findSiblingInlineCallSites(testFile: string, utilsFiles: string[], _helpers: UtilsHelper[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingFile[]): Promise<InlineCallSite[]>;
83
+ export declare function findSiblingInlineCallSites(testFile: string, utilsFiles: string[], _helpers: UtilsHelper[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingScan): Promise<InlineCallSite[]>;
74
84
  /** One operation written in two or more sibling generated specs that no shared
75
85
  * helper wraps — the extraction the reuse pass exists to perform and did not. */
76
86
  export interface UnextractedDuplicate {
@@ -108,11 +118,11 @@ export interface UnextractedSite {
108
118
  * A group must span two or more FILES: a spec repeating one request twice is a
109
119
  * different question. Advisory by design — consolidation is a judgement. Never throws.
110
120
  */
111
- export declare function findUnextractedDuplicates(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingFile[]): Promise<UnextractedDuplicate[]>;
121
+ export declare function findUnextractedDuplicates(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingScan): Promise<UnextractedDuplicate[]>;
112
122
  /** The generated siblings less the utils files themselves (a utils file carries no
113
123
  * codegen marker, so this is belt and braces). Pre-read siblings are used as given —
114
124
  * the caller's walk decides whether the spec itself is among them. */
115
- export declare function nonUtilsSiblings(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings: SiblingFile[] | undefined, includeSelf: boolean): Promise<SiblingFile[]>;
125
+ export declare function nonUtilsSiblings(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings: SiblingScan | undefined, includeSelf: boolean): Promise<SiblingFile[]>;
116
126
  /** Lines after a call scanned for its schema assertion — the generated shape puts it
117
127
  * immediately after the status-code assertion. */
118
128
  export declare const SCHEMA_SCAN_LINES = 12;
@@ -14,8 +14,9 @@ const HEADER_SCAN_LINES = 5;
14
14
  * The Skyramp-generated tests in the spec's directory, each read once — THE sibling
15
15
  * walk, shared by the two sibling call-site scans and the importer count (the inline
16
16
  * scan excludes the spec itself; the other two include it). Utils files carry no
17
- * codegen version marker, so the marker check alone excludes them. Fails open to []
18
- * on any read error.
17
+ * codegen version marker, so the marker check alone excludes them. Fails open to an
18
+ * empty, INCOMPLETE scan on a listing failure, and drops an unreadable file from a
19
+ * scan it then reports as incomplete.
19
20
  */
20
21
  export async function generatedSiblings(testFile, spec, opts) {
21
22
  let dir;
@@ -27,8 +28,9 @@ export async function generatedSiblings(testFile, spec, opts) {
27
28
  entries = await fs.promises.readdir(dir);
28
29
  }
29
30
  catch {
30
- return [];
31
+ return { files: [], complete: false };
31
32
  }
33
+ let complete = true;
32
34
  const out = [];
33
35
  for (const name of entries.sort()) {
34
36
  const file = path.join(dir, name);
@@ -41,13 +43,16 @@ export async function generatedSiblings(testFile, spec, opts) {
41
43
  content = await fs.promises.readFile(file, "utf8");
42
44
  }
43
45
  catch {
46
+ // A candidate this walk could not read may or may not have been a generated
47
+ // test importing the module — so the scan is short by an unknown amount.
48
+ complete = false;
44
49
  continue;
45
50
  }
46
51
  if (!CODEGEN_MARKER_RE.test(content.split("\n").slice(0, HEADER_SCAN_LINES).join("\n")))
47
52
  continue;
48
53
  out.push({ file, content });
49
54
  }
50
- return out;
55
+ return { files: out, complete };
51
56
  }
52
57
  /** An SDK request call. Global: clone with `new RegExp(...)` before `matchAll`. */
53
58
  export const REQUEST_CALL_RE = /\b(?:send_request|sendRequest)\s*\(/g;
@@ -183,8 +188,9 @@ async function helperRoutes(utilsFiles, spec) {
183
188
  * the caller's walk decides whether the spec itself is among them. */
184
189
  export async function nonUtilsSiblings(testFile, utilsFiles, spec, preReadSiblings, includeSelf) {
185
190
  const utilsSet = new Set(await Promise.all(utilsFiles.map((f) => realpath(f))));
186
- return (preReadSiblings ??
187
- (await generatedSiblings(testFile, spec, { includeSelf }))).filter((sib) => !utilsSet.has(path.resolve(sib.file)));
191
+ const scan = preReadSiblings ??
192
+ (await generatedSiblings(testFile, spec, { includeSelf }));
193
+ return scan.files.filter((sib) => !utilsSet.has(path.resolve(sib.file)));
188
194
  }
189
195
  /** Every keyable request call in a generated test. Calls whose method or path cannot
190
196
  * be read are skipped: they have no route to compare. Calls are located, and their