@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
@@ -7,19 +7,21 @@ 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 { addedSinceHead, changedSinceHead, contentAtHead, readAtHead, } from "./head.js";
24
+ import { alignLines, BaselineErrors, baselineErrorKey, inheritedTypeError, } from "./changed-lines.js";
23
25
  import { incumbentSignatureViolations } from "./incumbent.js";
24
26
  import { helperFamilyFor, helperFamilyOf, scenarioNameViolation, utilsSpecFor, utilsSpecForFile, } from "./language-spec.js";
25
27
  /** What the compiler says about a name defined twice, which `helper-redefined`
@@ -51,9 +53,11 @@ const REDECLARATION_CODES = new Set([2393, 2323, 2451]);
51
53
  * returned. See status-once.ts.
52
54
  * - `type-error` (TypeScript): the delivered module or spec fails the compiler with
53
55
  * every strictness flag off — an error under every configuration (a path parameter
54
- * typed `unknown`, request options built as a record and handed to `sendRequest`).
55
- * An error only the repository's own tsconfig raises is the `tsconfig-type-error`
56
- * ADVISORY. See typecheckDelivered.
56
+ * typed `unknown`, request options built as a record and handed to `sendRequest`)
57
+ * — and this run CAUSED it: an error the file already had at HEAD on an
58
+ * unchanged line is the `inherited-type-error` ADVISORY, and an error only the
59
+ * repository's own tsconfig raises is the `tsconfig-type-error` ADVISORY. See
60
+ * typecheckDelivered and inheritedTypeError.
57
61
  *
58
62
  * Analyses the utils files' INTERNAL invariants — unlike the POM verifier, which
59
63
  * checks a spec's cross-file references — so it reads the utils files, not the spec,
@@ -80,9 +84,16 @@ export async function verifyUtils(params) {
80
84
  // locateInHouseModules). Found BEFORE the utils files so a customer module that
81
85
  // gained our header this run is judged by the additive gate, never by the Skyramp
82
86
  // invariants: the header locator would otherwise claim it.
83
- const inHouse = spec
87
+ const located = spec
84
88
  ? await locateInHouseModules({ testFile: params.testFile, spec, cwd: root })
85
89
  : [];
90
+ // Both reuse settings on: a module that declares a class is a page object, which
91
+ // the page-object pass owned and its verifier checked. The in-house parser cannot
92
+ // read a class, and its additive gate would judge a page-object edit it did not
93
+ // prescribe — so the split is by shape, the one pom-scope/ownership.ts states.
94
+ const pomFirst = !!spec && isPomFirstTarget(spec.language, params.testType);
95
+ const pageObjectFiles = new Set(pomFirst ? (await pageObjectCandidates(located)).map((m) => m.file) : []);
96
+ const inHouse = located.filter((m) => !pageObjectFiles.has(m.file));
86
97
  const inHouseFiles = new Set(inHouse.map((m) => m.file));
87
98
  const utilsFiles = spec
88
99
  ? (await locateUtilsFiles({
@@ -94,6 +105,8 @@ export async function verifyUtils(params) {
94
105
  const result = {
95
106
  ok: true,
96
107
  utilsFiles,
108
+ incumbentModules: [],
109
+ headUnknownModules: [],
97
110
  helpers: 0,
98
111
  imported: 0,
99
112
  importedNames: [],
@@ -107,6 +120,7 @@ export async function verifyUtils(params) {
107
120
  inlineCallSites: [],
108
121
  unextractedDuplicates: [],
109
122
  singleImporters: [],
123
+ importersComplete: false,
110
124
  };
111
125
  if (!spec)
112
126
  return result;
@@ -127,6 +141,11 @@ export async function verifyUtils(params) {
127
141
  * kept: both the status advisory's scope and the restatement's remediation
128
142
  * turn on it. */
129
143
  const incumbentsOfFile = new Map();
144
+ /** Per utils file, the module at HEAD — `undefined` for a module HEAD does not
145
+ * hold. The compiler's attribution reads it: an error the module already had at
146
+ * HEAD is not this run's. */
147
+ const headContentOfFile = new Map();
148
+ const contentOfFile = new Map();
130
149
  const allHelpers = [];
131
150
  const helpersByFile = new Map();
132
151
  const allAllows = [];
@@ -161,10 +180,17 @@ export async function verifyUtils(params) {
161
180
  // helpers this run writes, so a defaulted status on an incumbent is not advised
162
181
  // — and an incumbent's signature is held to what it was (incumbent.ts). Absent
163
182
  // for a module this run created, or outside git: then every helper is this run's.
164
- const baseline = await committedBaseline(file);
183
+ const atHead = await readAtHead(file);
184
+ const baseline = "content" in atHead ? atHead.content : undefined;
185
+ if (baseline !== undefined)
186
+ result.incumbentModules.push(file);
187
+ else if ("unknown" in atHead)
188
+ result.headUnknownModules.push(file);
165
189
  const headHelpers = baseline === undefined ? [] : parseUtilsFile(baseline, fileSpec);
166
190
  const incumbents = new Set(headHelpers.map((h) => h.name));
167
191
  incumbentsOfFile.set(file, incumbents);
192
+ headContentOfFile.set(file, baseline);
193
+ contentOfFile.set(file, content);
168
194
  pending.push(...incumbentSignatureViolations(headHelpers, helpers, file));
169
195
  for (const h of helpers) {
170
196
  // A static reading of the signature. An unannotated parameter is an error only
@@ -321,6 +347,9 @@ export async function verifyUtils(params) {
321
347
  }
322
348
  if (utilsSet.has(imp.resolved) || imp.resolved === realTestFile)
323
349
  continue;
350
+ // The page-object pass's module: checked by its verifier, not this one.
351
+ if (pageObjectFiles.has(imp.resolved))
352
+ continue;
324
353
  if (inHouseFiles.has(imp.resolved)) {
325
354
  if (!inHouseResult)
326
355
  unchecked.push(imp.resolved);
@@ -372,7 +401,7 @@ export async function verifyUtils(params) {
372
401
  if (!inHouseResult && inHouse.length > 0)
373
402
  result.inHouseChecksSkipped = `this pass ran ${params.testType
374
403
  ? `as a ${params.testType.toLowerCase()} test`
375
- : "with no test type supplied"}, and they run for a UI test with page-object reuse off`;
404
+ : "with no test type supplied"}, and they run for a UI test in TypeScript or JavaScript with utils reuse on`;
376
405
  }
377
406
  // The spec asserts a status a helper it calls already asserted on the response it
378
407
  // returned. Spec-level, like assertion-loss: the finding names the spec and the
@@ -419,8 +448,10 @@ export async function verifyUtils(params) {
419
448
  // The compiler over the delivered files: the shared module(s) and the spec that
420
449
  // imports them. The executor transpiles TypeScript without typechecking it, so
421
450
  // this is the first typecheck the files meet before the customer's own. An error
422
- // under the permissive settings fails under every configuration and blocks; one
423
- // only the repository's tsconfig raises advises (see typecheckDelivered).
451
+ // under the permissive settings fails under every configuration and blocks when
452
+ // this run caused it — one the file already had at HEAD advises (see the
453
+ // attribution below); one only the repository's tsconfig raises advises (see
454
+ // typecheckDelivered).
424
455
  // Over the TypeScript files only, by extension: a JavaScript module is not
425
456
  // typechecked (its spec would be `checkJs`, which no delivered file relies on),
426
457
  // and a JavaScript spec is not compiled beside a TypeScript module.
@@ -432,7 +463,7 @@ export async function verifyUtils(params) {
432
463
  const realSpec = await realpath(params.testFile);
433
464
  const specFile = realSpec && isTs(realSpec) ? realSpec : undefined;
434
465
  const files = specFile ? [...tsUtils, specFile] : tsUtils;
435
- const tc = await typecheckDelivered(files);
466
+ const tc = await typecheckDelivered(files, { anchor: specFile });
436
467
  result.typecheck = tc;
437
468
  const subjectOf = (file, line) => file === specFile
438
469
  ? path.basename(specFile).replace(/\W/g, "_")
@@ -447,11 +478,96 @@ export async function verifyUtils(params) {
447
478
  const redefined = new Set(pending
448
479
  .filter((v) => v.kind === "helper-redefined")
449
480
  .map((v) => `${v.file}\0${v.helper}`));
481
+ // The files as the repository held them BEFORE this run — each at HEAD —
482
+ // compiled once more, so that an error is excused only when the repository
483
+ // already had it on the same line: attribution by CAUSE. A file HEAD does not
484
+ // hold (a module or a spec this run created) is hidden from that compile and
485
+ // every error in it is this run's — the generator's included: a line any
486
+ // pass of this run wrote is the run's to fix, and only what the customer or
487
+ // an earlier run committed is not.
488
+ const baselineContents = new Map();
489
+ const hidden = new Set();
490
+ const attribution = {
491
+ ran: false,
492
+ headUnknown: [...result.headUnknownModules],
493
+ };
494
+ result.attribution = attribution;
495
+ for (const file of tsUtils) {
496
+ const head = headContentOfFile.get(file);
497
+ if (head === undefined)
498
+ hidden.add(file);
499
+ else
500
+ baselineContents.set(file, head);
501
+ }
502
+ if (specFile) {
503
+ const specAtHead = await readAtHead(specFile);
504
+ if ("content" in specAtHead)
505
+ baselineContents.set(specFile, specAtHead.content);
506
+ else {
507
+ hidden.add(specFile);
508
+ if ("unknown" in specAtHead)
509
+ attribution.headUnknown.push(specFile);
510
+ }
511
+ }
512
+ // Every OTHER file the program may reach — a page object, an in-house helper,
513
+ // a config module — as HEAD held it too: one the run changed is served from
514
+ // HEAD, one the run added does not exist. Read from the working tree, a
515
+ // page-object edit put its error into both compiles and it was excused as
516
+ // inherited. Only the extensions the compiler reads; nothing under
517
+ // node_modules, which the run does not edit.
518
+ // Nothing to attribute on a clean compile: the HEAD compile — a full second
519
+ // program build — is skipped, and the skip is stated so the field is never
520
+ // read as "attribution ran and excused nothing".
521
+ const baselineErrors = new BaselineErrors();
522
+ if (tc.errors.length === 0)
523
+ attribution.reason = "the delivered files compiled clean";
524
+ else if (baselineContents.size === 0)
525
+ attribution.reason =
526
+ "no delivered file exists at HEAD, so there is nothing to inherit from";
527
+ else if (!(await addBaselineForChangedFiles(root, baselineContents, hidden)))
528
+ attribution.reason =
529
+ "the files this run changed could not be listed, so no file could be read at HEAD";
530
+ else {
531
+ const before = await typecheckDelivered([...baselineContents.keys()], {
532
+ anchor: specFile,
533
+ baseline: { contents: baselineContents, hidden },
534
+ });
535
+ if (!before.ran) {
536
+ // The delivered compile ran and this one did not: something in the HEAD
537
+ // content or the host. Stated, never silent — the old errors then block
538
+ // as this run's, which is the outcome attribution exists to prevent.
539
+ attribution.reason = `the HEAD compile did not run: ${before.reason ?? "unknown cause"}`;
540
+ logger.warning("The HEAD compile did not run — no type error will be excused as inherited", { testFile: params.testFile, reason: before.reason });
541
+ }
542
+ else {
543
+ attribution.ran = true;
544
+ for (const e of before.errors)
545
+ baselineErrors.add(baselineErrorKey(e.file, e.line, e.code, e.message));
546
+ }
547
+ }
548
+ const unchangedOf = new Map();
549
+ for (const [file, before] of baselineContents) {
550
+ const now = file === specFile ? specContent : contentOfFile.get(file);
551
+ if (now !== undefined)
552
+ unchangedOf.set(file, alignLines(before, now));
553
+ }
554
+ const inherited = inheritedTypeError((file) => unchangedOf.get(file), baselineErrors);
450
555
  for (const e of tc.errors) {
451
556
  const helper = subjectOf(e.file, e.line);
452
557
  if (REDECLARATION_CODES.has(e.code) &&
453
558
  redefined.has(`${e.file}\0${helper}`))
454
559
  continue;
560
+ const why = inherited(e.file, e.line, e.code, e.message);
561
+ if (why !== undefined) {
562
+ result.advisories.push({
563
+ kind: "inherited-type-error",
564
+ file: e.file,
565
+ helper,
566
+ line: e.line,
567
+ detail: `\`TS${e.code}: ${e.message}\` — ${why}`,
568
+ });
569
+ continue;
570
+ }
455
571
  pending.push({
456
572
  kind: "type-error",
457
573
  file: e.file,
@@ -478,12 +594,13 @@ export async function verifyUtils(params) {
478
594
  });
479
595
  }
480
596
  // One directory walk feeds every sibling advisory: each sibling is read exactly once.
481
- let siblings = [];
597
+ let siblings = { files: [], complete: false };
482
598
  try {
483
599
  siblings = await generatedSiblings(params.testFile, spec, {
484
600
  includeSelf: true,
485
601
  });
486
602
  result.singleImporters = await singleImporterHelpers(params.testFile, utilsFiles, helpersByFile, spec, siblings);
603
+ result.importersComplete = siblings.complete;
487
604
  }
488
605
  catch {
489
606
  /* advisory only — never fails a verify */
@@ -673,6 +790,50 @@ function remediation(helper, literal, incumbent) {
673
790
  }
674
791
  /** The compared counts as one phrase, shared by the assertion-loss detail and the
675
792
  * PASSED line's note so the two cannot drift. */
793
+ /** Extensions the compiler reads as source. */
794
+ const COMPILED_EXTENSIONS = new Set([
795
+ ".ts",
796
+ ".tsx",
797
+ ".mts",
798
+ ".cts",
799
+ ".js",
800
+ ".jsx",
801
+ ".mjs",
802
+ ".cjs",
803
+ ".json",
804
+ ]);
805
+ /**
806
+ * Add to the baseline every compiled file in the repository that differs from
807
+ * HEAD and is not already accounted for: changed files served from HEAD, added
808
+ * files hidden. Returns whether the listing succeeded — a baseline that cannot
809
+ * be completed must excuse NOTHING, since any file it reads from disk may carry
810
+ * the run's own edit; the caller then compiles no baseline and every error
811
+ * blocks, as before attribution existed.
812
+ */
813
+ async function addBaselineForChangedFiles(root, contents, hidden) {
814
+ if (!root)
815
+ return true; // outside git nothing has a HEAD; nothing is excused
816
+ const compiled = (f) => COMPILED_EXTENSIONS.has(path.extname(f)) &&
817
+ !f.split(path.sep).includes("node_modules");
818
+ try {
819
+ for (const file of await changedSinceHead(root, { filter: "M" })) {
820
+ if (!compiled(file) || contents.has(file) || hidden.has(file))
821
+ continue;
822
+ const head = await contentAtHead(file, root);
823
+ if (head === undefined)
824
+ return false; // git did not answer: excuse nothing
825
+ contents.set(file, head);
826
+ }
827
+ for (const file of await addedSinceHead(root))
828
+ if (compiled(file) && !contents.has(file))
829
+ hidden.add(file);
830
+ return true;
831
+ }
832
+ catch (err) {
833
+ logger.warning("Could not list the files this run changed — no type error will be excused as inherited", { root, error: String(err) });
834
+ return false;
835
+ }
836
+ }
676
837
  export function describeAssertionCount(c) {
677
838
  return `${c.baseline} assertions at the modularization hand-out, ${c.delivered} delivered (${c.inSpec} in the test, ${c.inHelpers} in the helpers it imports and calls)`;
678
839
  }
@@ -864,9 +1025,9 @@ incumbents = new Set()) {
864
1025
  });
865
1026
  }
866
1027
  else if (sameName &&
867
- h.arity !== undefined &&
868
- sameName.arity !== undefined &&
869
- h.arity !== sameName.arity &&
1028
+ h.params !== undefined &&
1029
+ sameName.params !== undefined &&
1030
+ h.params.length !== sameName.params.length &&
870
1031
  !(routeDuplicateOf?.file === sameName.file &&
871
1032
  routeDuplicateOf?.line === sameName.line)) {
872
1033
  nameReported = true;
@@ -875,7 +1036,7 @@ incumbents = new Set()) {
875
1036
  file,
876
1037
  helper: h.name,
877
1038
  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`,
1039
+ 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
1040
  });
880
1041
  }
881
1042
  if (!inFile)
@@ -898,7 +1059,8 @@ incumbents = new Set()) {
898
1059
  line: h.line,
899
1060
  // Only the parameter COUNT is compared, and only when both were read.
900
1061
  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
1062
+ ? `${h.name} is defined twice (also at ${path.basename(first.file)}:${first.line}) with the same ${h.params !== undefined &&
1063
+ h.params.length === first.params?.length
902
1064
  ? "parameter count and the same "
903
1065
  : ""}action sequence: ${describeActionKey(h.actionKey)}; keep one definition and point every importer at it`
904
1066
  : `${h.name} and ${first.name} (${path.basename(first.file)}:${first.line}) perform the same action sequence: ${describeActionKey(h.actionKey)}`,
@@ -1,4 +1,4 @@
1
- export declare const SKYRAMP_IMAGE_VERSION = "v1.3.46";
2
- export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.46";
3
- export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.46";
1
+ export declare const SKYRAMP_IMAGE_VERSION = "v1.3.47";
2
+ export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.47";
3
+ export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.47";
4
4
  export declare const WORKER_CONTROL_PORT = 35142;
@@ -1,4 +1,4 @@
1
- export const SKYRAMP_IMAGE_VERSION = "v1.3.46";
1
+ export const SKYRAMP_IMAGE_VERSION = "v1.3.47";
2
2
  export const EXECUTOR_DOCKER_IMAGE = `skyramp/executor:${SKYRAMP_IMAGE_VERSION}`;
3
3
  export const WORKER_DOCKER_IMAGE = `skyramp/worker:${SKYRAMP_IMAGE_VERSION}`;
4
4
  // Control port the Skyramp worker listens on (SDK `CONTAINER_PORT`).
@@ -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 {
@@ -88,10 +88,12 @@ Requested path: ${outputZip}` }],
88
88
  counter++;
89
89
  outputZip = `${base}_${counter}${ext}`;
90
90
  }
91
- const { jsonl: jsonlContent, actionCount, skipped } = (0, import_skyRampExport.buildJsonlContent)(ctx.trackedActions, "chromium", ctx.harPath);
91
+ const { jsonl: jsonlContent, actionCount, skipped, simplified } = (0, import_skyRampExport.buildJsonlContent)(ctx.trackedActions, "chromium", ctx.harPath);
92
92
  await (0, import_skyRampExport.writeSkyrampZip)(outputZip, jsonlContent, ctx.harPath);
93
93
  const skippedNote = skipped.length ? `
94
94
  Skipped (not exportable): ${[...new Set(skipped)].join(", ")}` : "";
95
+ const simplifiedNote = simplified.length ? `
96
+ Exported with a simplified locator (a part the trace format cannot carry was dropped, so the step may match more elements at replay): ${[...new Set(simplified)].join("; ")}` : "";
95
97
  const dropped = ctx.droppedActions ?? [];
96
98
  const droppedNote = dropped.length ? `
97
99
  Failed & dropped (not recorded): ${summarizeDropped(dropped)}` : "";
@@ -102,7 +104,7 @@ Failed & dropped (not recorded): ${summarizeDropped(dropped)}` : "";
102
104
  Skyramp zip written to ${outputZip}
103
105
 
104
106
  Contains:
105
- - skyramp_playwright.txt (${actionCount} actions)${skippedNote}${droppedNote}`
107
+ - skyramp_playwright.txt (${actionCount} actions)${skippedNote}${simplifiedNote}${droppedNote}`
106
108
  }]
107
109
  };
108
110
  };
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var snapshotLocators_exports = {};
20
+ __export(snapshotLocators_exports, {
21
+ exactLeaf: () => exactLeaf,
22
+ isInputLine: () => isInputLine,
23
+ isTestHookCss: () => isTestHookCss,
24
+ isVolatileCss: () => isVolatileCss,
25
+ leaf: () => leaf,
26
+ locatorFromSelector: () => locatorFromSelector,
27
+ renderSelector: () => renderSelector,
28
+ snapshotInputValue: () => snapshotInputValue,
29
+ snapshotLineText: () => snapshotLineText,
30
+ snapshotMatches: () => snapshotMatches,
31
+ unquoteYaml: () => unquoteYaml,
32
+ withNth: () => withNth
33
+ });
34
+ module.exports = __toCommonJS(snapshotLocators_exports);
35
+ var import_dynamicId = require("../../dom-analyzer/dynamicId");
36
+ const INPUT_ROLES = /* @__PURE__ */ new Set(["textbox", "searchbox", "combobox", "spinbutton"]);
37
+ function isInputLine(refLine) {
38
+ const role = refLine.match(/^\s*-\s*(\w+)/)?.[1];
39
+ return !!role && INPUT_ROLES.has(role);
40
+ }
41
+ function snapshotInputValue(snapLines, refIdx) {
42
+ if (refIdx < 0)
43
+ return "";
44
+ const refLine = snapLines[refIdx];
45
+ const inline = refLine.match(/\]:\s*(.+)$/)?.[1]?.trim();
46
+ if (inline)
47
+ return unquoteYaml(inline);
48
+ if (!isInputLine(refLine))
49
+ return "";
50
+ const indentOf = (line) => line.match(/^(\s*)/)?.[1]?.length ?? 0;
51
+ const indent = indentOf(refLine);
52
+ let childIndent;
53
+ for (let i = refIdx + 1; i < snapLines.length; i++) {
54
+ const lineIndent = indentOf(snapLines[i]);
55
+ if (lineIndent <= indent)
56
+ break;
57
+ childIndent ??= lineIndent;
58
+ if (lineIndent !== childIndent)
59
+ continue;
60
+ const child = snapLines[i].match(/^\s*-\s*text:\s*(.+?)\s*$/)?.[1];
61
+ if (child !== void 0)
62
+ return unquoteYaml(child);
63
+ }
64
+ return "";
65
+ }
66
+ function snapshotLineText(line) {
67
+ const quoted = line.match(/^\s*-\s*\w+\s+"([^"]*)"/);
68
+ if (quoted)
69
+ return quoted[1];
70
+ const colon = line.match(/\[ref=\w+\](?:\s*\[[^\]]*\])*\s*:\s*(.+?)\s*$/);
71
+ const value = colon?.[1]?.trim();
72
+ return value ? unquoteYaml(value) : null;
73
+ }
74
+ function unquoteYaml(value) {
75
+ if (/^"(?:[^"\\]|\\.)*"$/.test(value)) {
76
+ try {
77
+ return JSON.parse(value);
78
+ } catch {
79
+ return value;
80
+ }
81
+ }
82
+ return value;
83
+ }
84
+ function snapshotMatches(snapLines, locator) {
85
+ if (locator.next)
86
+ return void 0;
87
+ const text = locator.kind === "text" ? locator.body : locator.kind === "role" ? locator.options?.name : void 0;
88
+ if (typeof text !== "string")
89
+ return void 0;
90
+ const exact = !!locator.options?.exact;
91
+ const same = (v) => v !== null && (exact ? v === text : v.toLowerCase().includes(text.toLowerCase()));
92
+ return snapLines.filter((l) => {
93
+ if (locator.kind === "role") {
94
+ const m = l.match(/^\s*-\s*(\w+)\s+"([^"]*)"/);
95
+ return !!m && m[1] === locator.body && same(m[2]);
96
+ }
97
+ return same(snapshotLineText(l));
98
+ }).length;
99
+ }
100
+ function isVolatileCss(selector) {
101
+ return selector.startsWith("#") && (0, import_dynamicId.isDynamicId)(selector.slice(1));
102
+ }
103
+ function isTestHookCss(selector) {
104
+ return /^(?:[a-z][\w-]*)?\[data-[\w-]+=(?:"[^"]+"|'[^']+')\]$/i.test(selector);
105
+ }
106
+ function renderSelector(locator) {
107
+ const q = (v, exact) => `${JSON.stringify(v)}${exact ? "s" : "i"}`;
108
+ let head;
109
+ switch (locator.kind) {
110
+ case "test-id":
111
+ head = `internal:testid=[${locator.options?.attribute ?? "data-testid"}=${JSON.stringify(locator.body)}s]`;
112
+ break;
113
+ case "role":
114
+ head = `internal:role=${locator.body}${typeof locator.options?.name === "string" ? `[name=${q(locator.options.name, locator.options.exact)}]` : ""}`;
115
+ break;
116
+ case "text":
117
+ head = `internal:text=${q(locator.body, locator.options?.exact)}`;
118
+ break;
119
+ case "label":
120
+ head = `internal:label=${q(locator.body, locator.options?.exact)}`;
121
+ break;
122
+ case "has-text":
123
+ head = `internal:has-text=${q(locator.body, locator.options?.exact)}`;
124
+ break;
125
+ case "placeholder":
126
+ case "alt":
127
+ case "title":
128
+ head = `internal:attr=[${locator.kind}=${q(locator.body, locator.options?.exact)}]`;
129
+ break;
130
+ case "nth":
131
+ head = `nth=${locator.body}`;
132
+ break;
133
+ default:
134
+ head = String(locator.body);
135
+ break;
136
+ }
137
+ return locator.next ? `${head} >> ${renderSelector(locator.next)}` : head;
138
+ }
139
+ function leaf(locator) {
140
+ let tail = locator;
141
+ while (tail.next)
142
+ tail = tail.next;
143
+ return tail;
144
+ }
145
+ const copyChain = (l) => ({ ...l, options: { ...l.options }, ...l.next ? { next: copyChain(l.next) } : {} });
146
+ function exactLeaf(parsed, fullText) {
147
+ const locator = copyChain(parsed.locator);
148
+ const tail = leaf(locator);
149
+ const leafText = tail.kind === "text" ? tail.body : tail.kind === "role" ? tail.options?.name : void 0;
150
+ if (typeof leafText !== "string" || leafText !== fullText)
151
+ return parsed;
152
+ tail.options = { ...tail.options, exact: true };
153
+ return { selector: renderSelector(locator), locator };
154
+ }
155
+ function withNth(parsed, index) {
156
+ const locator = copyChain(parsed.locator);
157
+ leaf(locator).next = { kind: "nth", body: String(index), options: { attrs: [], exact: false } };
158
+ return { selector: renderSelector(locator), locator };
159
+ }
160
+ function selectorParts(selector) {
161
+ const parts = [];
162
+ let quote;
163
+ let depth = 0;
164
+ let start = 0;
165
+ for (let i = 0; i < selector.length; i++) {
166
+ const c = selector[i];
167
+ if (quote) {
168
+ if (c === "\\")
169
+ i++;
170
+ else if (c === quote)
171
+ quote = void 0;
172
+ } else if (c === '"' || c === "'") {
173
+ quote = c;
174
+ } else if (c === "[" || c === "(") {
175
+ depth++;
176
+ } else if (c === "]" || c === ")") {
177
+ depth--;
178
+ } else if (!depth && selector.startsWith(" >> ", i)) {
179
+ parts.push(selector.slice(start, i));
180
+ start = i + 4;
181
+ i += 3;
182
+ }
183
+ }
184
+ parts.push(selector.slice(start));
185
+ return parts;
186
+ }
187
+ const QUOTED = '("(?:[^"\\\\]|\\\\.)*")([is])';
188
+ function decodeQuoted(quoted) {
189
+ try {
190
+ return JSON.parse(quoted.replace(/[\u0000-\u001f]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")));
191
+ } catch {
192
+ return void 0;
193
+ }
194
+ }
195
+ function locatorFromSelector(selector, opts = {}) {
196
+ const links = [];
197
+ for (const part of selectorParts(selector)) {
198
+ let m;
199
+ let value;
200
+ if (m = part.match(/^nth=(-?\d+)$/)) {
201
+ links.push({ kind: "nth", body: m[1], options: { attrs: [], exact: false } });
202
+ } else if (m = part.match(new RegExp(`^internal:role=([a-z]+)(?:\\[name=${QUOTED}\\])?$`))) {
203
+ if (m[2] && (value = decodeQuoted(m[2])) === void 0)
204
+ return null;
205
+ links.push({ kind: "role", body: m[1], options: { attrs: [], exact: m[3] === "s", ...m[2] ? { name: value } : {} } });
206
+ } else if (m = part.match(new RegExp(`^internal:testid=\\[([\\w-]+)=${QUOTED}\\]$`))) {
207
+ if ((value = decodeQuoted(m[2])) === void 0)
208
+ return null;
209
+ links.push({ kind: "test-id", body: value, options: m[1] === "data-testid" ? {} : { attribute: m[1] } });
210
+ } else if (m = part.match(new RegExp(`^internal:(label|text|has-text)=${QUOTED}$`))) {
211
+ if ((value = decodeQuoted(m[2])) === void 0)
212
+ return null;
213
+ links.push({ kind: m[1], body: value, options: { exact: m[3] === "s" } });
214
+ } else if (m = part.match(new RegExp(`^internal:attr=\\[(placeholder|alt|title)=${QUOTED}\\]$`))) {
215
+ if ((value = decodeQuoted(m[2])) === void 0)
216
+ return null;
217
+ links.push({ kind: m[1], body: value, options: { exact: m[3] === "s" } });
218
+ } else if (/^(?:[a-z-]+=|internal:|\/\/|\.\.)/i.test(part) || !part.trim()) {
219
+ return null;
220
+ } else {
221
+ links.push({ kind: "default", body: part, options: {} });
222
+ }
223
+ }
224
+ while (!opts.keepTrailingNth && links.length && links[links.length - 1].kind === "nth")
225
+ links.pop();
226
+ if (!links.length || links[0].kind === "nth")
227
+ return null;
228
+ for (let i = links.length - 1; i > 0; i--)
229
+ links[i - 1].next = links[i];
230
+ return links[0];
231
+ }
232
+ // Annotate the CommonJS export names for ESM import in node:
233
+ 0 && (module.exports = {
234
+ exactLeaf,
235
+ isInputLine,
236
+ isTestHookCss,
237
+ isVolatileCss,
238
+ leaf,
239
+ locatorFromSelector,
240
+ renderSelector,
241
+ snapshotInputValue,
242
+ snapshotLineText,
243
+ snapshotMatches,
244
+ unquoteYaml,
245
+ withNth
246
+ });