@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
@@ -188,6 +188,14 @@ export declare const COVERAGE_CONTRACT: {
188
188
  readonly message: "No {kind} test cites this change, which is on the {surface}.";
189
189
  readonly suggestion: "Add a test on that surface that cites this change, or answer in one line why that surface needs no test.";
190
190
  };
191
+ readonly surfaceCase: {
192
+ readonly message: "This change is on the page and the API, and no ui or e2e test that cites it chooses `{case}` on the page.";
193
+ readonly suggestion: "Add a ui or e2e test, or a step in one, that chooses that value on the page and names it in the step, as in `Principal radio`, or answer in one line why the page cannot send it.";
194
+ };
195
+ readonly surfaceReject: {
196
+ readonly message: "This change is on the page and the API, and no ui or e2e test that cites it tries `{case}` on the page or checks that the page no longer offers it.";
197
+ readonly suggestion: "Add a ui or e2e test, or a step in one, that names that value in the step, as in `Intern radio absent`: try it and assert the page refuses it, or assert the page no longer offers it. If an existing UI spec already does this and this run edits it, declare it in `maintains` with its real path and name the value in `differsBy`. If this run cannot write the test, answer it and set `blocker` to what stopped the run; an answer with no blocker leaves this open.";
198
+ };
191
199
  };
192
200
  readonly suggestion: "Which planned test tests each behaviour this change promises and each file it touches, and for one nothing covers, why does it need none?";
193
201
  };
@@ -255,8 +263,16 @@ export declare const REQUIREMENT_SOURCED_CONTRACT: {
255
263
  readonly suggestion: "Cite a change you read in the pull request or in a requirements file, or report this test as something other than a requirement conflict.";
256
264
  };
257
265
  readonly notNamed: {
258
- readonly message: "Change `{change}` cites `{source}`, and the pull request names no such file.";
259
- readonly suggestion: "Cite a file the title or description names. If neither names it, you read the change in the repository, so set `source` to `diff`.";
266
+ readonly message: "Change `{change}` cites `{source}`, and the pull request neither names nor changes such a file.";
267
+ readonly suggestion: "Cite a file the title or description names, or one the diff changes. If none does, you read the change in the repository, so set `source` to `diff`.";
268
+ };
269
+ readonly itemUnlinked: {
270
+ readonly message: "`{source}` lists item {number}, \"{item}\", and no change in this plan names it.";
271
+ readonly suggestion: "Set `source` to `{source}#{number}` on the change that covers this item, or answer with the reason it needs no test.";
272
+ };
273
+ readonly unnumbered: {
274
+ readonly message: "`{source}` lists {count} items, and no change names one of them by its number.";
275
+ readonly suggestion: "Name the item each change covers in its source, as `{source}#<number>` (`#7,8` for two), so each item can be checked.";
260
276
  };
261
277
  readonly emptyPullRequest: {
262
278
  readonly message: "Change `{change}` says it was read from the pull request, and the pull request has no title or description.";
@@ -307,6 +323,20 @@ export declare const DEFECTS_CONTRACT: {
307
323
  };
308
324
  readonly suggestion: "Which planned test proves each defect the review found, and for one nothing proves, why does it need no test?";
309
325
  };
326
+ export declare const CHANGED_LINES_CLAIMED_CONTRACT: {
327
+ readonly id: "changedLinesClaimed";
328
+ readonly objections: {
329
+ readonly unclaimed: {
330
+ readonly message: "These changed lines are in a file a planned test covers, and no change in the plan names them in its `lines`.";
331
+ readonly suggestion: "Add these lines to the `lines` of the change they belong to, or declare a change for them, with a case that states what a test must see. If the lines change nothing a test can observe, say so as the answer.";
332
+ };
333
+ readonly noCase: {
334
+ readonly message: "These changed lines belong to a change that states no case, so no test is held to what they do.";
335
+ readonly suggestion: "Give the change a case whose `expectedValue` or `derived` states what a test must see after these lines. If the lines change nothing a test can observe, say so as the answer.";
336
+ };
337
+ };
338
+ readonly suggestion: "Which change owns each run of changed lines in a tested file, and what must a test see because of it?";
339
+ };
310
340
  /** Fills a contract sentence's `{placeholder}` spans from the run's or the
311
341
  * planned test's own values. A placeholder with no value is left as written rather
312
342
  * than blanked, so a missed key shows up as `{knob}` instead of reading as a gap
@@ -202,6 +202,19 @@ export const COVERAGE_CONTRACT = {
202
202
  message: "No {kind} test cites this change, which is on the {surface}.",
203
203
  suggestion: "Add a test on that surface that cites this change, or answer in one line why that surface needs no test.",
204
204
  },
205
+ // `{case}` is the rendered case, as in `level="Principal" (accept)`. SKYR-4529: a form offered Lead and Principal, and
206
+ // only an API test sent Principal.
207
+ surfaceCase: {
208
+ message: "This change is on the page and the API, and no ui or e2e test that cites it chooses `{case}` on the page.",
209
+ suggestion: "Add a ui or e2e test, or a step in one, that chooses that value on the page and names it in the step, as in `Principal radio`, or answer in one line why the page cannot send it.",
210
+ },
211
+ // `{case}` is the rendered case, as in `level="Principal" (accept)`. The XR02 run removed Intern from a form; the page
212
+ // objection closed by a sentence, and no generated test checked the option was
213
+ // gone.
214
+ surfaceReject: {
215
+ message: "This change is on the page and the API, and no ui or e2e test that cites it tries `{case}` on the page or checks that the page no longer offers it.",
216
+ suggestion: "Add a ui or e2e test, or a step in one, that names that value in the step, as in `Intern radio absent`: try it and assert the page refuses it, or assert the page no longer offers it. If an existing UI spec already does this and this run edits it, declare it in `maintains` with its real path and name the value in `differsBy`. If this run cannot write the test, answer it and set `blocker` to what stopped the run; an answer with no blocker leaves this open.",
217
+ },
205
218
  },
206
219
  suggestion: "Which planned test tests each behaviour this change promises and each file it touches, and for one nothing covers, why does it need none?",
207
220
  };
@@ -305,11 +318,25 @@ export const REQUIREMENT_SOURCED_CONTRACT = {
305
318
  suggestion: "Cite a change you read in the pull request or in a requirements file, or report this test as something other than a requirement conflict.",
306
319
  },
307
320
  // The server rendered the prompt, so it holds the title and description the run
308
- // was given. A requirements file the pull request never names is a file the agent
309
- // found by looking, which is reading the repository, not reading a requirement.
321
+ // was given, and it holds the diff. A requirements file the pull request neither
322
+ // names nor changes is a file the agent found by looking, which is reading the
323
+ // repository, not reading a requirement.
310
324
  notNamed: {
311
- message: "Change `{change}` cites `{source}`, and the pull request names no such file.",
312
- suggestion: "Cite a file the title or description names. If neither names it, you read the change in the repository, so set `source` to `diff`.",
325
+ message: "Change `{change}` cites `{source}`, and the pull request neither names nor changes such a file.",
326
+ suggestion: "Cite a file the title or description names, or one the diff changes. If none does, you read the change in the repository, so set `source` to `diff`.",
327
+ },
328
+ // `{item}` is one top-level list line of the cited file or the description,
329
+ // `{number}` its number, `{source}` that source as the first change citing it
330
+ // spelled it, without its `#` anchor. The check
331
+ // reads only whether some change names the number, not whether a test covers it.
332
+ itemUnlinked: {
333
+ message: '`{source}` lists item {number}, "{item}", and no change in this plan names it.',
334
+ suggestion: "Set `source` to `{source}#{number}` on the change that covers this item, or answer with the reason it needs no test.",
335
+ },
336
+ // `{count}` is the number of top-level list items in `{source}`.
337
+ unnumbered: {
338
+ message: "`{source}` lists {count} items, and no change names one of them by its number.",
339
+ suggestion: "Name the item each change covers in its source, as `{source}#<number>` (`#7,8` for two), so each item can be checked.",
313
340
  },
314
341
  // Says only what was read: this pull request arrived with no words in it.
315
342
  emptyPullRequest: {
@@ -368,6 +395,20 @@ export const DEFECTS_CONTRACT = {
368
395
  },
369
396
  suggestion: "Which planned test proves each defect the review found, and for one nothing proves, why does it need no test?",
370
397
  };
398
+ export const CHANGED_LINES_CLAIMED_CONTRACT = {
399
+ id: "changedLinesClaimed",
400
+ objections: {
401
+ unclaimed: {
402
+ message: "These changed lines are in a file a planned test covers, and no change in the plan names them in its `lines`.",
403
+ suggestion: "Add these lines to the `lines` of the change they belong to, or declare a change for them, with a case that states what a test must see. If the lines change nothing a test can observe, say so as the answer.",
404
+ },
405
+ noCase: {
406
+ message: "These changed lines belong to a change that states no case, so no test is held to what they do.",
407
+ suggestion: "Give the change a case whose `expectedValue` or `derived` states what a test must see after these lines. If the lines change nothing a test can observe, say so as the answer.",
408
+ },
409
+ },
410
+ suggestion: "Which change owns each run of changed lines in a tested file, and what must a test see because of it?",
411
+ };
371
412
  /** Fills a contract sentence's `{placeholder}` spans from the run's or the
372
413
  * planned test's own values. A placeholder with no value is left as written rather
373
414
  * than blanked, so a missed key shows up as `{knob}` instead of reading as a gap
@@ -0,0 +1,17 @@
1
+ import { PlanChange, PlanInput, Verifier, VerifyContext } from "../types.js";
2
+ import type { ChangedRun } from "../../utils/changedRuns.js";
3
+ /** Whether a change's `lines` reach this run. Any overlap counts: a run that the
4
+ * claimed range touches is the change's to answer for. */
5
+ export declare function changeCoversRun(change: PlanChange, run: ChangedRun): boolean;
6
+ /** The changed runs in files a planned test names as its changed file, each with
7
+ * the changes that can claim it: a change counts in the repositories of the tests
8
+ * that cite it, and in every repository when no test cites it. */
9
+ export declare function runsInTestedFiles(registration: PlanInput, ctx: VerifyContext): Array<{
10
+ repository: string;
11
+ run: ChangedRun;
12
+ changes: PlanChange[];
13
+ }>;
14
+ /** Every changed run in a file a planned test covers belongs to a change that
15
+ * states what a test must see. The server reads which lines changed; the agent
16
+ * says what they do. One objection per run. */
17
+ export declare const changedLinesClaimed: Verifier;
@@ -0,0 +1,88 @@
1
+ import { normalizeCitedPath } from "./citedPath.js";
2
+ import { CHANGED_LINES_CLAIMED_CONTRACT as CONTRACT } from "../verifierContracts.js";
3
+ /** One `lines` entry read as a file and a line range: `path:12` or `path:12-20`. */
4
+ function readLines(raw) {
5
+ if (typeof raw !== "string")
6
+ return undefined;
7
+ const match = /^(.+):(\d+)(?:-(\d+))?$/.exec(raw.trim());
8
+ if (!match)
9
+ return undefined;
10
+ const file = normalizeCitedPath(match[1]);
11
+ const start = Number(match[2]);
12
+ const end = match[3] === undefined ? start : Number(match[3]);
13
+ return file && end >= start ? { file, start, end } : undefined;
14
+ }
15
+ /** Whether a change's `lines` reach this run. Any overlap counts: a run that the
16
+ * claimed range touches is the change's to answer for. */
17
+ export function changeCoversRun(change, run) {
18
+ const lines = Array.isArray(change?.lines) ? change.lines : [];
19
+ return lines.some((raw) => {
20
+ const range = readLines(raw);
21
+ return (range !== undefined &&
22
+ range.file === run.file &&
23
+ range.start <= run.end &&
24
+ range.end >= run.start);
25
+ });
26
+ }
27
+ /** Whether a change states what its test must see: some case carries an
28
+ * `expectedValue` or a `derived` rule. */
29
+ function statesAnOutput(change) {
30
+ return (change.testCases ?? []).some((entry) => entry &&
31
+ (entry.expectedValue !== undefined ||
32
+ String(entry.derived ?? "").trim().length > 0));
33
+ }
34
+ /** The changed runs in files a planned test names as its changed file, each with
35
+ * the changes that can claim it: a change counts in the repositories of the tests
36
+ * that cite it, and in every repository when no test cites it. */
37
+ export function runsInTestedFiles(registration, ctx) {
38
+ const touched = new Set();
39
+ const citedIn = new Map();
40
+ for (const plannedTest of registration.plannedTests) {
41
+ const file = normalizeCitedPath(plannedTest.declarations.changedFile);
42
+ if (file)
43
+ touched.add(`${plannedTest.repository}\u0000${file}`);
44
+ for (const id of plannedTest.declarations.changes ?? []) {
45
+ const key = String(id ?? "").trim();
46
+ if (!citedIn.has(key))
47
+ citedIn.set(key, new Set());
48
+ citedIn.get(key)?.add(plannedTest.repository);
49
+ }
50
+ }
51
+ return Object.entries(ctx.changedRuns).flatMap(([repository, runs]) => runs
52
+ .filter((run) => touched.has(`${repository}\u0000${run.file}`))
53
+ .map((run) => ({
54
+ repository,
55
+ run,
56
+ changes: registration.changes.filter((change) => {
57
+ const repos = citedIn.get(String(change.id ?? "").trim());
58
+ return ((!repos || repos.has(repository)) && changeCoversRun(change, run));
59
+ }),
60
+ })));
61
+ }
62
+ /** Every changed run in a file a planned test covers belongs to a change that
63
+ * states what a test must see. The server reads which lines changed; the agent
64
+ * says what they do. One objection per run. */
65
+ export const changedLinesClaimed = {
66
+ name: "changedLinesClaimed",
67
+ run(registration, ctx) {
68
+ const objections = [];
69
+ for (const { repository, run, changes } of runsInTestedFiles(registration, ctx)) {
70
+ if (changes.some(statesAnOutput))
71
+ continue;
72
+ const kind = changes.length === 0 ? "unclaimed" : "noCase";
73
+ const where = `${run.file}:${run.start}${run.end > run.start ? `-${run.end}` : ""}`;
74
+ objections.push({
75
+ objectionId: `changedLinesClaimed:${kind}:${repository}/${run.file}:${run.start}`,
76
+ verifier: "changedLinesClaimed",
77
+ message: CONTRACT.objections[kind].message,
78
+ evidence: kind === "unclaimed"
79
+ ? `${repository} ${where} (\`${run.firstLine.trim().slice(0, 120)}\`): no change's \`lines\` covers it`
80
+ : `${repository} ${where} (\`${run.firstLine.trim().slice(0, 120)}\`): covered by ${changes
81
+ .map((change) => String(change.id ?? "?"))
82
+ .join(", ")}, and none of them has a case with \`expectedValue\` or \`derived\``,
83
+ suggestion: CONTRACT.objections[kind].suggestion,
84
+ });
85
+ }
86
+ return objections;
87
+ },
88
+ };
@@ -5,6 +5,7 @@ import { isTestPath } from "../../utils/testFileClassification.js";
5
5
  import { maintainedFileIsInItsRepository, normalizeCitedPath, } from "./citedPath.js";
6
6
  import { COVERAGE_CONTRACT, fillPlaceholders } from "../verifierContracts.js";
7
7
  import { isUIPlannedTest, stateRouteKey, subjectStep } from "../subjectStep.js";
8
+ import { normalizeStepMethod } from "../../types/StepMethod.js";
8
9
  /** Whether a path can be product code at all, by the deny list route grounding
9
10
  * already applies to a cited file. Nothing else is excluded: a file's kind is
10
11
  * not this check's judgment to make. */
@@ -640,9 +641,6 @@ function rejectObjections(registration) {
640
641
  }
641
642
  return objections;
642
643
  }
643
- // ── The surface a change is on ──────────────────────────────────────────────
644
- /** The test types that reach each surface. An `e2e` test drives the page AND the
645
- * requests behind it, so it counts for both. */
646
644
  const TEST_TYPES_FOR = {
647
645
  api: {
648
646
  types: new Set(["contract", "integration", "e2e"]),
@@ -658,7 +656,7 @@ function declaredSurfaces(change) {
658
656
  return [];
659
657
  return [
660
658
  ...new Set(named.map((surface) => String(surface ?? "").trim())),
661
- ].filter((surface) => surface in TEST_TYPES_FOR);
659
+ ].filter((surface) => Object.hasOwn(TEST_TYPES_FOR, surface));
662
660
  }
663
661
  /** A change on a page needs a test that opens the page, and a change on the API
664
662
  * needs one that calls it. Run 34065056830 cited a page change from an API test
@@ -698,6 +696,146 @@ function surfaceObjections(registration) {
698
696
  }
699
697
  return objections;
700
698
  }
699
+ /** Whether `text` names `value` as a whole word, ignoring case. Letters in any
700
+ * script: an ASCII boundary found "Gr" in "Größe". */
701
+ function namesWord(text, value) {
702
+ const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
703
+ return new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu").test(text);
704
+ }
705
+ /** An `absent` case names no value, so it draws no page objection. */
706
+ function pageObjectionKind(testCase) {
707
+ if (testCase.absent === true)
708
+ return undefined;
709
+ if (testCase.expect === "accept")
710
+ return "surfaceCase";
711
+ if (testCase.expect === "reject")
712
+ return "surfaceReject";
713
+ return undefined;
714
+ }
715
+ /** The value as a UI step would spell it, or undefined when no step can: a number,
716
+ * a boolean, text with no letter and a `{placeholder}` are never spelled in a step,
717
+ * and a page chooses no HTTP verb. */
718
+ function valueAPageSpells(testCase) {
719
+ const param = testCase.param.trim();
720
+ const value = testCase.value;
721
+ if (!param || param.toLowerCase() === METHOD_PARAM)
722
+ return undefined;
723
+ if (typeof value !== "string")
724
+ return undefined;
725
+ const word = value.trim();
726
+ if (!/\p{L}/u.test(word))
727
+ return undefined;
728
+ if (/^(true|false)$/i.test(word) || PLACEHOLDER_VALUE.test(word))
729
+ return undefined;
730
+ return word;
731
+ }
732
+ /** The page steps that choose a value. An `assert` that the radio is visible
733
+ * chose nothing: the XR02 run passed Principal on one, and no test saved at it.
734
+ * An e2e test's HTTP step is not the page either. */
735
+ const CHOOSING_VERBS = new Set([
736
+ "click",
737
+ "type",
738
+ "press",
739
+ "tap",
740
+ ]);
741
+ /** A refused value is also met by asserting the page no longer offers it. */
742
+ const REFUSING_VERBS = new Set([
743
+ ...CHOOSING_VERBS,
744
+ "assert",
745
+ ]);
746
+ /** The text that can name a value on the page: the steps of the UI tests whose verb
747
+ * is in `verbs`, and the edit note of each maintained spec, which is prose with no
748
+ * verb to read. */
749
+ function pageTexts(uiTests, maintained, verbs) {
750
+ return [
751
+ ...uiTests.flatMap((plannedTest) => plannedTest.scenario.steps
752
+ .filter((step) => verbs.has(normalizeStepMethod(step.method) ?? ""))
753
+ .flatMap((step) => [step.path, step.description ?? ""])),
754
+ ...maintained.map((entry) => entry.differsBy),
755
+ ];
756
+ }
757
+ /** A change on the page AND the API needs each accepted value chosen on the page,
758
+ * and each refused value tried or shown absent there, not only sent by an API
759
+ * test. `surfaceObjections` passes the page once any UI test cites the change,
760
+ * whatever value it picks. A UI step is free text (`Senior radio`), so the check
761
+ * reads whether a page step of a citing ui or e2e test that chooses the value (or,
762
+ * for a refused one, asserts it), or the `differsBy` of a citing maintained spec,
763
+ * names the value.
764
+ *
765
+ * A refused value takes a test, a resolving maintained spec, or a blocker, never a
766
+ * sentence: in the XR02 run the page objection for a removed option closed by
767
+ * prose, and only an existing spec checked that the option was gone. */
768
+ function surfaceCaseObjections(registration, ctx) {
769
+ const objections = [];
770
+ const seen = new Set();
771
+ const pageTypes = TEST_TYPES_FOR.page.types;
772
+ for (const change of registration.changes) {
773
+ const id = change.id.trim();
774
+ const surfaces = declaredSurfaces(change);
775
+ if (!id || !surfaces.includes("page") || !surfaces.includes("api"))
776
+ continue;
777
+ const uiTests = registration.plannedTests.filter((plannedTest) => citesChange(plannedTest, id) &&
778
+ pageTypes.has(plannedTest.scenario.testType.toLowerCase()));
779
+ // A maintained spec counts only when its file resolves, the bar
780
+ // `citedBehaviors` sets: the refused half closes by no sentence, so a note on a
781
+ // file nobody can open must not close it either.
782
+ const maintained = (registration.maintains ?? []).filter((entry) => entry.changes.some((cited) => cited.trim() === id) &&
783
+ maintainedFileIsInItsRepository(entry, ctx));
784
+ const citedAtAll = maintained.length > 0 ||
785
+ registration.plannedTests.some((plannedTest) => citesChange(plannedTest, id));
786
+ const texts = {
787
+ surfaceCase: pageTexts(uiTests, maintained, CHOOSING_VERBS),
788
+ surfaceReject: pageTexts(uiTests, maintained, REFUSING_VERBS),
789
+ };
790
+ for (const testCase of change.testCases ?? []) {
791
+ const kind = pageObjectionKind(testCase);
792
+ // An accepted value with no UI test and no maintained spec already draws
793
+ // `coverage:surface:<id>:page`, and a second objection would need a second
794
+ // answer for one gap. A maintained spec alone does not stop that objection,
795
+ // but a sentence citing the spec closed it while a value went untried, so the
796
+ // value is checked then. A refused value is always checked: that sentence is
797
+ // the gap. A change nothing cites draws `coverage:change:<id>`.
798
+ if (!kind || !citedAtAll)
799
+ continue;
800
+ if (kind === "surfaceCase" &&
801
+ uiTests.length === 0 &&
802
+ maintained.length === 0)
803
+ continue;
804
+ const word = valueAPageSpells(testCase);
805
+ if (word === undefined)
806
+ continue;
807
+ if (texts[kind].some((text) => namesWord(text, word)))
808
+ continue;
809
+ const rendered = renderTestCase({
810
+ ...testCase,
811
+ param: testCase.param.trim(),
812
+ value: word,
813
+ });
814
+ const objectionId = `coverage:${kind}:${id}:page:${rendered}`;
815
+ // One step closes `Intern` and `intern` alike, so they are one objection.
816
+ const key = objectionId.toLowerCase();
817
+ if (seen.has(key))
818
+ continue;
819
+ seen.add(key);
820
+ const citers = [
821
+ ...uiTests.map((plannedTest) => plannedTest.plannedTestId),
822
+ ...maintained.map((entry) => entry.file),
823
+ ];
824
+ objections.push({
825
+ objectionId,
826
+ verifier: "coverage",
827
+ message: fillPlaceholders(COVERAGE_CONTRACT.objections[kind].message, {
828
+ case: rendered,
829
+ }),
830
+ evidence: citers.length === 0
831
+ ? `No ui or e2e test or maintained spec cites ${id}.`
832
+ : `${citers.join(", ")} cite ${id}; no step or maintained edit of theirs names ${word}.`,
833
+ suggestion: COVERAGE_CONTRACT.objections[kind].suggestion,
834
+ });
835
+ }
836
+ }
837
+ return objections;
838
+ }
701
839
  /** One repository's changed files, and the name to attribute uncovered ones to.
702
840
  * Keying the objection id on the path alone let two repositories that each change
703
841
  * `src/App.tsx` share one id, so every id names its repository. */
@@ -759,6 +897,7 @@ export const coverage = {
759
897
  ...testCaseObjections(registration),
760
898
  ...rejectObjections(registration),
761
899
  ...surfaceObjections(registration),
900
+ ...surfaceCaseObjections(registration, ctx),
762
901
  ];
763
902
  if (allChangedFiles(ctx).length === 0)
764
903
  return objections;
@@ -4,6 +4,7 @@ import { EXPECTED_VALUE_SOURCED_CONTRACT as CONTRACT } from "../verifierContract
4
4
  import { appearsIn, searchable } from "../pullRequestText.js";
5
5
  import { escapeRegExp } from "../../utils/regex.js";
6
6
  import { renderTestCase } from "./coverage.js";
7
+ import { runsInTestedFiles } from "./changedLinesClaimed.js";
7
8
  function readSource(raw) {
8
9
  const source = typeof raw === "string" ? raw.trim() : "";
9
10
  if (source.length === 0)
@@ -119,6 +120,8 @@ export const expectedValueSourced = {
119
120
  const changed = changedPathSet(allChangedFiles(ctx), true);
120
121
  const prText = searchable(ctx.pullRequest);
121
122
  const raised = new Set();
123
+ // The changes that own a changed run in a file a planned test covers.
124
+ const owners = new Set(runsInTestedFiles(registration, ctx).flatMap(({ changes }) => changes));
122
125
  const changes = registration.changes;
123
126
  for (const change of changes) {
124
127
  const changeId = String(change.id ?? "?");
@@ -170,6 +173,11 @@ export const expectedValueSourced = {
170
173
  raise("unsourced", CONTRACT.objections.missingSource, `expected value with no usable \`expectedFrom\`: ${where}`);
171
174
  }
172
175
  else if (source.kind === "code") {
176
+ // A change read from the diff that names its changed lines has no other
177
+ // source: no requirement exists for the code to disagree with.
178
+ if (/^diff$/i.test(String(change.source ?? "").trim()) &&
179
+ owners.has(change))
180
+ return;
173
181
  raise("code", CONTRACT.objections.sourcedFromCode,
174
182
  // A derived-only case pins no value, so quoting one read `expected
175
183
  // undefined` — a sentence naming nothing the agent can act on.