@skyramp/mcp 0.3.7 → 0.3.9-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 (69) hide show
  1. package/build/playwright/registerPlaywrightTools.js +1 -0
  2. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +22 -1
  3. package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
  4. package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
  5. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +4 -1
  6. package/build/prompts/test-recommendation/diffExecutionPlan.js +41 -9
  7. package/build/prompts/test-recommendation/fullRepoCatalog.js +3 -2
  8. package/build/prompts/test-recommendation/recommendationSections.js +3 -3
  9. package/build/prompts/test-recommendation/scopeAssessment.d.ts +2 -2
  10. package/build/prompts/test-recommendation/scopeAssessment.js +58 -76
  11. package/build/prompts/test-recommendation/test-recommendation-prompt.js +14 -1
  12. package/build/prompts/testbot/testbot-prompts.js +35 -15
  13. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +39 -5
  14. package/build/recommendation/budgeters/shared.d.ts +21 -8
  15. package/build/recommendation/budgeters/shared.js +134 -56
  16. package/build/recommendation/planRanker.d.ts +5 -3
  17. package/build/recommendation/planRanker.js +1 -1
  18. package/build/services/containerEnv.d.ts +1 -1
  19. package/build/services/containerEnv.js +12 -0
  20. package/build/tools/executeSkyrampTestTool.d.ts +80 -0
  21. package/build/tools/executeSkyrampTestTool.js +246 -19
  22. package/build/tools/submitReportTool.d.ts +5 -5
  23. package/build/tools/submitReportTool.js +11 -2
  24. package/build/tools/test-management/actionsTool.js +71 -3
  25. package/build/tools/test-management/analyzeChangesTool.d.ts +10 -0
  26. package/build/tools/test-management/analyzeChangesTool.js +73 -0
  27. package/build/tools/test-management/registerTestPlanTool.d.ts +3 -3
  28. package/build/tools/test-management/registerTestPlanTool.js +29 -8
  29. package/build/types/RepositoryAnalysis.d.ts +10 -10
  30. package/build/types/TestAnalysis.d.ts +12 -0
  31. package/build/types/TestExecution.d.ts +4 -0
  32. package/build/types/TestRecommendation.d.ts +11 -1
  33. package/build/types/TestRecommendation.js +34 -0
  34. package/build/utils/AnalysisStateManager.d.ts +7 -0
  35. package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
  36. package/build/utils/assertion-verify/ui-lints.js +32 -0
  37. package/build/utils/frontendSelectors.d.ts +33 -0
  38. package/build/utils/frontendSelectors.js +196 -5
  39. package/build/utils/pathSignatures.d.ts +4 -1
  40. package/build/utils/pathSignatures.js +14 -2
  41. package/build/utils/planOnlyMode.d.ts +33 -0
  42. package/build/utils/planOnlyMode.js +40 -0
  43. package/build/utils/rebaselineSnapshots.d.ts +24 -0
  44. package/build/utils/rebaselineSnapshots.js +65 -0
  45. package/build/utils/removedUiElements.d.ts +34 -0
  46. package/build/utils/removedUiElements.js +153 -0
  47. package/build/utils/reportVerification.d.ts +7 -6
  48. package/build/utils/reportVerification.js +68 -4
  49. package/build/utils/scenarioDrafting.d.ts +1 -1
  50. package/build/utils/scenarioDrafting.js +1 -1
  51. package/build/utils/versions.d.ts +3 -3
  52. package/build/utils/versions.js +1 -1
  53. package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
  54. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
  55. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
  56. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
  57. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
  58. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
  59. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  60. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
  61. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
  62. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
  63. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  64. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  65. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  66. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
  67. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
  68. package/node_modules/playwright/package.json +1 -1
  69. package/package.json +3 -3
@@ -8,7 +8,8 @@ import * as path from "path";
8
8
  import { AnalyticsService } from "../../services/AnalyticsService.js";
9
9
  import { dualChannelResult, toolError } from "../../utils/utils.js";
10
10
  import { isTestFile } from "../../prompts/test-recommendation/scopeAssessment.js";
11
- import { buildRenameStrategy, buildFileRenameStrategy, buildUpdateStrategy, buildRegenerateStrategy, buildDeleteStrategy, buildUpdateFileInstruction, buildRegenerateFileInstruction, } from "../../prompts/test-maintenance/actionsInstructions.js";
11
+ import { REBASELINE_SNAPSHOT_NAME_RE, looksLikeOnDiskBaselineName } from "../../utils/rebaselineSnapshots.js";
12
+ import { buildRenameStrategy, buildFileRenameStrategy, buildUpdateStrategy, buildRebaselineStrategy, buildRegenerateStrategy, buildDeleteStrategy, buildUpdateFileInstruction, buildRegenerateFileInstruction, } from "../../prompts/test-maintenance/actionsInstructions.js";
12
13
  /**
13
14
  * Compute a suggested new filename when an endpoint is renamed.
14
15
  */
@@ -102,6 +103,15 @@ const recommendationSchema = z.object({
102
103
  .optional()
103
104
  .describe("For a large UPDATE file, the concrete per-site edits you already located while reading it — one entry per place that must change, with a verbatim anchor. " +
104
105
  "Surfaced to the apply step as a checklist so it edits each site directly instead of re-grepping/re-paging the file. Provide this whenever an UPDATE touches more than a couple of sites."),
106
+ rebaselineSnapshots: z
107
+ // Loose here on purpose: one malformed name must not reject the whole batch and
108
+ // lose every other verdict. Entries are validated per item below; invalid ones
109
+ // are dropped and surfaced in the response.
110
+ .array(z.string())
111
+ .optional()
112
+ .describe("UPDATE only — the toHaveScreenshot() baseline filenames (e.g. [\"page-001.png\"]) whose captured page/element/region the diff visibly changed, so the committed PNG is stale (SKYR-4298). " +
113
+ "List only the baselines the change reaches; leave unaffected ones out. These are refreshed at the final skyramp_execute_test run via its rebaselineSnapshots parameter, never by editing the assertion, its maxDiffPixelRatio, or the PNG. " +
114
+ "Omit when the diff does not explain a screenshot mismatch — that failure stays red."),
105
115
  renamedEndpoints: z
106
116
  .array(z.object({
107
117
  oldPath: z.string().describe("Previous endpoint path"),
@@ -182,6 +192,7 @@ export function registerActionsTool(server) {
182
192
  // and passes results here directly — analyzeTestHealthTool never writes assessment
183
193
  // data back to the state file.
184
194
  const recommendations = [];
195
+ const droppedRebaselineNotes = [];
185
196
  (args.recommendations ?? []).forEach((rec) => {
186
197
  // Schema requires absolute paths; resolve any relative paths defensively
187
198
  // against repoRoot in case the LLM sends a relative path despite the schema.
@@ -294,6 +305,26 @@ export function registerActionsTool(server) {
294
305
  resolvedPomFile = undefined;
295
306
  }
296
307
  }
308
+ if (rec.action !== DriftAction.Update && rec.rebaselineSnapshots?.length) {
309
+ droppedRebaselineNotes.push(`${rec.testFilePath}: rebaselineSnapshots [${rec.rebaselineSnapshots.join(", ")}] ignored on ${rec.action} — a baseline refresh is only meaningful on UPDATE.`);
310
+ }
311
+ // Per-entry validation (the schema is deliberately loose, see above).
312
+ const validBaselines = [];
313
+ for (const raw of rec.rebaselineSnapshots ?? []) {
314
+ const name = raw.trim();
315
+ if (!REBASELINE_SNAPSHOT_NAME_RE.test(name)) {
316
+ droppedRebaselineNotes.push(`${rec.testFilePath}: rebaselineSnapshots entry ${JSON.stringify(raw)} dropped — must be the bare .png name the test passes to toHaveScreenshot(), e.g. "page-001.png" (no path, commas, or whitespace).`);
317
+ continue;
318
+ }
319
+ if (looksLikeOnDiskBaselineName(name)) {
320
+ // Kept — a test can legitimately pass a name like this — but flagged: the
321
+ // on-disk file carries Playwright's project/platform suffix, and passing it
322
+ // by mistake refreshes nothing (the execution result will say NOT refreshed).
323
+ droppedRebaselineNotes.push(`${rec.testFilePath}: rebaselineSnapshots entry "${name}" kept but looks like an on-disk filename (Playwright's project/platform suffix). If the test passes "${name.replace(/-(?:[^-]+-)?(?:darwin|linux|win32)\.png$/i, ".png")}" to toHaveScreenshot(), pass that instead.`);
324
+ }
325
+ if (!validBaselines.includes(name))
326
+ validBaselines.push(name);
327
+ }
297
328
  recommendations.push({
298
329
  testFilePath: resolvedFile,
299
330
  pomFile: resolvedPomFile,
@@ -306,6 +337,19 @@ export function registerActionsTool(server) {
306
337
  updateInstructions: rec.updateInstructions ?? "",
307
338
  renamedEndpoints: rec.renamedEndpoints ?? [],
308
339
  ...(rec.editSites?.length ? { editSites: rec.editSites } : {}),
340
+ // Only an UPDATE refreshes a baseline. A REGENERATEd spec that reuses a
341
+ // baseline name still compares against the committed PNG, and DELETE
342
+ // removes the spec — neither is a refresh, so the list is dropped (and
343
+ // surfaced in the response below rather than silently discarded).
344
+ ...(rec.action === DriftAction.Update && validBaselines.length
345
+ ? {
346
+ rebaselineSnapshots: validBaselines,
347
+ // No updateInstructions → the refresh is the whole maintenance and
348
+ // the report gate must not expect a spec/POM edit. With
349
+ // updateInstructions the gate holds the UPDATE to both.
350
+ rebaselineOnly: !rec.updateInstructions,
351
+ }
352
+ : {}),
309
353
  });
310
354
  });
311
355
  // Derive the no-action set here instead of asking the LLM to enumerate it.
@@ -340,6 +384,11 @@ export function registerActionsTool(server) {
340
384
  rationale: r.rationale,
341
385
  testType: r.testType,
342
386
  endpoint: r.endpoint,
387
+ // Persisted so the report-time working-tree check knows this UPDATE's
388
+ // change is the refreshed PNG, not the spec (SKYR-4298).
389
+ ...(r.rebaselineSnapshots?.length
390
+ ? { rebaselineSnapshots: r.rebaselineSnapshots, rebaselineOnly: r.rebaselineOnly === true }
391
+ : {}),
343
392
  })),
344
393
  }, { repo: args.repository, repositoryPath, step: "actions" });
345
394
  }
@@ -449,6 +498,7 @@ export function registerActionsTool(server) {
449
498
  suggestedNewFile,
450
499
  updateInstructions: rec.updateInstructions,
451
500
  rationale: rec.rationale,
501
+ rebaselineSnapshots: rec.rebaselineSnapshots,
452
502
  }));
453
503
  }
454
504
  // ── REGENERATE: emit overwrite instructions ──
@@ -500,6 +550,10 @@ export function registerActionsTool(server) {
500
550
  deleteRecommendations.length === 0) {
501
551
  sections.push(`No action required. All existing tests appear healthy.`);
502
552
  }
553
+ if (droppedRebaselineNotes.length > 0) {
554
+ sections.push(`## rebaselineSnapshots notes (${droppedRebaselineNotes.length})\n\n` +
555
+ droppedRebaselineNotes.map((n) => `- ${n}`).join("\n"));
556
+ }
503
557
  sections.push(`**This tool is currently in Early Preview stage. Please verify the results.**`);
504
558
  const responseText = sections.join("\n\n");
505
559
  // ── Build LLM instructions ──
@@ -534,18 +588,32 @@ export function registerActionsTool(server) {
534
588
  // Update context: per-file guidance + current content for the downstream LLM.
535
589
  // Including file content avoids re-reads on each Edit turn, reducing token usage.
536
590
  const updateContext = updateRecommendations
537
- .filter((rec) => !!rec.updateInstructions)
591
+ .filter((rec) => !!rec.updateInstructions || !!rec.rebaselineSnapshots?.length)
538
592
  .map((rec) => {
539
593
  const editTarget = rec.pomFile ?? rec.testFilePath;
594
+ const hasEdit = !!rec.updateInstructions;
540
595
  return {
541
596
  file: editTarget,
542
- context: rec.updateInstructions,
597
+ // A rebaseline-only entry has nothing to edit: no context, and an
598
+ // explicit edit_required: false so the strategy's "make the changes
599
+ // described in context" cannot apply to it (rationale is always set,
600
+ // so it must not stand in for edit instructions here).
601
+ ...(hasEdit ? { context: rec.updateInstructions } : { edit_required: false }),
543
602
  ...(rec.editSites?.length ? { edit_sites: rec.editSites } : {}),
603
+ // The baselines to pass as skyramp_execute_test's rebaselineSnapshots for
604
+ // the SPEC (testFilePath, which is what runs) — never `file`, which is
605
+ // the page object for a POM-backed UPDATE.
606
+ ...(rec.rebaselineSnapshots?.length
607
+ ? { spec_file: rec.testFilePath, rebaseline_snapshots: rec.rebaselineSnapshots }
608
+ : {}),
544
609
  };
545
610
  });
546
611
  if (updateContext.length > 0) {
547
612
  llmInstructionsObj.update_context = updateContext;
548
613
  llmInstructionsObj.update_strategy = buildUpdateStrategy();
614
+ if (updateContext.some((u) => "rebaseline_snapshots" in u)) {
615
+ llmInstructionsObj.rebaseline_strategy = buildRebaselineStrategy();
616
+ }
549
617
  }
550
618
  // REGENERATE context: existing file content gives the generation tool the
551
619
  // endpoint URL, auth pattern, test type, and language to replicate.
@@ -6,6 +6,7 @@ import type { FrontendFileIntegration } from "../../types/FrontendIntegration.js
6
6
  import { TraceFile } from "../../types/RepositoryAnalysis.js";
7
7
  import { BranchDiffData } from "../../utils/branchDiff.js";
8
8
  import { ScannedEndpoint } from "../../utils/repoScanner.js";
9
+ import { RemovedUiElement } from "../../utils/removedUiElements.js";
9
10
  import { TraceParseResult } from "../../utils/trace-parser.js";
10
11
  /** Exported for testing: maps a parsed trace result to a TraceFile. */
11
12
  export declare function buildTraceFileEntry(tracePath: string, result: TraceParseResult): TraceFile;
@@ -28,6 +29,15 @@ export declare const analyzeChangesInputSchema: {
28
29
  export declare const NO_UI_INSTRUCTIONS = "No UI changes detected \u2014 no blueprint capture needed.";
29
30
  export declare const NO_RESOLVABLE_URLS_INSTRUCTIONS = "Frontend changes detected but no candidate URLs could be resolved (no route files matched the changed files or their importers, and no frontend baseUrl to fall back to). UI recommendations will be source-grounded only.";
30
31
  export declare function buildCaptureInstructions(pages: CandidateUiPage[]): string;
32
+ /**
33
+ * Instruction block for data-testids the diff removed from pages that survive
34
+ * (SKYR-4305). The UI analog of the `[removed]` endpoint 404 guard: one UI
35
+ * candidate per element, asserting the element is gone plus a positive check on a
36
+ * retained element of the same page. Whole-feature removals never reach here —
37
+ * collectRemovedUiElements drops elements whose file was deleted or whose page
38
+ * could not be resolved.
39
+ */
40
+ export declare function buildRemovedElementsInstructions(elements: RemovedUiElement[]): string;
31
41
  /**
32
42
  * Instruction block for changed frontend files the server determined have no
33
43
  * production importer (SKYR-3855) — dead/orphaned components. Used both to
@@ -33,6 +33,7 @@ import { detectProjectMetadata } from "../../utils/projectMetadata.js";
33
33
  import { draftScenariosFromEndpoints, draftResponseShapeScenarios, capScenarios } from "../../utils/scenarioDrafting.js";
34
34
  import { mapUnmatchedFilesToImporterEndpoints, buildHopResponseShapeInputs, deriveResourceTokensForFile, deriveResourceToken, extractChangedSymbols, sliceDiffByFile } from "../../utils/importerHop.js";
35
35
  import { extractChangedSelectors, MAX_CHANGED_SELECTORS } from "../../utils/frontendSelectors.js";
36
+ import { collectRemovedUiElements, MAX_REMOVED_UI_ELEMENTS } from "../../utils/removedUiElements.js";
36
37
  import { buildAnalysisOutputText } from "../../prompts/test-recommendation/analysisOutputPrompt.js";
37
38
  import { parseTraceFile, discoverTraceFiles, discoverPlaywrightZips, } from "../../utils/trace-parser.js";
38
39
  import { TestSource } from "../../types/TestAnalysis.js";
@@ -359,6 +360,39 @@ If a candidate URL 404s or redirects unexpectedly, navigate from the frontend ba
359
360
 
360
361
  If \`browser_blueprint\` fails on every candidate URL (app unreachable, all 404s), proceed and log an \`issuesFound\` info entry. Recommendations will be source-grounded; non-UI work is unaffected.`;
361
362
  }
363
+ /**
364
+ * Instruction block for data-testids the diff removed from pages that survive
365
+ * (SKYR-4305). The UI analog of the `[removed]` endpoint 404 guard: one UI
366
+ * candidate per element, asserting the element is gone plus a positive check on a
367
+ * retained element of the same page. Whole-feature removals never reach here —
368
+ * collectRemovedUiElements drops elements whose file was deleted or whose page
369
+ * could not be resolved.
370
+ */
371
+ export function buildRemovedElementsInstructions(elements) {
372
+ const byPage = new Map();
373
+ for (const e of elements) {
374
+ for (const url of e.pageUrls) {
375
+ if (!byPage.has(url))
376
+ byPage.set(url, []);
377
+ byPage.get(url).push(e);
378
+ }
379
+ }
380
+ const target = (e) => e.attribute === "data-testid" ? `testId: "${e.value}"` : `attribute: "${e.attribute}", value: "${e.value}"`;
381
+ const pageLines = [...byPage.entries()]
382
+ .map(([url, els], i) => ` ${i + 1}. ${url}\n` + els.map((e) => ` - \`${e.attribute}="${e.value}"\` (removed from ${e.file}; browser_assert_hidden target: ${target(e)})`).join("\n"))
383
+ .join("\n");
384
+ const capNote = elements.length >= MAX_REMOVED_UI_ELEMENTS
385
+ ? `\n(The list is capped at ${MAX_REMOVED_UI_ELEMENTS} elements; the analyze log records how many were dropped.)`
386
+ : "";
387
+ return `**Removed UI elements — grounding for the removal test.** This diff deletes the elements below (identified by a \`data-*\` attribute) from pages that still exist after the change. The budget rule already requires a test asserting a removed element is absent; this list tells you WHICH elements and on WHICH page, which no blueprint can (the elements are gone). Renames, moved/extracted elements, elements in deleted files, files with no production importer, and values an existing test already references were filtered out server-side; the page is attributed through the import graph (the file, or one hop via the files that import it), not observed.
388
+
389
+ **Removed elements by page:**
390
+ ${pageLines}${capNote}
391
+
392
+ **How to record (Task 2), per page — not per element:** if the plan already has a UI candidate on that page, record the guard inside that trace; otherwise register ONE UI candidate \`verify-removed-ui-<page-slug>\` (\`testType: "ui"\`, category \`breaking_change\`; \`skyramp_register_test_plan\` requires at least one step, so use a placeholder \`GET <page path>\` → 200). \`browser_navigate\` to the page and let the app render, then one \`browser_assert_hidden\` call per element using the target shown above — \`testId\` when the attribute is \`data-testid\`, otherwise \`attribute\` + \`value\` (the generated test then uses \`getByTestId(...)\` or \`page.locator('[data-cy="..."]')\` respectively; no \`ref\` — a removed element has no snapshot ref; the tool checks the live page and refuses to record on a blank or un-rendered page), then at least one \`browser_assert\` on a RETAINED element of the same page verifying its text, value or state — the generated \`toBeHidden()\` assertions must never stand alone. Then \`skyramp_export_zip\` and \`skyramp_ui_test_generation\` as usual.
393
+
394
+ **Blueprint Citation for these recommendations:** the removed elements will not appear in any blueprint you capture — that is expected. For a \`data-testid\` element set \`targetElements[].testId\` to the value; for any other attribute leave \`testId\` null and name the \`attribute="value"\` pair in \`reasoning\`. Take \`role\`/\`accessibleName\` from the deleted JSX in the diff, set \`pageContext.url\` to the page above, and prefix the recommendation's \`reasoning\` with \`[removed-element]\`.`;
395
+ }
362
396
  /**
363
397
  * Instruction block for changed frontend files the server determined have no
364
398
  * production importer (SKYR-3855) — dead/orphaned components. Used both to
@@ -775,6 +809,7 @@ export function registerAnalyzeChangesTool(server) {
775
809
  }
776
810
  let existingTests = [];
777
811
  let discoveredRelevantExternalPaths = [];
812
+ let testDiscoveryFailed = false;
778
813
  try {
779
814
  const testDiscoveryService = new TestDiscoveryService();
780
815
  // Run-scoped: only the primary call carries testsRepoDir, so never
@@ -799,6 +834,7 @@ export function registerAnalyzeChangesTool(server) {
799
834
  });
800
835
  }
801
836
  catch (err) {
837
+ testDiscoveryFailed = true;
802
838
  logger.warning("Test discovery failed, continuing with empty list", {
803
839
  error: err instanceof Error ? err.message : String(err),
804
840
  });
@@ -1379,10 +1415,44 @@ export function registerAnalyzeChangesTool(server) {
1379
1415
  // the pages that import them.
1380
1416
  const frontendFileIntegration = checkFrontendFileIntegration(params.repositoryPath, changedFrontendFiles);
1381
1417
  const candidateUiPages = await enumerateCandidateUiPages(params.repositoryPath, changedFrontendFiles, frontendFileIntegration);
1418
+ // SKYR-4305: data-testids the diff removed from a page that survives —
1419
+ // grounding for the removal test the budget rule requires (a removed
1420
+ // element appears in no blueprint). Purely additive: a failure here must
1421
+ // never cost the analyze call.
1422
+ let removedElements = [];
1423
+ if (diffData?.diffContent) {
1424
+ try {
1425
+ removedElements = collectRemovedUiElements({
1426
+ changedFrontendFiles,
1427
+ deletedFiles: diffData.deletedFiles ?? [],
1428
+ diffByFile: getDiffByFile(),
1429
+ frontendFileIntegration,
1430
+ candidateUiPages,
1431
+ existingTestFiles: [
1432
+ ...existingTests.map((t) => t.testFile),
1433
+ ...discoveredRelevantExternalPaths,
1434
+ ],
1435
+ discoveryFailed: testDiscoveryFailed,
1436
+ });
1437
+ }
1438
+ catch (err) {
1439
+ logger.warning("removedUiElements: collection failed, continuing without removal-guard grounding", {
1440
+ error: err instanceof Error ? err.message : String(err),
1441
+ });
1442
+ removedElements = [];
1443
+ }
1444
+ }
1445
+ if (removedElements.length > 0) {
1446
+ logger.info("Removed UI elements detected (removal-guard grounding)", {
1447
+ count: removedElements.length,
1448
+ removed: removedElements.map((e) => `${e.attribute}="${e.value}"`),
1449
+ });
1450
+ }
1382
1451
  return {
1383
1452
  changedFrontendFiles,
1384
1453
  candidateUiPages,
1385
1454
  frontendFileIntegration,
1455
+ ...(removedElements.length > 0 ? { removedElements } : {}),
1386
1456
  };
1387
1457
  })();
1388
1458
  // Derive the UI blueprint-capture instructions the agent acts on after
@@ -1407,6 +1477,9 @@ export function registerAnalyzeChangesTool(server) {
1407
1477
  : buildCaptureInstructions(uiContext.candidateUiPages)) +
1408
1478
  (unintegratedFrontendFiles.length > 0
1409
1479
  ? `\n\n${buildUnintegratedInstructions(unintegratedFrontendFiles)}`
1480
+ : "") +
1481
+ (uiContext.removedElements?.length
1482
+ ? `\n\n${buildRemovedElementsInstructions(uiContext.removedElements)}`
1410
1483
  : "");
1411
1484
  // ── SKYR-3879 Path B: budget context + pre-seeded approved plan ──
1412
1485
  // Computed BEFORE the state write (not after, alongside the
@@ -6,7 +6,7 @@ import { ChangedRoute } from "../../utils/changedRoutes.js";
6
6
  declare const registerCandidateSchema: z.ZodObject<{
7
7
  scenarioName: z.ZodString;
8
8
  description: z.ZodString;
9
- category: z.ZodEnum<["new_endpoint", "bug_caught", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
9
+ category: z.ZodEnum<["new_endpoint", "bug_caught", "requirement_conflict", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
10
10
  priority: z.ZodEnum<["high", "medium", "low"]>;
11
11
  testType: z.ZodEffects<z.ZodNativeEnum<typeof TestType>, TestType, TestType>;
12
12
  steps: z.ZodArray<z.ZodObject<{
@@ -124,7 +124,7 @@ declare const registerCandidateSchema: z.ZodObject<{
124
124
  priority: "high" | "medium" | "low";
125
125
  testType: TestType;
126
126
  scenarioName: string;
127
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
127
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
128
128
  steps: {
129
129
  path: string;
130
130
  method: HttpMethod;
@@ -160,7 +160,7 @@ declare const registerCandidateSchema: z.ZodObject<{
160
160
  priority: "high" | "medium" | "low";
161
161
  testType: TestType;
162
162
  scenarioName: string;
163
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
163
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
164
164
  steps: {
165
165
  path: string;
166
166
  method: HttpMethod;
@@ -12,7 +12,7 @@ import { CandidateSource, computeCandidateId, scenarioMergeKey, DiscriminatorKin
12
12
  import { selectPlan } from "../../recommendation/planRanker.js";
13
13
  import { resolveSubjectEndpoints } from "../../utils/subjectEndpoints.js";
14
14
  import { collectChangedRouteLines } from "../../utils/changedRoutes.js";
15
- import { reservedUISlots } from "../../recommendation/budgeters/shared.js";
15
+ import { generateFloors } from "../../recommendation/budgeters/shared.js";
16
16
  import { inferScenarioType } from "../../recommendation/diversity.js";
17
17
  import { validateDiscriminator } from "../../recommendation/discriminators.js";
18
18
  import { isAttackSurfaceSecurityBoundary } from "../../prompts/test-recommendation/recommendationShared.js";
@@ -76,7 +76,7 @@ const discriminatorClaimSchema = z.object({
76
76
  const registerCandidateSchema = z.object({
77
77
  scenarioName: z.string().min(1).describe("Name for this test/scenario. Reused as the match key against the generation tool's scenarioName — keep it identical when you later call the generation tool."),
78
78
  description: z.string().min(1),
79
- category: z.enum(SCENARIO_CATEGORIES).describe("Test category — drives ranking (never your own priority tag)."),
79
+ category: z.enum(SCENARIO_CATEGORIES).describe("Test category — drives ranking (never your own priority tag). `bug_caught` targets a `<bug_found>` flaw from code review. `requirement_conflict` asserts a requirement the PR title/description (or a requirements file it references) states and the implemented behavior contradicts — it fails on the current code by design. Both rank in the top tier but are carved out independently, so a requirement conflict never loses its slot to a code-review bug."),
80
80
  priority: z.enum(["high", "medium", "low"]).describe("Your own assessment — recorded, but ranking is derived from category, not this field."),
81
81
  testType: z.nativeEnum(TestType).refine((t) => PLAN_TEST_TYPES.has(t), {
82
82
  message: `testType must be one of ${[...PLAN_TEST_TYPES].join(", ")}`,
@@ -340,7 +340,26 @@ function renderGenerationDirective(plan) {
340
340
  (nonUICount > 0 ? ` Generate the ${tests(nonUICount)} of other types as well.` : ""),
341
341
  ];
342
342
  }
343
- function renderPlanText(plan, dropped) {
343
+ /** SKYR-4292: the server cannot count the diff's changed behaviors, so an
344
+ * under-budget plan is flagged (not rejected) with what a missing candidate looks
345
+ * like. Wipro PR #24 registered 3 candidates against a 20-slot UI-only budget and
346
+ * the tool answered "AUTHORITATIVE and FINAL" with no pushback. */
347
+ function renderBudgetCheck(plan, maxTotal) {
348
+ const filled = plan.generate.length + plan.additional.length;
349
+ if (filled >= maxTotal)
350
+ return [];
351
+ return [
352
+ "",
353
+ `### Budget check: ${filled} of ${maxTotal} slots filled`,
354
+ "Every observable behavior this diff changes — each option value added, removed, renamed or reordered; each label, " +
355
+ "element, route, state, validation rule, computed value or style value (spacing, size, color, radius, font weight, " +
356
+ "class name) that renders differently; each removed element, asserted absent — needs a candidate above, however " +
357
+ "small the change. A change is covered only when an existing test asserts the changed value, not when a test merely " +
358
+ "reaches the component. If any changed behavior has no candidate, call skyramp_register_test_plan again with your " +
359
+ "full updated list. If the diff truly changes fewer behaviors than the budget, this plan stands as-is.",
360
+ ];
361
+ }
362
+ function renderPlanText(plan, dropped, maxTotal) {
344
363
  const lines = [];
345
364
  lines.push(`## Approved Test Plan (${plan.planId})`);
346
365
  lines.push("");
@@ -402,6 +421,7 @@ function renderPlanText(plan, dropped) {
402
421
  }
403
422
  lines.push("");
404
423
  lines.push(...renderGenerationDirective(plan));
424
+ lines.push(...renderBudgetCheck(plan, maxTotal));
405
425
  return lines.join("\n");
406
426
  }
407
427
  // ── Tool registration ───────────────────────────────────────────────────────
@@ -544,10 +564,11 @@ export function registerRegisterTestPlanTool(server) {
544
564
  // changes would silently plan zero UI tests (runs 32419025266,
545
565
  // 32431042133, 32431106864) — reject so the agent self-corrects in one
546
566
  // step, exactly like the empty-candidates rejection above.
547
- if (reservedUISlots(budgetContext) > 0 &&
548
- !allCandidates.some((c) => inferScenarioType(c.scenario) === TestType.UI)) {
549
- errorResult = toolError("This run has frontend changes, so the plan reserves a UI slot — but no submitted or recovered " +
550
- "candidate has testType 'ui'. Resubmit your full candidate list including at least one UI candidate " +
567
+ const uiFloor = generateFloors(budgetContext).ui;
568
+ if (uiFloor > 0 && !allCandidates.some((c) => inferScenarioType(c.scenario) === TestType.UI)) {
569
+ errorResult = toolError(`This run has frontend changes, so the plan MUST include UI tests — at least ${uiFloor} of the ` +
570
+ "GENERATE slots are held for them, and more are open to UI candidates on rank. No submitted or " +
571
+ "recovered candidate has testType 'ui'. Resubmit your full candidate list including UI candidates " +
551
572
  "for the changed frontend (see uiContext.changedFrontendFiles).");
552
573
  return errorResult;
553
574
  }
@@ -576,7 +597,7 @@ export function registerRegisterTestPlanTool(server) {
576
597
  return errorResult;
577
598
  }
578
599
  return {
579
- content: [{ type: "text", text: renderPlanText(approvedPlan, result.dropped) }],
600
+ content: [{ type: "text", text: renderPlanText(approvedPlan, result.dropped, budgetContext.maxTotal) }],
580
601
  };
581
602
  }
582
603
  catch (error) {
@@ -1119,7 +1119,7 @@ export declare const scenarioStepSchema: z.ZodObject<{
1119
1119
  export declare const draftedScenarioSchema: z.ZodObject<{
1120
1120
  scenarioName: z.ZodString;
1121
1121
  description: z.ZodString;
1122
- category: z.ZodEnum<["new_endpoint", "bug_caught", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
1122
+ category: z.ZodEnum<["new_endpoint", "bug_caught", "requirement_conflict", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
1123
1123
  priority: z.ZodEnum<["high", "medium", "low"]>;
1124
1124
  steps: z.ZodArray<z.ZodObject<{
1125
1125
  order: z.ZodNumber;
@@ -1231,7 +1231,7 @@ export declare const draftedScenarioSchema: z.ZodObject<{
1231
1231
  description: string;
1232
1232
  priority: "high" | "medium" | "low";
1233
1233
  scenarioName: string;
1234
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1234
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1235
1235
  steps: {
1236
1236
  path: string;
1237
1237
  method: string;
@@ -1268,7 +1268,7 @@ export declare const draftedScenarioSchema: z.ZodObject<{
1268
1268
  description: string;
1269
1269
  priority: "high" | "medium" | "low";
1270
1270
  scenarioName: string;
1271
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1271
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1272
1272
  steps: {
1273
1273
  path: string;
1274
1274
  method: string;
@@ -1607,7 +1607,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
1607
1607
  draftedScenarios: z.ZodArray<z.ZodObject<{
1608
1608
  scenarioName: z.ZodString;
1609
1609
  description: z.ZodString;
1610
- category: z.ZodEnum<["new_endpoint", "bug_caught", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
1610
+ category: z.ZodEnum<["new_endpoint", "bug_caught", "requirement_conflict", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>;
1611
1611
  priority: z.ZodEnum<["high", "medium", "low"]>;
1612
1612
  steps: z.ZodArray<z.ZodObject<{
1613
1613
  order: z.ZodNumber;
@@ -1719,7 +1719,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
1719
1719
  description: string;
1720
1720
  priority: "high" | "medium" | "low";
1721
1721
  scenarioName: string;
1722
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1722
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1723
1723
  steps: {
1724
1724
  path: string;
1725
1725
  method: string;
@@ -1756,7 +1756,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
1756
1756
  description: string;
1757
1757
  priority: "high" | "medium" | "low";
1758
1758
  scenarioName: string;
1759
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1759
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1760
1760
  steps: {
1761
1761
  path: string;
1762
1762
  method: string;
@@ -1799,7 +1799,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
1799
1799
  description: string;
1800
1800
  priority: "high" | "medium" | "low";
1801
1801
  scenarioName: string;
1802
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1802
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1803
1803
  steps: {
1804
1804
  path: string;
1805
1805
  method: string;
@@ -1842,7 +1842,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
1842
1842
  description: string;
1843
1843
  priority: "high" | "medium" | "low";
1844
1844
  scenarioName: string;
1845
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1845
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1846
1846
  steps: {
1847
1847
  path: string;
1848
1848
  method: string;
@@ -2743,7 +2743,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
2743
2743
  description: string;
2744
2744
  priority: "high" | "medium" | "low";
2745
2745
  scenarioName: string;
2746
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
2746
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
2747
2747
  steps: {
2748
2748
  path: string;
2749
2749
  method: string;
@@ -2956,7 +2956,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
2956
2956
  description: string;
2957
2957
  priority: "high" | "medium" | "low";
2958
2958
  scenarioName: string;
2959
- category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
2959
+ category: "new_endpoint" | "bug_caught" | "requirement_conflict" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
2960
2960
  steps: {
2961
2961
  path: string;
2962
2962
  method: string;
@@ -41,6 +41,18 @@ export interface MaintenanceActionCore {
41
41
  * downstream stage that reasons about the edit needs both — notably the report-time
42
42
  * working-tree check, which otherwise calls a POM-backed UPDATE unbacked (SKYR-4129). */
43
43
  pomFile?: string;
44
+ /** Visual-snapshot baselines (toHaveScreenshot filenames, e.g. "page-001.png") an UPDATE
45
+ * refreshes because the diff changed how the captured page/element looks (SKYR-4298).
46
+ * The edit lands in the PNG under `<spec>-snapshots/`, not in the spec, so the
47
+ * report-time working-tree check must accept that PNG as the UPDATE's backing, and
48
+ * the final `skyramp_execute_test` must receive the list as `rebaselineSnapshots`. */
49
+ rebaselineSnapshots?: string[];
50
+ /** True when the UPDATE carries rebaselineSnapshots and no updateInstructions: the
51
+ * refresh is its whole maintenance, so no edit to the spec/POM is expected. When false
52
+ * (or absent) with baselines listed, the report-time check requires BOTH the edit and
53
+ * the rewritten PNG — a listed baseline must never exempt the selector edit the same
54
+ * verdict claimed (SKYR-3883 stays in force). */
55
+ rebaselineOnly?: boolean;
44
56
  }
45
57
  /** Normalized internal recommendation built from LLM-supplied args.recommendations. */
46
58
  export interface DriftRecommendation extends MaintenanceActionCore {
@@ -60,6 +60,10 @@ export interface TestExecutionOptions {
60
60
  playwrightSaveStoragePath?: string;
61
61
  dockerNetwork?: string;
62
62
  useHostNetwork?: boolean;
63
+ /** Visual-snapshot baselines (toHaveScreenshot filenames, e.g. "page-001.png") this run
64
+ * replaces instead of comparing against — forwarded to SmartPlaywright as
65
+ * SKYRAMP_UPDATE_SNAPSHOTS (SKYR-4298). Only for an intended UI change the diff explains. */
66
+ rebaselineSnapshots?: string[];
63
67
  }
64
68
  /**
65
69
  * Progress callback for reporting execution status
@@ -12,7 +12,7 @@ export declare enum Novelty {
12
12
  EXISTING = "existing"
13
13
  }
14
14
  /** All categories including internal ones. */
15
- export declare const SCENARIO_CATEGORIES: readonly ["new_endpoint", "bug_caught", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"];
15
+ export declare const SCENARIO_CATEGORIES: readonly ["new_endpoint", "bug_caught", "requirement_conflict", "business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"];
16
16
  export type ScenarioCategory = typeof SCENARIO_CATEGORIES[number];
17
17
  /** Categories valid for tool submissions (excludes internal-only categories). */
18
18
  export declare const TEST_CATEGORIES: readonly ["business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"];
@@ -21,3 +21,13 @@ export type TestCategory = typeof TEST_CATEGORIES[number];
21
21
  export declare const CATEGORY_PRIORITY: Record<ScenarioCategory, PriorityTier>;
22
22
  /** Map internal-only categories to their external equivalent for tool submission. */
23
23
  export declare function externalCategory(cat: ScenarioCategory): TestCategory;
24
+ /**
25
+ * Categories whose scenarios target a specific identified defect — a code flaw
26
+ * (`bug_caught`) or a stated requirement the code contradicts
27
+ * (`requirement_conflict`). They survive external-test dedup: an existing test on
28
+ * the same endpoint exercises the surface, not the flaw, so removing them would
29
+ * drop the only test that fails on the defect.
30
+ */
31
+ export declare const FLAW_TARGETING_CATEGORIES: readonly ["bug_caught", "requirement_conflict"];
32
+ /** Whether `category` targets a specific identified defect (see {@link FLAW_TARGETING_CATEGORIES}). */
33
+ export declare function isFlawTargetingCategory(category: ScenarioCategory | undefined): boolean;
@@ -17,6 +17,14 @@ export var Novelty;
17
17
  const INTERNAL_CATEGORIES = [
18
18
  "new_endpoint", // MEDIUM - diff-direct scenario; where a test came from, not a guarantee of a slot
19
19
  "bug_caught", // CRITICAL - tests targeting a specific <bug_found> flaw identified during enrichment
20
+ // CRITICAL - tests asserting a requirement the PR title/description (or a
21
+ // requirements file it references) states, which the implemented behavior
22
+ // contradicts. Separate from bug_caught deliberately (SKYR-4291): labelled
23
+ // bug_caught, a requirement-vs-code mismatch competed with the code-review
24
+ // flaws for the same promotion and lost it on severity. Its own category means
25
+ // its own carve-out here, first place in the promotion order SKYR-4275's bound
26
+ // hands out, and its own coverage gate.
27
+ "requirement_conflict",
20
28
  ];
21
29
  /** External categories valid for tool submissions, ordered by priority. */
22
30
  const CATEGORIES = [
@@ -59,6 +67,12 @@ export const CATEGORY_PRIORITY = {
59
67
  // fill GENERATE.
60
68
  new_endpoint: PriorityTier.MEDIUM,
61
69
  bug_caught: PriorityTier.CRITICAL, // tests targeting a <bug_found> flaw — always in GENERATE
70
+ // A stated requirement the implementation contradicts is the point of the PR,
71
+ // so it sits in the same top tier as bug_caught. Being its own category, it is
72
+ // carved out separately in planRanker and takes the FIRST slot of the promotion
73
+ // bound SKYR-4275 sets — the code-review flaws promote into what is left, so a
74
+ // requirement conflict is never the finding that loses on severity.
75
+ requirement_conflict: PriorityTier.CRITICAL,
62
76
  business_rule: PriorityTier.HIGH, // formula/business-logic bugs are high priority
63
77
  security_boundary: PriorityTier.HIGH,
64
78
  data_integrity: PriorityTier.HIGH,
@@ -75,5 +89,25 @@ export function externalCategory(cat) {
75
89
  return "crud";
76
90
  if (cat === "bug_caught")
77
91
  return "business_rule";
92
+ // The stated requirement IS the business rule the test asserts — same landing
93
+ // spot as bug_caught, so a requirement-conflict test reads as a rule check in
94
+ // the customer-facing report rather than an unrecognised label.
95
+ if (cat === "requirement_conflict")
96
+ return "business_rule";
78
97
  return cat;
79
98
  }
99
+ /**
100
+ * Categories whose scenarios target a specific identified defect — a code flaw
101
+ * (`bug_caught`) or a stated requirement the code contradicts
102
+ * (`requirement_conflict`). They survive external-test dedup: an existing test on
103
+ * the same endpoint exercises the surface, not the flaw, so removing them would
104
+ * drop the only test that fails on the defect.
105
+ */
106
+ export const FLAW_TARGETING_CATEGORIES = [
107
+ "bug_caught",
108
+ "requirement_conflict",
109
+ ];
110
+ /** Whether `category` targets a specific identified defect (see {@link FLAW_TARGETING_CATEGORIES}). */
111
+ export function isFlawTargetingCategory(category) {
112
+ return category !== undefined && FLAW_TARGETING_CATEGORIES.includes(category);
113
+ }
@@ -5,6 +5,7 @@ import { RepositoryAnalysis, AnalysisScope } from "../types/RepositoryAnalysis.j
5
5
  import { PRTestContext } from "./pr-comment-parser.js";
6
6
  import { ParsedDiffEndpoint } from "./routeParsers.js";
7
7
  import type { CandidateUiPage } from "./uiPageEnumerator.js";
8
+ import type { RemovedUiElement } from "./removedUiElements.js";
8
9
  import type { FrontendFileIntegration } from "../types/FrontendIntegration.js";
9
10
  import type { ApprovedPlanItem } from "../types/Recommendation.js";
10
11
  import type { ExternalTestRunRecord } from "../types/ExternalTestExecution.js";
@@ -135,6 +136,12 @@ export interface UiAnalysisContext {
135
136
  * the agent running its own grep mid-task. Same order as changedFrontendFiles.
136
137
  */
137
138
  frontendFileIntegration?: FrontendFileIntegration[];
139
+ /**
140
+ * data-testids the diff removed from files whose page survives (SKYR-4305).
141
+ * Present only when non-empty. The testbot records a removal guard
142
+ * (`toBeHidden()` on the id + a positive sibling assertion) for each entry.
143
+ */
144
+ removedElements?: RemovedUiElement[];
138
145
  }
139
146
  /**
140
147
  * SKYR-3879 Path B — the server-side selection checkpoint's persisted result.