@skyramp/mcp 0.4.2-rc.1 → 0.4.2-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 (104) hide show
  1. package/build/commands/localDevTestChangesCommand.js +1 -1
  2. package/build/commands/recommendTestsAndExecuteCommand.js +10 -1
  3. package/build/commands/testThisEndpointCommand.js +19 -2
  4. package/build/execution/wrapperConfig.d.ts +56 -0
  5. package/build/execution/wrapperConfig.js +155 -0
  6. package/build/index.js +6 -6
  7. package/build/playwright/registerPlaywrightTools.js +14 -0
  8. package/build/playwright/traceExportStore.d.ts +22 -0
  9. package/build/playwright/traceExportStore.js +81 -0
  10. package/build/playwright/traceRecordingPrompt.js +2 -1
  11. package/build/prompts/code-reuse.js +24 -21
  12. package/build/prompts/local-dev/local-dev-plan.js +6 -23
  13. package/build/prompts/local-dev/local-dev-prompts.js +1 -1
  14. package/build/prompts/shared-helper-policy.d.ts +36 -0
  15. package/build/prompts/shared-helper-policy.js +33 -1
  16. package/build/prompts/startTraceCollectionPrompts.js +1 -1
  17. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +6 -7
  18. package/build/prompts/sut-setup/shared.d.ts +1 -1
  19. package/build/prompts/sut-setup/shared.js +5 -3
  20. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +16 -8
  21. package/build/prompts/test-maintenance/drift-analysis-prompt.js +90 -36
  22. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +1 -1
  23. package/build/prompts/test-recommendation/recommendationShared.d.ts +1 -1
  24. package/build/prompts/test-recommendation/recommendationShared.js +0 -1
  25. package/build/prompts/testbot/testbot-prompts.js +11 -9
  26. package/build/services/TestDiscoveryService.js +32 -4
  27. package/build/skills/runTestSkill.d.ts +6 -0
  28. package/build/skills/runTestSkill.js +17 -0
  29. package/build/tool-phases.js +0 -1
  30. package/build/tools/budgetExcuse.d.ts +15 -0
  31. package/build/tools/budgetExcuse.js +113 -0
  32. package/build/tools/code-refactor/utils-verify-gates.js +17 -3
  33. package/build/tools/executeSkyrampTestTool.d.ts +97 -48
  34. package/build/tools/executeSkyrampTestTool.js +775 -449
  35. package/build/tools/submitReportTool.js +128 -0
  36. package/build/tools/test-management/actionsTool.js +31 -0
  37. package/build/tools/test-management/analyzeChangesTool.d.ts +4 -4
  38. package/build/tools/test-management/analyzeTestHealthTool.d.ts +0 -11
  39. package/build/tools/test-management/analyzeTestHealthTool.js +7 -63
  40. package/build/tools/test-management/testsOwedBeforeRun.d.ts +28 -0
  41. package/build/tools/test-management/testsOwedBeforeRun.js +53 -0
  42. package/build/tools/trace/stopTraceCollectionTool.js +1 -1
  43. package/build/types/RepositoryAnalysis.d.ts +32 -32
  44. package/build/types/ReuseOutcome.d.ts +4 -3
  45. package/build/types/TestExecution.d.ts +2 -2
  46. package/build/types/TestTypes.d.ts +3 -0
  47. package/build/types/TestTypes.js +6 -0
  48. package/build/utils/AnalysisStateManager.d.ts +0 -7
  49. package/build/utils/AnalysisStateManager.js +1 -1
  50. package/build/utils/connectionErrors.d.ts +10 -0
  51. package/build/utils/connectionErrors.js +10 -0
  52. package/build/utils/language-helper.js +24 -3
  53. package/build/utils/progress.d.ts +1 -1
  54. package/build/utils/progress.js +1 -1
  55. package/build/utils/rebaselineSnapshots.d.ts +1 -1
  56. package/build/utils/rebaselineSnapshots.js +6 -16
  57. package/build/utils/reuseRouting.d.ts +10 -0
  58. package/build/utils/reuseRouting.js +15 -0
  59. package/build/utils/runContextGauge.d.ts +27 -0
  60. package/build/utils/runContextGauge.js +181 -0
  61. package/build/utils/skyrampMdContent.d.ts +1 -1
  62. package/build/utils/skyrampMdContent.js +1 -1
  63. package/build/utils/skyrampSdkVersion.d.ts +9 -0
  64. package/build/utils/skyrampSdkVersion.js +16 -0
  65. package/build/utils/testDependencyPolicy.js +21 -0
  66. package/build/utils/testExecutionRecord.d.ts +5 -1
  67. package/build/utils/testExecutionRecord.js +3 -1
  68. package/build/utils/testFileClassification.d.ts +8 -0
  69. package/build/utils/testFileClassification.js +36 -3
  70. package/build/utils/utils-verify/action-key.d.ts +42 -0
  71. package/build/utils/utils-verify/action-key.js +118 -36
  72. package/build/utils/utils-verify/action-sites.d.ts +32 -0
  73. package/build/utils/utils-verify/action-sites.js +202 -0
  74. package/build/utils/utils-verify/body-reach.js +2 -4
  75. package/build/utils/utils-verify/call-sites.d.ts +25 -6
  76. package/build/utils/utils-verify/call-sites.js +8 -5
  77. package/build/utils/utils-verify/index.d.ts +1 -0
  78. package/build/utils/utils-verify/index.js +1 -0
  79. package/build/utils/utils-verify/language-spec.d.ts +25 -0
  80. package/build/utils/utils-verify/language-spec.js +16 -2
  81. package/build/utils/utils-verify/parse.d.ts +10 -1
  82. package/build/utils/utils-verify/parse.js +19 -2
  83. package/build/utils/utils-verify/verify.d.ts +3 -2
  84. package/build/utils/utils-verify/verify.js +16 -18
  85. package/build/workspace/workspace.d.ts +72 -52
  86. package/build/workspace/workspace.js +12 -8
  87. package/package.json +1 -1
  88. package/plugin/prompts/testbot-task1.md +0 -2
  89. package/plugin/skills/fix-test-import-errors/SKILL.md +2 -1
  90. package/plugin/skills/run-test/SKILL.md +16 -0
  91. package/build/adapters/jestAdapter.d.ts +0 -14
  92. package/build/adapters/jestAdapter.js +0 -131
  93. package/build/adapters/mochaAdapter.d.ts +0 -13
  94. package/build/adapters/mochaAdapter.js +0 -93
  95. package/build/adapters/playwrightAdapter.d.ts +0 -17
  96. package/build/adapters/playwrightAdapter.js +0 -184
  97. package/build/adapters/pytestAdapter.d.ts +0 -15
  98. package/build/adapters/pytestAdapter.js +0 -119
  99. package/build/tools/runExistingTestsTool.d.ts +0 -138
  100. package/build/tools/runExistingTestsTool.js +0 -666
  101. package/build/types/ExternalTestExecution.d.ts +0 -67
  102. package/build/types/ExternalTestExecution.js +0 -8
  103. package/build/workspace/testSuites.d.ts +0 -20
  104. package/build/workspace/testSuites.js +0 -17
@@ -1,7 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { SKYRAMP_UTILS_HEADER } from "../utils.js";
4
- import { blankCommentsAndStrings, isSingleLiteral, parseUtilsFile, } from "./parse.js";
4
+ import { blankCommentsAndStrings, isSingleLiteral, localHelperAt, localHelpersOf, parseUtilsFile, } from "./parse.js";
5
5
  import { normalizePath } from "./language-spec.js";
6
6
  import { realpath } from "./locate.js";
7
7
  import { escapeRegExp } from "../regex.js";
@@ -66,7 +66,9 @@ export const REQUEST_CALL_RE = /\b(?:send_request|sendRequest)\s*\(/g;
66
66
  * normalised path, nothing else — literals are what the call's arguments are for.
67
67
  *
68
68
  * Advisory by design: it names a change to a PRE-EXISTING test, which the maintenance
69
- * flow owns, so it informs rather than gates. Never throws.
69
+ * flow owns, so it informs rather than gates. Never throws. The API family's scan;
70
+ * findSiblingInlineActionSites is the browser family's, and inlineCallSiteFamily
71
+ * decides which runs.
70
72
  */
71
73
  export async function findSiblingInlineCallSites(testFile, utilsFiles, _helpers, spec, preReadSiblings) {
72
74
  const byRoute = await helperRoutes(utilsFiles, spec);
@@ -91,6 +93,7 @@ export async function findSiblingInlineCallSites(testFile, utilsFiles, _helpers,
91
93
  }))
92
94
  continue;
93
95
  out.push({
96
+ kind: "request",
94
97
  file,
95
98
  line: c.line,
96
99
  method: c.method,
@@ -178,7 +181,7 @@ async function helperRoutes(utilsFiles, spec) {
178
181
  /** The generated siblings less the utils files themselves (a utils file carries no
179
182
  * codegen marker, so this is belt and braces). Pre-read siblings are used as given —
180
183
  * the caller's walk decides whether the spec itself is among them. */
181
- async function nonUtilsSiblings(testFile, utilsFiles, spec, preReadSiblings, includeSelf) {
184
+ export async function nonUtilsSiblings(testFile, utilsFiles, spec, preReadSiblings, includeSelf) {
182
185
  const utilsSet = new Set(await Promise.all(utilsFiles.map((f) => realpath(f))));
183
186
  return (preReadSiblings ??
184
187
  (await generatedSiblings(testFile, spec, { includeSelf }))).filter((sib) => !utilsSet.has(path.resolve(sib.file)));
@@ -189,7 +192,7 @@ async function nonUtilsSiblings(testFile, utilsFiles, spec, preReadSiblings, inc
189
192
  * commented-out or quoted request is never a site and a `)` inside a string never
190
193
  * truncates the argument text, which is read from the original. */
191
194
  function requestCallsIn(content, spec) {
192
- const localHelpers = parseUtilsFile(content, spec).filter((h) => !/^test/i.test(h.name));
195
+ const localHelpers = localHelpersOf(content, spec);
193
196
  const scan = blankCommentsAndStrings(content, spec);
194
197
  const out = [];
195
198
  for (const m of scan.matchAll(REQUEST_CALL_RE)) {
@@ -205,7 +208,7 @@ function requestCallsIn(content, spec) {
205
208
  if (!method || !pm || !isSingleLiteral(call, pm))
206
209
  continue;
207
210
  const p = pm[1];
208
- const owner = localHelpers.find((h) => line >= h.line && line <= h.endLine);
211
+ const owner = localHelperAt(localHelpers, line);
209
212
  out.push({
210
213
  line,
211
214
  start,
@@ -5,5 +5,6 @@ export * from "./locate.js";
5
5
  export * from "./verify.js";
6
6
  export * from "./stage.js";
7
7
  export * from "./call-sites.js";
8
+ export * from "./action-sites.js";
8
9
  export * from "./retrofit-equivalence.js";
9
10
  export * from "./in-house.js";
@@ -5,5 +5,6 @@ export * from "./locate.js";
5
5
  export * from "./verify.js";
6
6
  export * from "./stage.js";
7
7
  export * from "./call-sites.js";
8
+ export * from "./action-sites.js";
8
9
  export * from "./retrofit-equivalence.js";
9
10
  export * from "./in-house.js";
@@ -78,7 +78,32 @@ export interface HelperFamilySpec {
78
78
  /** One clause for the fix-instruction sentence — composed by the gate, never
79
79
  * restated there (the browser clause reads as a continuation of the api one). */
80
80
  fixRule: string;
81
+ /** The sibling inline call-site advisory's family-specific half: what a site is
82
+ * called, what the helper already does with it, and how to replace it. The gate
83
+ * appends the tail both families share. */
84
+ inlineSite: {
85
+ /** The prompt's noun for a site — `HelperPolicy.inlineSite.noun`. */
86
+ noun: string;
87
+ /** `… that a shared helper already <verb>:` */
88
+ already: string;
89
+ /** What makes a site a candidate, and what the replacement keeps. */
90
+ instruction: string;
91
+ /** The copy as the shared tail names it inside a local helper: `that request`
92
+ * / `the sequence`. */
93
+ localCopy: string;
94
+ };
81
95
  }
96
+ /**
97
+ * The RECEIVED value inside `expect(...)` — its first argument, up to the matcher.
98
+ *
99
+ * Only the first, because Playwright takes a message as the second: `expect(locator,
100
+ * "why this matters")`. Reading every argument let that message decide, so a
101
+ * structural wait whose message merely NAMED a text-selecting form was refused — and
102
+ * a refusal blocks a delivery that is right, which is worse than missing one that is
103
+ * wrong. The split walks bracket depth on a copy whose strings are blanked, so a comma
104
+ * inside a selector or a message does not end the argument.
105
+ */
106
+ export declare function expectArgs(expr: string): string;
82
107
  export declare const HELPER_FAMILIES: Record<HelperFamily, HelperFamilySpec>;
83
108
  /** The helper family a test type's shared helpers follow. Unlike `isBrowserTestType`,
84
109
  * an ABSENT test type resolves to `api`: this runs against a utils file that already
@@ -10,7 +10,7 @@
10
10
  * SKYR-4196 Part B runs 5 and 11 leaked a scenario-literal body assertion into a shared
11
11
  * helper despite both rules being in the prompt.
12
12
  */
13
- import { HELPER_ASSERTION_POLICIES, CONTENT_ROLES, } from "../../prompts/shared-helper-policy.js";
13
+ import { API_HELPERS, BROWSER_HELPERS, HELPER_ASSERTION_POLICIES, CONTENT_ROLES, } from "../../prompts/shared-helper-policy.js";
14
14
  import { escapeRegExp } from "../regex.js";
15
15
  /**
16
16
  * A Playwright name as the language SPELLS it: camelCase unchanged, Python
@@ -228,7 +228,7 @@ const ROLE_WITH_NAME_RE = /\b(?:getByRole|get_by_role)\s*\(\s*["'`]([A-Za-z]+)["
228
228
  * wrong. The split walks bracket depth on a copy whose strings are blanked, so a comma
229
229
  * inside a selector or a message does not end the argument.
230
230
  */
231
- function expectArgs(expr) {
231
+ export function expectArgs(expr) {
232
232
  const open = expr.indexOf("(", expr.indexOf("expect"));
233
233
  // `.not.to…` in TypeScript, `.not_to_…` in Python. Reading only the first left a
234
234
  // negated Python assertion with no end, so the whole source line — a trailing
@@ -325,6 +325,14 @@ export const HELPER_FAMILIES = {
325
325
  isAllowed: (spec, expr) => spec.allowedAssertion.test(expr),
326
326
  assertionRule: HELPER_ASSERTION_POLICIES.api.boundary,
327
327
  fixRule: "a helper that wraps an SDK request asserts only the status code — one helper per method+path, extended with defaulted parameters (the expected status excepted: it has no default and every call passes it), named method + resource",
328
+ inlineSite: {
329
+ // The prompt's word for the same thing, read from the policy so the advisory
330
+ // and the rule the agent followed name the site alike.
331
+ noun: API_HELPERS.inlineSite.noun,
332
+ already: "wraps",
333
+ instruction: "Each is a candidate: the request's method, path and call shape match the helper's. Replace the block with a call to the helper only if the remaining differences are literals (they become the arguments; keep the block's response variable; import the helper into that file; change nothing else there).",
334
+ localCopy: "that request",
335
+ },
328
336
  },
329
337
  browser: {
330
338
  family: "browser",
@@ -336,6 +344,12 @@ export const HELPER_FAMILIES = {
336
344
  !selectsByRenderedText(expectArgs(expr)),
337
345
  assertionRule: `${HELPER_ASSERTION_POLICIES.browser.boundary}. Move this value assertion to the test body immediately after the helper call, selector verbatim`,
338
346
  fixRule: "every other helper holds actions and structural waits only, named for the user intent",
347
+ inlineSite: {
348
+ noun: BROWSER_HELPERS.inlineSite.noun,
349
+ already: "performs",
350
+ instruction: "Each is a candidate: the sequence's actions and locators equal the helper's, in order. Replace the sequence with a call to the helper only if the remaining differences are literals — fill values, option labels, literal arguments — which become the arguments (import the helper into that file; every assertion and the page-error guard stay in the test body where they are; change nothing else there).",
351
+ localCopy: "the sequence",
352
+ },
339
353
  },
340
354
  };
341
355
  /** The helper family a test type's shared helpers follow. Unlike `isBrowserTestType`,
@@ -44,8 +44,10 @@ export interface UtilsHelper {
44
44
  /** The helper's Playwright action sequence — the duplicate key of a browser
45
45
  * helper, where an API helper has method+path. See actionKeyOf. Read for
46
46
  * TypeScript, JavaScript and Python (through the JavaScript spelling); absent
47
- * for a helper with no readable action. */
47
+ * for a helper with no readable action. `actionSteps` is the same sequence one
48
+ * step per entry, for the sibling scan that matches step by step. */
48
49
  actionKey?: string;
50
+ actionSteps?: string[];
49
51
  }
50
52
  /** The declared parameters of a signature, raw text trimmed, empty entries dropped
51
53
  * (a trailing comma). Reads the signature from the definition line to its body
@@ -91,6 +93,13 @@ export declare function unusableParamTypesOf(sigLines: string[]): string[];
91
93
  * next non-blank column-0 line; TS/JS bodies end when the brace depth opened by
92
94
  * the definition returns to zero.
93
95
  */
96
+ /** A generated TEST's own module-level helpers: what parseUtilsFile reads from it,
97
+ * less the test functions themselves (`test_…` / `testUi`), which are not helpers a
98
+ * call can sit "inside" in the sense the sibling scans and the body-reach check
99
+ * report. */
100
+ export declare function localHelpersOf(content: string, spec: UtilsLanguageSpec): UtilsHelper[];
101
+ /** The helper whose body holds `line` (1-based), if any. */
102
+ export declare function localHelperAt(helpers: UtilsHelper[], line: number): UtilsHelper | undefined;
94
103
  export declare function parseUtilsFile(content: string, spec: UtilsLanguageSpec): UtilsHelper[];
95
104
  /** True when the `path` value is a single string literal ending the argument — a
96
105
  * concatenation (`"/api/orders/" + str(id) + "/items"`) has no route this can name. */
@@ -1,5 +1,11 @@
1
1
  import { normalizePath } from "./language-spec.js";
2
- import { actionKeyOf } from "./action-key.js";
2
+ import { actionStepKeys, actionStepsOf, } from "./action-key.js";
3
+ /** A helper's action steps and their joined key, read once from its body. */
4
+ function actionSequence(body, language) {
5
+ const steps = actionStepsOf(body, language);
6
+ const keys = actionStepKeys(steps);
7
+ return keys ? { actionKey: keys.join("; "), actionSteps: keys } : {};
8
+ }
3
9
  import { fixedSleepsIn, pythonSleepBindings, } from "./fixed-sleep.js";
4
10
  import { stripComments, stripCommentsAndStrings, stripStrings, } from "../pom-scope/strip.js";
5
11
  /** Whether the `<` at `i` opens a generic (`Record<string, …>`, `Array<{…}>`) rather
@@ -204,6 +210,17 @@ export function unusableParamTypesOf(sigLines) {
204
210
  * next non-blank column-0 line; TS/JS bodies end when the brace depth opened by
205
211
  * the definition returns to zero.
206
212
  */
213
+ /** A generated TEST's own module-level helpers: what parseUtilsFile reads from it,
214
+ * less the test functions themselves (`test_…` / `testUi`), which are not helpers a
215
+ * call can sit "inside" in the sense the sibling scans and the body-reach check
216
+ * report. */
217
+ export function localHelpersOf(content, spec) {
218
+ return parseUtilsFile(content, spec).filter((h) => !/^test/i.test(h.name));
219
+ }
220
+ /** The helper whose body holds `line` (1-based), if any. */
221
+ export function localHelperAt(helpers, line) {
222
+ return helpers.find((h) => line >= h.line && line <= h.endLine);
223
+ }
207
224
  export function parseUtilsFile(content, spec) {
208
225
  const lines = content.split("\n");
209
226
  // Structure — where a body ends — is read on a copy with comments and string
@@ -263,7 +280,7 @@ export function parseUtilsFile(content, spec) {
263
280
  fixedSleeps: fixedSleepsIn(body, scan, spec.language, params ?? [], start + 1, sleepBindings),
264
281
  // Comments blanked, strings kept: a commented-out click is not a step, and
265
282
  // the selector literals are the key.
266
- actionKey: actionKeyOf(blankComments(body.join("\n"), spec), spec.language),
283
+ ...actionSequence(blankComments(body.join("\n"), spec), spec.language),
267
284
  };
268
285
  let requests = 0;
269
286
  for (let k = 0; k < body.length; k++) {
@@ -50,8 +50,9 @@ export interface UtilsVerifyResult {
50
50
  * a helper that issues an SDK request is judged by the api rules regardless. See
51
51
  * helperFamilyOf. */
52
52
  fallbackFamily: HelperFamilySpec;
53
- /** Advisory (does not affect `ok`): inline request calls in sibling generated tests
54
- * that a utils helper already wraps — see findSiblingInlineCallSites. API family only. */
53
+ /** Advisory (does not affect `ok`): inline request calls (integration) or inline
54
+ * action sequences (seeded UI) in sibling generated tests that a utils helper
55
+ * already wraps — see findSiblingInlineCallSites. */
55
56
  inlineCallSites: InlineCallSite[];
56
57
  /** Advisory (does not affect `ok`): operations written in two or more sibling
57
58
  * generated tests (this one included) that NO utils helper wraps — the extraction
@@ -7,10 +7,12 @@ 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 { isModularizeFirstTarget } from "../reuseRouting.js";
10
+ import { inlineCallSiteFamily, isModularizeFirstTarget, } from "../reuseRouting.js";
11
+ import { describeActionKey } from "./action-key.js";
11
12
  import { findDuplicateBodyReach, findUnreachableBodies } from "./body-reach.js";
12
13
  import { misnamedNewModules } from "./module-name.js";
13
14
  import { additiveChanges, locateInHouseModules, resolveInHouseCalls, } from "./in-house.js";
15
+ import { findSiblingInlineActionSites } from "./action-sites.js";
14
16
  import { findSiblingInlineCallSites, findUnextractedDuplicates, generatedSiblings, } from "./call-sites.js";
15
17
  import { parseUtilsAllows, } from "./allow.js";
16
18
  import { describeFixedSleeps, fixedSleepReplacement } from "./fixed-sleep.js";
@@ -633,17 +635,24 @@ export async function verifyUtils(params) {
633
635
  });
634
636
  result.ok =
635
637
  result.violations.length === 0 && result.malformedAllows.length === 0;
636
- // Integration only — the same gate as the prompt rule. `contract`, `load` and an
637
- // absent type also resolve to the API family, but the reuse flow never targets them,
638
- // so an advisory (and a report count) there would describe reuse the feature could
639
- // not have taken.
640
- if ((params.testType ?? "").toLowerCase() === "integration") {
638
+ // The gate the prompt renders the rule on: one predicate, so the advisory can
639
+ // never describe a rewiring the prompt did not ask for.
640
+ const scanFamily = inlineCallSiteFamily(params.testType, spec.language);
641
+ if (scanFamily) {
641
642
  try {
642
- result.inlineCallSites = await findSiblingInlineCallSites(params.testFile, utilsFiles, allHelpers, spec, siblings);
643
+ result.inlineCallSites =
644
+ scanFamily === "api"
645
+ ? await findSiblingInlineCallSites(params.testFile, utilsFiles, allHelpers, spec, siblings)
646
+ : await findSiblingInlineActionSites(params.testFile, helpersByFile, spec, siblings);
643
647
  }
644
648
  catch {
645
649
  /* advisory only — never fails a verify */
646
650
  }
651
+ }
652
+ // Integration only — `contract`, `load` and an absent type also resolve to the API
653
+ // family, but the reuse flow never targets them.
654
+ const integration = (params.testType ?? "").toLowerCase() === "integration";
655
+ if (integration) {
647
656
  try {
648
657
  result.unextractedDuplicates = await findUnextractedDuplicates(params.testFile, utilsFiles, spec, siblings);
649
658
  }
@@ -953,14 +962,3 @@ incumbents = new Set()) {
953
962
  }
954
963
  return out;
955
964
  }
956
- /** `click empty-cart-btn; fill qty` → `click \`empty-cart-btn\`; fill \`qty\`` — the
957
- * key as the agent reads it, selectors quoted so a `#id` never reads as prose. */
958
- function describeActionKey(key) {
959
- return key
960
- .split("; ")
961
- .map((step) => {
962
- const at = step.indexOf(" ");
963
- return `${step.slice(0, at)} \`${step.slice(at + 1)}\``;
964
- })
965
- .join("; ");
966
- }