@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
@@ -7,7 +7,7 @@ function actionSequence(body, language) {
7
7
  return keys ? { actionKey: keys.join("; "), actionSteps: keys } : {};
8
8
  }
9
9
  import { fixedSleepsIn, pythonSleepBindings, } from "./fixed-sleep.js";
10
- import { stripComments, stripCommentsAndStrings, stripStrings, } from "../pom-scope/strip.js";
10
+ import { stringSpans, stripComments, stripCommentsAndStrings, stripStrings, } from "../pom-scope/strip.js";
11
11
  /** Whether the `<` at `i` opens a generic (`Record<string, …>`, `Array<{…}>`) rather
12
12
  * than comparing (`i < 3`): a generic follows an identifier or `>` and is followed
13
13
  * by a type start; a comparison is followed by a number, a space and a value, or
@@ -266,6 +266,7 @@ export function parseUtilsFile(content, spec) {
266
266
  const helper = {
267
267
  name,
268
268
  line: start + 1,
269
+ startLine: decoratorStart(lines, structure, start) + 1,
269
270
  endLine: Math.max(end, start + 1),
270
271
  wrapsRequest: false,
271
272
  hasGuard: hasPageErrorGuard(body.join("\n"), spec),
@@ -355,69 +356,100 @@ export function isSingleLiteral(line, m) {
355
356
  after.startsWith(")") ||
356
357
  after.startsWith("}"));
357
358
  }
358
- /** Comments and string CONTENTS blanked (quotes kept, line count preserved). Python
359
- * comments are `#…`; TS/JS go through the shared stripper. */
360
- /** String CONTENTS blanked (quotes kept, comments kept, line count preserved). */
361
- export function blankStrings(src, spec) {
362
- if (spec.language !== "python")
363
- return stripStrings(src);
364
- return blankPythonStrings(src);
359
+ /** The string literals in `src`, quotes included, in order. TS/JS go through the
360
+ * shared scanner; Python goes through the one below, so a caller sees one definition
361
+ * of a literal for each language. */
362
+ export function stringSpansOf(src, spec) {
363
+ return spec.language === "python" ? pythonStringSpans(src) : stringSpans(src);
365
364
  }
366
365
  /**
367
- * Blank every Python string literal, triple-quoted ones included, preserving offsets.
366
+ * The string literals in Python source, quotes included.
368
367
  *
369
- * A line-by-line regex cannot do this: a `"""…"""` block spans lines, so a `#` inside a
370
- * docstring or an embedded SQL block read as a comment. Every scanner built on this
371
- * then treated quoted text as code the file executes — and the delivery-time marker
372
- * strip deleted a line out of a value rather than out of a comment.
368
+ * Triple quotes are tested BEFORE single ones, and this is the whole reason the scan
369
+ * is not a regex: a `"""` read one quote at a time matches an empty `""` and leaves
370
+ * the payload to be treated as code. A multiline payload's own indentation is then
371
+ * read as formatting, and a change to it disappears.
372
+ *
373
+ * A single-quoted string that never closes before the end of its line is a stray
374
+ * apostrophe in code (`# don't`, or a regex), not an opener — the same rule the TS
375
+ * scanner applies. A prefix (`r`, `f`, `b`, `rb`) is code; only the quote opens the
376
+ * literal.
377
+ *
378
+ * COMMENTS ARE SKIPPED while looking for an opener, in the same pass rather than a
379
+ * prior one: a comment that merely mentions a quote (`# a docstring starts with """`)
380
+ * would otherwise open a span that runs to the next real delimiter, blanking every
381
+ * definition in between so its helpers vanish from every check that reads the
382
+ * structure. A `#` inside a literal is not a comment, which is why one pass has to
383
+ * decide both.
373
384
  */
374
- function blankPythonStrings(src) {
375
- const out = src.split("");
376
- const blank = (from, to) => {
377
- for (let k = from; k < to; k++)
378
- if (out[k] !== "\n")
379
- out[k] = " ";
380
- };
381
- let i = 0;
382
- const n = src.length;
383
- while (i < n) {
384
- const c = src[i];
385
- // A comment runs to the end of the line and holds no string opener.
386
- if (c === "#") {
387
- while (i < n && src[i] !== "\n")
385
+ function pythonStringSpans(src) {
386
+ const out = [];
387
+ for (let i = 0; i < src.length; i++) {
388
+ const q = src[i];
389
+ if (q === "#") {
390
+ // Outside a literal by construction — a literal advances `i` past its end.
391
+ while (i < src.length && src[i] !== "\n")
388
392
  i++;
389
393
  continue;
390
394
  }
391
- if (c !== '"' && c !== "'") {
392
- i++;
395
+ if (q !== '"' && q !== "'")
393
396
  continue;
394
- }
395
- // A string prefix (r, b, f, u, rb, …) is part of the literal, not of the quote.
396
- const triple = src.startsWith(c.repeat(3), i);
397
- const quote = triple ? c.repeat(3) : c;
398
- const start = i;
399
- i += quote.length;
400
- let closed = false;
401
- while (i < n) {
402
- if (src[i] === "\\") {
403
- i += 2;
397
+ const triple = src.startsWith(q + q + q, i);
398
+ const close = triple ? q + q + q : q;
399
+ let j = i + close.length;
400
+ let end = -1;
401
+ while (j < src.length) {
402
+ if (src[j] === "\\") {
403
+ j += 2;
404
404
  continue;
405
405
  }
406
- if (src.startsWith(quote, i)) {
407
- i += quote.length;
408
- closed = true;
406
+ if (src.startsWith(close, j)) {
407
+ end = j + close.length;
409
408
  break;
410
409
  }
411
410
  // Only a triple-quoted literal may hold a raw newline.
412
- if (src[i] === "\n" && !triple)
411
+ if (src[j] === "\n" && !triple)
413
412
  break;
414
- i++;
415
- }
416
- if (!closed) {
417
- i = start + 1;
418
- continue;
413
+ j++;
419
414
  }
420
- blank(start + quote.length, i - quote.length);
415
+ if (end === -1)
416
+ continue; // never closed: not a literal
417
+ out.push({ start: i, end });
418
+ i = end - 1;
419
+ }
420
+ return out;
421
+ }
422
+ /** Comments and string CONTENTS blanked (quotes kept, line count preserved). Python
423
+ * comments are `#…`; TS/JS go through the shared stripper. */
424
+ /** String CONTENTS blanked (quotes kept, comments kept, line count preserved). */
425
+ export function blankStrings(src, spec) {
426
+ if (spec.language !== "python")
427
+ return stripStrings(src);
428
+ return blankPythonStrings(src);
429
+ }
430
+ /**
431
+ * String CONTENTS blanked in Python source, quotes kept, offsets and line count
432
+ * preserved.
433
+ *
434
+ * Reads its literals from `pythonStringSpans` — the same scan `stringSpansOf` serves —
435
+ * so ONE definition decides what a Python literal is, for the callers that blank a
436
+ * literal and the callers that must copy one through unchanged.
437
+ *
438
+ * A line-by-line regex cannot do this. A `"""…"""` block spans lines, so a `#` inside a
439
+ * docstring or an embedded SQL block read as a comment, and every scanner built on this
440
+ * then treated quoted text as code the file executes — the delivery-time marker strip
441
+ * deleted a line out of a value rather than out of a comment, and a `def` at column 0
442
+ * inside a docstring ended a helper early and reappeared as a second definition of the
443
+ * same name.
444
+ */
445
+ function blankPythonStrings(src) {
446
+ const out = src.split("");
447
+ for (const { start, end } of pythonStringSpans(src)) {
448
+ // The quote delimiters stay: callers read this copy for offsets and expect a
449
+ // literal to still look like one.
450
+ const q = src.startsWith(src[start].repeat(3), start) ? 3 : 1;
451
+ for (let i = start + q; i < end - q; i++)
452
+ out[i] = src[i] === "\n" ? "\n" : " ";
421
453
  }
422
454
  return out.join("");
423
455
  }
@@ -508,6 +540,47 @@ function signatureLines(body) {
508
540
  }
509
541
  return out;
510
542
  }
543
+ /** The index of the first line of the decorator stack above `start`, or `start` when
544
+ * there is none. Comments and blank lines may sit between decorators; anything else
545
+ * ends the stack.
546
+ *
547
+ * A decorator may span lines — `@retry(` … `)` — so the walk goes up by EXPRESSION,
548
+ * not by line: from a line that closes more brackets than it opens, it keeps going
549
+ * until the brackets balance, and only then asks whether that line starts with `@`.
550
+ * A line-at-a-time walk sees `)` above the definition, stops there, and drops the
551
+ * whole decorator out of the comparison.
552
+ *
553
+ * Brackets are counted on `structure`, the copy with comments and string contents
554
+ * blanked, for the same reason every other structural read here uses it: a bracket
555
+ * typed inside a string (`retry_if_message(")")`) is not a bracket. The `@` itself is
556
+ * read from `lines`, where it is still legible. Both copies keep the same line count,
557
+ * so one index walks both.
558
+ *
559
+ * Language-neutral: a definition with no `@` line simply gets `start` back. */
560
+ function decoratorStart(lines, structure, start) {
561
+ let first = start;
562
+ let depth = 0;
563
+ for (let i = start - 1; i >= 0; i--) {
564
+ const l = lines[i].trim();
565
+ // Blank, or a comment: `structure` has comments blanked already, so the test
566
+ // is one and the same in every language — no prefix list to keep in step.
567
+ if (depth === 0 && (structure[i] ?? "").trim() === "")
568
+ continue;
569
+ for (const ch of structure[i] ?? "") {
570
+ if (")]}".includes(ch))
571
+ depth++;
572
+ else if ("([{".includes(ch))
573
+ depth--;
574
+ }
575
+ if (depth > 0)
576
+ continue; // inside a wrapped expression; keep walking up
577
+ depth = 0;
578
+ if (!l.startsWith("@"))
579
+ break;
580
+ first = i;
581
+ }
582
+ return first;
583
+ }
511
584
  /** Whether the def at `start` carries `@overload` / `@typing.overload` anywhere in
512
585
  * its decorator stack (comments and other decorators may sit between). */
513
586
  function isOverloadStub(lines, start) {
@@ -41,8 +41,3 @@ export interface RetrofitMismatch {
41
41
  * than a refusal the agent cannot act on.
42
42
  */
43
43
  export declare function retrofitMismatches(retrofitFile: string, utilsFiles: string[], spec: UtilsLanguageSpec): Promise<RetrofitMismatch[]>;
44
- /** The file's content at HEAD, or undefined when there is nothing to compare against
45
- * (untracked, no git root, or git unavailable). Never throws. The ONE reader of a
46
- * file's committed baseline on the utils path: the incumbent-signature gate reads
47
- * the utils module through it too. */
48
- export declare function committedBaseline(file: string): Promise<string | undefined>;
@@ -1,14 +1,9 @@
1
- import { execFile } from "child_process";
2
1
  import * as fs from "fs";
3
- import * as path from "path";
4
- import { promisify } from "util";
5
- import { logger } from "../logger.js";
6
- import { detectGitRoot } from "../gitStaging.js";
2
+ import { contentAtHead } from "./head.js";
7
3
  import { blankCommentsAndStrings, parseUtilsFile } from "./parse.js";
8
4
  import { normalizePath } from "./language-spec.js";
9
5
  import { callText, hasSchemaCheck, requestFields, REQUEST_CALL_RE, SCHEMA_SCAN_LINES, shapeKeys, } from "./call-sites.js";
10
6
  import { escapeRegExp } from "../regex.js";
11
- const exec = promisify(execFile);
12
7
  /**
13
8
  * Whether a retrofit changed the request the rewired test sends.
14
9
  *
@@ -41,7 +36,7 @@ const exec = promisify(execFile);
41
36
  * than a refusal the agent cannot act on.
42
37
  */
43
38
  export async function retrofitMismatches(retrofitFile, utilsFiles, spec) {
44
- const baseline = await committedBaseline(retrofitFile);
39
+ const baseline = await contentAtHead(retrofitFile);
45
40
  if (baseline === undefined)
46
41
  return [];
47
42
  let delivered;
@@ -191,30 +186,3 @@ function sameSet(a, b) {
191
186
  return false;
192
187
  return true;
193
188
  }
194
- /** The file's content at HEAD, or undefined when there is nothing to compare against
195
- * (untracked, no git root, or git unavailable). Never throws. The ONE reader of a
196
- * file's committed baseline on the utils path: the incumbent-signature gate reads
197
- * the utils module through it too. */
198
- export async function committedBaseline(file) {
199
- const root = detectGitRoot(file);
200
- if (!root)
201
- return undefined;
202
- const rel = path.relative(root, file);
203
- if (!rel || rel.startsWith(".."))
204
- return undefined;
205
- try {
206
- const { stdout } = await exec("git", ["show", `HEAD:${rel}`], {
207
- cwd: root,
208
- maxBuffer: 10 * 1024 * 1024,
209
- });
210
- return stdout;
211
- }
212
- catch (err) {
213
- // A file added by this run has no HEAD version — the common case, not an error.
214
- logger.debug("No committed baseline for a retrofit; skipping equivalence", {
215
- file,
216
- error: String(err),
217
- });
218
- return undefined;
219
- }
220
- }
@@ -1,15 +1,13 @@
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, stageGeneratedPaths } from "../gitStaging.js";
6
4
  import { logger } from "../logger.js";
7
5
  import { isTestbotEnabled } from "../featureFlags.js";
8
6
  import { locateUtilsFiles, realpath } from "./locate.js";
7
+ import { changedSinceHead } from "./head.js";
9
8
  import { locateInHouseModules } from "./in-house.js";
10
9
  import { CODEGEN_MARKER_RE } from "./call-sites.js";
11
10
  import { utilsSpecFor, utilsSpecForFile } from "./language-spec.js";
12
- const execFileAsync = promisify(execFile);
13
11
  export async function stageUtilsArtifacts(testFile, language) {
14
12
  const staged = [];
15
13
  const failed = [];
@@ -99,10 +97,7 @@ async function modifiedInHouseModules(testFile, language) {
99
97
  const modules = await locateInHouseModules({ testFile, spec, cwd: root });
100
98
  if (modules.length === 0)
101
99
  return [];
102
- const { stdout } = await execFileAsync("git", ["diff", "-z", "--name-only", "--diff-filter=M", "HEAD"], { cwd: root, encoding: "utf8" });
103
- const modified = new Set();
104
- for (const rel of stdout.split("\0").filter(Boolean))
105
- modified.add(await realpath(path.resolve(root, rel)));
100
+ const modified = new Set(await changedSinceHead(root, { filter: "M" }));
106
101
  return modules.map((m) => m.file).filter((f) => modified.has(f));
107
102
  }
108
103
  catch (err) {
@@ -117,18 +112,12 @@ async function modifiedSiblingsImporting(testFile, utilsFiles, language) {
117
112
  return [];
118
113
  let modified;
119
114
  try {
120
- // `-z`: unquoted, NUL-separated — git C-quotes non-ASCII paths otherwise.
121
- const { stdout } = await execFileAsync("git",
122
115
  // Against HEAD, modified only: a test THIS run created is staged at discovery, so
123
116
  // its later STEP 5b edits show in a plain `git diff` and it would masquerade as a
124
117
  // pre-existing sibling (observed: the report gate then demanded an execution no
125
118
  // record could satisfy, and the agent un-shared the helpers to escape). Files
126
119
  // added since HEAD are excluded; they are staged by their own tool calls.
127
- ["diff", "-z", "--name-only", "--diff-filter=M", "HEAD"], {
128
- cwd: root,
129
- encoding: "utf8",
130
- });
131
- modified = stdout.split("\0").filter(Boolean);
120
+ modified = await changedSinceHead(root, { filter: "M" });
132
121
  }
133
122
  catch (err) {
134
123
  logger.warning("Could not list modified files — edited sibling tests will not be staged", {
@@ -140,8 +129,7 @@ async function modifiedSiblingsImporting(testFile, utilsFiles, language) {
140
129
  const stems = utilsFiles.map((f) => path.basename(f).replace(/\.[^.]+$/, ""));
141
130
  const self = await realpath(testFile);
142
131
  const out = [];
143
- for (const rel of modified) {
144
- const abs = await realpath(path.resolve(root, rel));
132
+ for (const abs of modified) {
145
133
  if (abs === self || utilsFiles.includes(abs))
146
134
  continue;
147
135
  if (!spec.extensions.some((e) => abs.toLowerCase().endsWith(e)))
@@ -1,7 +1,7 @@
1
1
  import { type UtilsTypecheck } from "./typecheck.js";
2
2
  import { type InlineCallSite, type UnextractedDuplicate } from "./call-sites.js";
3
3
  import { type UtilsAdvisoryKind, type UtilsViolationKind } from "./allow.js";
4
- import { type SingleImporterHelper } from "./importers.js";
4
+ import { type HelperImporters } from "./importers.js";
5
5
  import { type HelperFamilySpec, type UtilsLanguageSpec } from "./language-spec.js";
6
6
  export interface UtilsViolation {
7
7
  kind: UtilsViolationKind;
@@ -60,8 +60,14 @@ export interface UtilsVerifyResult {
60
60
  * findUnextractedDuplicates. API family only. */
61
61
  unextractedDuplicates: UnextractedDuplicate[];
62
62
  /** Helpers imported by 0 or 1 delivered test (SKYR-4276 A6). Advisory: a first run
63
- * seeds every helper single-use by design; the count is the trend to watch. */
64
- singleImporters: SingleImporterHelper[];
63
+ * seeds every helper single-use by design; the count is the trend to watch. Empty
64
+ * whenever `importersComplete` is false — see there. */
65
+ singleImporters: HelperImporters[];
66
+ /** The importer walk read the whole directory. When false, `singleImporters` is
67
+ * empty because the walk could not support a finding, NOT because every helper has
68
+ * importers: a file it could not read may be the one importer of a helper. A
69
+ * caller that states "nothing imports this" must consult it. */
70
+ importersComplete: boolean;
65
71
  /** The numbers the baseline assertion check compared — present only when a
66
72
  * baseline was supplied and the spec was readable. The check is a COUNT: it
67
73
  * cannot see one assertion swapped for another. The caller's text says so.
@@ -7,19 +7,20 @@ import { typecheckDelivered } from "./typecheck.js";
7
7
  import { blankComments, blankCommentsAndStrings, countAssertions, hasPageErrorGuard, parseUtilsFile, } from "./parse.js";
8
8
  import { escapeRegExp } from "../regex.js";
9
9
  import { detectGitRoot } from "../gitStaging.js";
10
- import { inlineCallSiteFamily, isModularizeFirstTarget, } from "../reuseRouting.js";
10
+ import { inlineCallSiteFamily, isModularizeFirstTarget, isPomFirstTarget, } from "../reuseRouting.js";
11
11
  import { describeActionKey } from "./action-key.js";
12
12
  import { findDuplicateBodyReach, findUnreachableBodies } from "./body-reach.js";
13
13
  import { misnamedNewModules } from "./module-name.js";
14
+ import { pageObjectCandidates } from "../pom-scope/ownership.js";
14
15
  import { additiveChanges, locateInHouseModules, resolveInHouseCalls, } from "./in-house.js";
15
16
  import { findSiblingInlineActionSites } from "./action-sites.js";
16
17
  import { findSiblingInlineCallSites, findUnextractedDuplicates, generatedSiblings, } from "./call-sites.js";
17
18
  import { parseUtilsAllows, } from "./allow.js";
18
19
  import { describeFixedSleeps, fixedSleepReplacement } from "./fixed-sleep.js";
19
- import { singleImporterHelpers, } from "./importers.js";
20
+ import { singleImporterHelpers } from "./importers.js";
20
21
  import { assertsStatusEquality, defaultedStatusParam, pinnedStatusLiteral, restatedStatusAssertions, } from "./status-once.js";
21
22
  import { STATUS_ONCE_RULE } from "../../prompts/shared-helper-policy.js";
22
- import { committedBaseline } from "./retrofit-equivalence.js";
23
+ import { contentAtHead } from "./head.js";
23
24
  import { incumbentSignatureViolations } from "./incumbent.js";
24
25
  import { helperFamilyFor, helperFamilyOf, scenarioNameViolation, utilsSpecFor, utilsSpecForFile, } from "./language-spec.js";
25
26
  /** What the compiler says about a name defined twice, which `helper-redefined`
@@ -80,9 +81,16 @@ export async function verifyUtils(params) {
80
81
  // locateInHouseModules). Found BEFORE the utils files so a customer module that
81
82
  // gained our header this run is judged by the additive gate, never by the Skyramp
82
83
  // invariants: the header locator would otherwise claim it.
83
- const inHouse = spec
84
+ const located = spec
84
85
  ? await locateInHouseModules({ testFile: params.testFile, spec, cwd: root })
85
86
  : [];
87
+ // Both reuse settings on: a module that declares a class is a page object, which
88
+ // the page-object pass owned and its verifier checked. The in-house parser cannot
89
+ // read a class, and its additive gate would judge a page-object edit it did not
90
+ // prescribe — so the split is by shape, the one pom-scope/ownership.ts states.
91
+ const pomFirst = !!spec && isPomFirstTarget(spec.language, params.testType);
92
+ const pageObjectFiles = new Set(pomFirst ? (await pageObjectCandidates(located)).map((m) => m.file) : []);
93
+ const inHouse = located.filter((m) => !pageObjectFiles.has(m.file));
86
94
  const inHouseFiles = new Set(inHouse.map((m) => m.file));
87
95
  const utilsFiles = spec
88
96
  ? (await locateUtilsFiles({
@@ -107,6 +115,7 @@ export async function verifyUtils(params) {
107
115
  inlineCallSites: [],
108
116
  unextractedDuplicates: [],
109
117
  singleImporters: [],
118
+ importersComplete: false,
110
119
  };
111
120
  if (!spec)
112
121
  return result;
@@ -161,7 +170,7 @@ export async function verifyUtils(params) {
161
170
  // helpers this run writes, so a defaulted status on an incumbent is not advised
162
171
  // — and an incumbent's signature is held to what it was (incumbent.ts). Absent
163
172
  // for a module this run created, or outside git: then every helper is this run's.
164
- const baseline = await committedBaseline(file);
173
+ const baseline = await contentAtHead(file);
165
174
  const headHelpers = baseline === undefined ? [] : parseUtilsFile(baseline, fileSpec);
166
175
  const incumbents = new Set(headHelpers.map((h) => h.name));
167
176
  incumbentsOfFile.set(file, incumbents);
@@ -321,6 +330,9 @@ export async function verifyUtils(params) {
321
330
  }
322
331
  if (utilsSet.has(imp.resolved) || imp.resolved === realTestFile)
323
332
  continue;
333
+ // The page-object pass's module: checked by its verifier, not this one.
334
+ if (pageObjectFiles.has(imp.resolved))
335
+ continue;
324
336
  if (inHouseFiles.has(imp.resolved)) {
325
337
  if (!inHouseResult)
326
338
  unchecked.push(imp.resolved);
@@ -372,7 +384,7 @@ export async function verifyUtils(params) {
372
384
  if (!inHouseResult && inHouse.length > 0)
373
385
  result.inHouseChecksSkipped = `this pass ran ${params.testType
374
386
  ? `as a ${params.testType.toLowerCase()} test`
375
- : "with no test type supplied"}, and they run for a UI test with page-object reuse off`;
387
+ : "with no test type supplied"}, and they run for a UI test in TypeScript or JavaScript with utils reuse on`;
376
388
  }
377
389
  // The spec asserts a status a helper it calls already asserted on the response it
378
390
  // returned. Spec-level, like assertion-loss: the finding names the spec and the
@@ -478,12 +490,13 @@ export async function verifyUtils(params) {
478
490
  });
479
491
  }
480
492
  // One directory walk feeds every sibling advisory: each sibling is read exactly once.
481
- let siblings = [];
493
+ let siblings = { files: [], complete: false };
482
494
  try {
483
495
  siblings = await generatedSiblings(params.testFile, spec, {
484
496
  includeSelf: true,
485
497
  });
486
498
  result.singleImporters = await singleImporterHelpers(params.testFile, utilsFiles, helpersByFile, spec, siblings);
499
+ result.importersComplete = siblings.complete;
487
500
  }
488
501
  catch {
489
502
  /* advisory only — never fails a verify */
@@ -864,9 +877,9 @@ incumbents = new Set()) {
864
877
  });
865
878
  }
866
879
  else if (sameName &&
867
- h.arity !== undefined &&
868
- sameName.arity !== undefined &&
869
- h.arity !== sameName.arity &&
880
+ h.params !== undefined &&
881
+ sameName.params !== undefined &&
882
+ h.params.length !== sameName.params.length &&
870
883
  !(routeDuplicateOf?.file === sameName.file &&
871
884
  routeDuplicateOf?.line === sameName.line)) {
872
885
  nameReported = true;
@@ -875,7 +888,7 @@ incumbents = new Set()) {
875
888
  file,
876
889
  helper: h.name,
877
890
  line: h.line,
878
- detail: `${h.name} is defined in two modules with different parameter lists — ${h.arity} parameter${h.arity === 1 ? "" : "s"} here, ${sameName.arity} at ${path.basename(sameName.file)}:${sameName.line}; the two modules disagree on one operation's signature, and the next test to import it gets whichever it finds first. Keep ONE definition, give every parameter the other lacked a default, and point every importer at it`,
891
+ detail: `${h.name} is defined in two modules with different parameter lists — ${h.params.length} parameter${h.params.length === 1 ? "" : "s"} here, ${sameName.params.length} at ${path.basename(sameName.file)}:${sameName.line}; the two modules disagree on one operation's signature, and the next test to import it gets whichever it finds first. Keep ONE definition, give every parameter the other lacked a default, and point every importer at it`,
879
892
  });
880
893
  }
881
894
  if (!inFile)
@@ -898,7 +911,8 @@ incumbents = new Set()) {
898
911
  line: h.line,
899
912
  // Only the parameter COUNT is compared, and only when both were read.
900
913
  detail: first.name === h.name
901
- ? `${h.name} is defined twice (also at ${path.basename(first.file)}:${first.line}) with the same ${h.arity !== undefined && h.arity === first.arity
914
+ ? `${h.name} is defined twice (also at ${path.basename(first.file)}:${first.line}) with the same ${h.params !== undefined &&
915
+ h.params.length === first.params?.length
902
916
  ? "parameter count and the same "
903
917
  : ""}action sequence: ${describeActionKey(h.actionKey)}; keep one definition and point every importer at it`
904
918
  : `${h.name} and ${first.name} (${path.basename(first.file)}:${first.line}) perform the same action sequence: ${describeActionKey(h.actionKey)}`,
@@ -608,6 +608,7 @@ async function domEvaluationScript(options) {
608
608
  if (m && /[0-9]/.test(m[1])) return true;
609
609
  if (/[a-zA-Z][0-9]{3,}$/.test(id)) return true;
610
610
  if (id.includes(":")) return true;
611
+ if (/«r[0-9a-z]*»|(^|[-_])_?r_[0-9a-z]+_/.test(id)) return true;
611
612
  if (/^.+__search_[a-zA-Z0-9]{4,}$/.test(id)) return true;
612
613
  return false;
613
614
  }
@@ -34,6 +34,7 @@ function isDynamicId(id) {
34
34
  if (shortHexMatch && /[0-9]/.test(shortHexMatch[1])) return true;
35
35
  if (/[a-zA-Z][0-9]{3,}$/.test(id)) return true;
36
36
  if (id.includes(":")) return true;
37
+ if (/«r[0-9a-z]*»|(^|[-_])_?r_[0-9a-z]+_/.test(id)) return true;
37
38
  if (/^.+__search_[a-zA-Z0-9]{4,}$/.test(id)) return true;
38
39
  return false;
39
40
  }
@@ -65,6 +65,12 @@ test("isDynamicId: single trailing digit after letter is not dynamic", () => {
65
65
  assertFalse((0, import_dynamicId.isDynamicId)("item1"), "item1 (single digit)");
66
66
  assertFalse((0, import_dynamicId.isDynamicId)("item12"), "item12 (2 digits, below 3-digit threshold)");
67
67
  });
68
+ test("isDynamicId: React 19 useId forms", () => {
69
+ assertTrue((0, import_dynamicId.isDynamicId)("\xABr2\xBB-form-item"), "\xABr2\xBB-form-item (React 19.0)");
70
+ assertTrue((0, import_dynamicId.isDynamicId)("radix-_r_16_"), "radix-_r_16_ (React 19.1 inside Radix)");
71
+ assertTrue((0, import_dynamicId.isDynamicId)("_r_1k_"), "_r_1k_ (bare React 19.1)");
72
+ assertFalse((0, import_dynamicId.isDynamicId)("user_r_name"), "user_r_name (word, not a useId)");
73
+ });
68
74
  let failed = 0;
69
75
  for (const { name, run } of cases) {
70
76
  try {