@skyramp/mcp 0.4.2 → 0.4.3-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/build/execution/wrapperConfig.d.ts +4 -0
  2. package/build/execution/wrapperConfig.js +14 -2
  3. package/build/prompts/code-reuse.d.ts +5 -1
  4. package/build/prompts/code-reuse.js +16 -10
  5. package/build/prompts/modularization/ui-test-modularization.js +9 -2
  6. package/build/prompts/pom-aware-code-reuse.d.ts +4 -1
  7. package/build/prompts/pom-aware-code-reuse.js +38 -6
  8. package/build/prompts/reuse-hand-off.d.ts +4 -0
  9. package/build/prompts/reuse-hand-off.js +10 -5
  10. package/build/prompts/shared-helper-policy.d.ts +5 -0
  11. package/build/prompts/shared-helper-policy.js +5 -0
  12. package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
  13. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +2 -1
  14. package/build/prompts/testbot/testbot-prompts.js +33 -16
  15. package/build/recommendation/fakeVerifyContext.d.ts +3 -0
  16. package/build/recommendation/fakeVerifyContext.js +16 -0
  17. package/build/recommendation/runVerifiers.js +2 -0
  18. package/build/recommendation/types.d.ts +12 -0
  19. package/build/recommendation/verifierContracts.d.ts +24 -2
  20. package/build/recommendation/verifierContracts.js +32 -4
  21. package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
  22. package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
  23. package/build/recommendation/verifiers/expectedValueSourced.js +8 -0
  24. package/build/recommendation/verifiers/requirementSourced.js +141 -11
  25. package/build/services/TestGenerationService.js +4 -0
  26. package/build/tools/code-refactor/caller-gate.d.ts +105 -0
  27. package/build/tools/code-refactor/caller-gate.js +174 -0
  28. package/build/tools/code-refactor/changed-helper-report.d.ts +62 -0
  29. package/build/tools/code-refactor/changed-helper-report.js +114 -0
  30. package/build/tools/code-refactor/codeReuseTool.js +131 -62
  31. package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -2
  32. package/build/tools/code-refactor/helper-callers.d.ts +65 -0
  33. package/build/tools/code-refactor/helper-callers.js +85 -0
  34. package/build/tools/code-refactor/modularizationTool.js +10 -0
  35. package/build/tools/code-refactor/pom-pass-state.d.ts +51 -0
  36. package/build/tools/code-refactor/pom-pass-state.js +115 -0
  37. package/build/tools/code-refactor/retrofit-state.d.ts +3 -2
  38. package/build/tools/code-refactor/retrofit-state.js +11 -12
  39. package/build/tools/code-refactor/reuse-outcome.d.ts +6 -0
  40. package/build/tools/code-refactor/reuse-record-store.d.ts +52 -0
  41. package/build/tools/code-refactor/reuse-record-store.js +126 -0
  42. package/build/tools/code-refactor/reuse-state.d.ts +18 -6
  43. package/build/tools/code-refactor/reuse-state.js +78 -131
  44. package/build/tools/code-refactor/utils-verify-gates.js +3 -3
  45. package/build/tools/code-refactor/verify-gates.d.ts +4 -0
  46. package/build/tools/code-refactor/verify-gates.js +8 -1
  47. package/build/tools/generate-tests/generateContractRestTool.js +7 -7
  48. package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -8
  49. package/build/tools/generate-tests/generateUIRestTool.js +3 -3
  50. package/build/tools/submitReportTool.js +91 -29
  51. package/build/tools/test-management/actionsTool.js +7 -0
  52. package/build/tools/test-management/registerTestPlanTool.d.ts +1 -0
  53. package/build/tools/test-management/registerTestPlanTool.js +70 -22
  54. package/build/types/RepositoryAnalysis.d.ts +20 -20
  55. package/build/types/ReuseOutcome.d.ts +96 -0
  56. package/build/types/TestExecution.d.ts +11 -0
  57. package/build/types/TestExecution.js +19 -0
  58. package/build/types/index.d.ts +1 -1
  59. package/build/utils/branchDiff.d.ts +1 -1
  60. package/build/utils/branchDiff.js +1 -1
  61. package/build/utils/changedRuns.d.ts +13 -0
  62. package/build/utils/changedRuns.js +56 -0
  63. package/build/utils/featureFlags.d.ts +8 -10
  64. package/build/utils/featureFlags.js +23 -21
  65. package/build/utils/normalizeSkyrampImports.d.ts +4 -1
  66. package/build/utils/normalizeSkyrampImports.js +24 -17
  67. package/build/utils/pom-scope/ownership.d.ts +19 -0
  68. package/build/utils/pom-scope/ownership.js +31 -0
  69. package/build/utils/pom-scope/pom-files.d.ts +8 -0
  70. package/build/utils/pom-scope/pom-files.js +8 -0
  71. package/build/utils/pom-scope/scoring.js +13 -1
  72. package/build/utils/pom-scope/strip.d.ts +10 -0
  73. package/build/utils/pom-scope/strip.js +9 -0
  74. package/build/utils/pom-verify/verify.js +13 -2
  75. package/build/utils/reuseRouting.d.ts +30 -2
  76. package/build/utils/reuseRouting.js +36 -15
  77. package/build/utils/testDependencyPolicy.js +4 -16
  78. package/build/utils/utils-verify/action-sites.d.ts +2 -2
  79. package/build/utils/utils-verify/body-reach.d.ts +2 -2
  80. package/build/utils/utils-verify/body-reach.js +4 -1
  81. package/build/utils/utils-verify/call-sites.d.ts +16 -6
  82. package/build/utils/utils-verify/call-sites.js +12 -6
  83. package/build/utils/utils-verify/changed-helpers.d.ts +38 -0
  84. package/build/utils/utils-verify/changed-helpers.js +91 -0
  85. package/build/utils/utils-verify/head.d.ts +48 -0
  86. package/build/utils/utils-verify/head.js +115 -0
  87. package/build/utils/utils-verify/importers.d.ts +59 -10
  88. package/build/utils/utils-verify/importers.js +121 -31
  89. package/build/utils/utils-verify/in-house.js +26 -31
  90. package/build/utils/utils-verify/index.d.ts +3 -0
  91. package/build/utils/utils-verify/index.js +3 -0
  92. package/build/utils/utils-verify/language-spec.js +10 -2
  93. package/build/utils/utils-verify/locate.d.ts +3 -0
  94. package/build/utils/utils-verify/locate.js +6 -6
  95. package/build/utils/utils-verify/parse.d.ts +13 -1
  96. package/build/utils/utils-verify/parse.js +122 -49
  97. package/build/utils/utils-verify/retrofit-equivalence.d.ts +0 -5
  98. package/build/utils/utils-verify/retrofit-equivalence.js +2 -34
  99. package/build/utils/utils-verify/stage.js +4 -16
  100. package/build/utils/utils-verify/verify.d.ts +9 -3
  101. package/build/utils/utils-verify/verify.js +26 -12
  102. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1 -0
  103. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +1 -0
  104. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +6 -0
  105. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +303 -150
  106. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  107. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-LNgEKtdV.js → codeMirrorModule-Bou8kKzE.js} +1 -1
  108. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-Bwr1eMKC.js → defaultSettingsView-DTp2-8Si.js} +3 -3
  109. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.BAkLd5DX.js → index.ZYuEFHsi.js} +1 -1
  110. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  111. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.BPopbasy.js → uiMode.GLqsx5cI.js} +1 -1
  112. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  113. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  114. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  115. package/node_modules/playwright/package.json +1 -1
  116. package/package.json +2 -2
  117. package/plugin/prompts/code-reuse/hand-off.md +13 -1
  118. package/plugin/prompts/plan-tests.md +2 -2
  119. package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -0
@@ -305,11 +305,25 @@ export const REQUIREMENT_SOURCED_CONTRACT = {
305
305
  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
306
  },
307
307
  // 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.
308
+ // was given, and it holds the diff. A requirements file the pull request neither
309
+ // names nor changes is a file the agent found by looking, which is reading the
310
+ // repository, not reading a requirement.
310
311
  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`.",
312
+ message: "Change `{change}` cites `{source}`, and the pull request neither names nor changes such a file.",
313
+ 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`.",
314
+ },
315
+ // `{item}` is one top-level list line of the cited file or the description,
316
+ // `{number}` its number, `{source}` that source as the first change citing it
317
+ // spelled it, without its `#` anchor. The check
318
+ // reads only whether some change names the number, not whether a test covers it.
319
+ itemUnlinked: {
320
+ message: '`{source}` lists item {number}, "{item}", and no change in this plan names it.',
321
+ suggestion: "Set `source` to `{source}#{number}` on the change that covers this item, or answer with the reason it needs no test.",
322
+ },
323
+ // `{count}` is the number of top-level list items in `{source}`.
324
+ unnumbered: {
325
+ message: "`{source}` lists {count} items, and no change names one of them by its number.",
326
+ suggestion: "Name the item each change covers in its source, as `{source}#<number>` (`#7,8` for two), so each item can be checked.",
313
327
  },
314
328
  // Says only what was read: this pull request arrived with no words in it.
315
329
  emptyPullRequest: {
@@ -368,6 +382,20 @@ export const DEFECTS_CONTRACT = {
368
382
  },
369
383
  suggestion: "Which planned test proves each defect the review found, and for one nothing proves, why does it need no test?",
370
384
  };
385
+ export const CHANGED_LINES_CLAIMED_CONTRACT = {
386
+ id: "changedLinesClaimed",
387
+ objections: {
388
+ unclaimed: {
389
+ message: "These changed lines are in a file a planned test covers, and no change in the plan names them in its `lines`.",
390
+ 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.",
391
+ },
392
+ noCase: {
393
+ message: "These changed lines belong to a change that states no case, so no test is held to what they do.",
394
+ 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.",
395
+ },
396
+ },
397
+ suggestion: "Which change owns each run of changed lines in a tested file, and what must a test see because of it?",
398
+ };
371
399
  /** Fills a contract sentence's `{placeholder}` spans from the run's or the
372
400
  * planned test's own values. A placeholder with no value is left as written rather
373
401
  * 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
+ };
@@ -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.
@@ -1,47 +1,111 @@
1
+ import { allChangedFiles, } from "../types.js";
1
2
  import { normalizeCategory } from "../../types/TestRecommendation.js";
2
3
  import { REQUIREMENT_SOURCED_CONTRACT, fillPlaceholders, } from "../verifierContracts.js";
3
- import { normalizeCitedPath } from "./citedPath.js";
4
+ import { changedPathSet, normalizeCitedPath } from "./citedPath.js";
4
5
  import { appearsIn, searchable } from "../pullRequestText.js";
5
6
  /** Verifier 10. A change names where the agent read it, and the server reads the
6
7
  * name back.
7
8
  *
8
9
  * `source` is the record of where a change came from, and the server holds the
9
10
  * pull request's own words, so it reads the claim back against them: a `spec:` path
10
- * has to name a file the checkout holds AND a file the pull request names, and a
11
+ * has to name a file the checkout holds AND a file the pull request names or changes, and a
11
12
  * change read from the pull request has to quote words the pull request carries.
12
13
  *
14
+ * A requirements file the pull request names or changes, and the description
15
+ * itself, are read for their lists: every top-level item needs a change that names
16
+ * it by number (`spec:<path>#11`, `pr-description#3`), or the plan answers why it
17
+ * needs no test.
18
+ *
13
19
  * A conflict is a disagreement between a requirement and the code. A change read
14
20
  * off the diff IS the code, so there is no second side for it to disagree with.
15
21
  *
16
22
  * The plan tool always supplies the pull request: the testbot fetches its prompt
17
23
  * from this same server, so the text is in the process. Both fields blank means
18
24
  * the run had no title and no description. */
19
- const { unsourced: UNSOURCED, unreadable: UNREADABLE, fromDiff: FROM_DIFF, noChange: NO_CHANGE, notNamed: NOT_NAMED, emptyPullRequest: EMPTY_PULL_REQUEST, notQuoted: NOT_QUOTED, unquoted: UNQUOTED, } = REQUIREMENT_SOURCED_CONTRACT.objections;
25
+ const { unsourced: UNSOURCED, unreadable: UNREADABLE, fromDiff: FROM_DIFF, noChange: NO_CHANGE, notNamed: NOT_NAMED, emptyPullRequest: EMPTY_PULL_REQUEST, notQuoted: NOT_QUOTED, unquoted: UNQUOTED, itemUnlinked: ITEM_UNLINKED, unnumbered: UNNUMBERED, } = REQUIREMENT_SOURCED_CONTRACT.objections;
20
26
  const CONFLICT_CATEGORY = "requirement_conflict";
27
+ const itemsIn = (anchor) => anchor !== undefined && /^\s*\d+(\s*,\s*\d+)*\s*$/.test(anchor)
28
+ ? anchor.split(",").map((n) => Number(n.trim()))
29
+ : [];
21
30
  function readSource(raw) {
22
31
  const none = (kind) => ({
23
32
  kind,
24
33
  file: "",
25
34
  citedAs: "",
35
+ items: [],
26
36
  });
27
37
  const source = raw.trim();
28
38
  if (source.length === 0)
29
39
  return none("unknown");
30
- if (/^(pr-title|pr-description)$/i.test(source))
40
+ if (/^pr-title$/i.test(source))
31
41
  return none("pullRequest");
42
+ const description = /^pr-description(?:#(.*))?$/i.exec(source);
43
+ if (description)
44
+ return { ...none("pullRequest"), items: itemsIn(description[1]) };
32
45
  if (/^diff$/i.test(source))
33
46
  return none("diff");
34
47
  const named = /^spec:(.*)$/i.exec(source);
35
48
  if (!named)
36
49
  return none("unknown");
37
- const citedAs = named[1]
38
- .split(/[#§]/)[0]
50
+ const [path, anchor] = named[1].split(/#/);
51
+ const citedAs = path
52
+ .split(/§/)[0]
39
53
  .replace(/(?::\d+)+(?:-\d+)?$/, "")
40
54
  .trim();
41
55
  const file = normalizeCitedPath(citedAs);
42
56
  // `spec:` with nothing after it names no file, so there is nothing to check and
43
57
  // nothing to trust either.
44
- return file ? { kind: "spec", file, citedAs } : none("unknown");
58
+ return file
59
+ ? { kind: "spec", file, citedAs, items: itemsIn(anchor) }
60
+ : none("unknown");
61
+ }
62
+ const LIST_LINE = /^([ \t]*)(\d+(?=[.)])|[-*+•])[.)]?\s+(.*)$/;
63
+ /** Only a prose file holds a list of requirements. A YAML or JSON file is a list of
64
+ * settings, and each of its lines would read as one. */
65
+ const PROSE_FILE = /(^|\/)[^./]+$|\.(md|markdown|txt|rst|adoc)$/i;
66
+ /** The top-level items of a list: its numbered or bulleted lines at the smallest
67
+ * indent. A nested bullet is not an item of its own. Lines inside a fenced code
68
+ * block or a leading front-matter block are not items, and neither is a line with
69
+ * no letter or digit (a `- - -` rule). A text with no list has no items.
70
+ *
71
+ * An item's number is the one the text writes (`11.`), so a plan can cite it as the
72
+ * reader sees it, and a bulleted list is numbered by position from 1. A text whose
73
+ * numbers repeat (two numbered lists) or that mixes numbered and bulleted items
74
+ * has no numbering a plan can cite, so it gives no items. */
75
+ function topLevelListItems(text) {
76
+ const lines = text
77
+ .replace(/^\uFEFF/, "")
78
+ .replace(/^---\r?\n[\s\S]*?\r?\n---(\r?\n|$)/, "")
79
+ .split(/\r\n?|\n/);
80
+ // The marker that opened the fence, so ``` is closed by ``` and not by ~~~.
81
+ let fence;
82
+ const listed = lines.flatMap((line) => {
83
+ const marker = /^\s*(```|~~~)/.exec(line)?.[1];
84
+ if (marker && (fence === undefined || fence === marker))
85
+ fence = fence === undefined ? marker : undefined;
86
+ const match = fence !== undefined || marker ? null : LIST_LINE.exec(line);
87
+ if (!match || !/[\p{L}\p{N}]/u.test(match[3]))
88
+ return [];
89
+ return [
90
+ { indent: match[1].length, written: match[2], text: match[3].trim() },
91
+ ];
92
+ });
93
+ const top = Math.min(...listed.map((line) => line.indent));
94
+ const items = listed.filter((line) => line.indent === top);
95
+ const written = items.map((item) => Number.parseInt(item.written, 10));
96
+ if (written.every(Number.isInteger)) {
97
+ // Numbers that repeat mean two numbered lists; no single number names an item.
98
+ if (new Set(written).size !== written.length)
99
+ return [];
100
+ return items.map((item, index) => ({
101
+ number: written[index],
102
+ text: item.text,
103
+ }));
104
+ }
105
+ // A mix of numbered and bulleted lines has no numbering a reader can cite.
106
+ if (written.some(Number.isInteger))
107
+ return [];
108
+ return items.map((item, index) => ({ number: index + 1, text: item.text }));
45
109
  }
46
110
  /** A source that stands for a requirement: the pull request, or a requirements file. */
47
111
  const statesARequirement = (source) => source.kind === "pullRequest" || source.kind === "spec";
@@ -54,6 +118,12 @@ export const requirementSourced = {
54
118
  const sourceById = new Map();
55
119
  const quoteById = new Map();
56
120
  const pullRequestText = searchable(ctx.pullRequest);
121
+ const changedPaths = changedPathSet(allChangedFiles(ctx), false);
122
+ // The list items the changes name, per requirements file the pull request
123
+ // names or changes, and for the description.
124
+ const descriptionItems = [];
125
+ let descriptionCited = false;
126
+ const itemsBySpec = new Map();
57
127
  for (const change of changes) {
58
128
  const id = String(change.id ?? "").trim();
59
129
  const changeId = id || "(unnamed)";
@@ -75,6 +145,9 @@ export const requirementSourced = {
75
145
  continue;
76
146
  }
77
147
  if (source.kind === "pullRequest") {
148
+ if (/^pr-description/i.test(written))
149
+ descriptionCited = true;
150
+ descriptionItems.push(...source.items);
78
151
  if (pullRequestText.length > 0)
79
152
  continue;
80
153
  objections.push({
@@ -103,11 +176,17 @@ export const requirementSourced = {
103
176
  });
104
177
  continue;
105
178
  }
106
- // A requirements file the pull request never names is a file the agent found by
107
- // looking through the repository, which is not reading a requirement.
179
+ // A requirements file the pull request neither names nor changes is a file the
180
+ // agent found by looking through the repository, which is not reading a
181
+ // requirement.
108
182
  if (appearsIn(pullRequestText, source.file) ||
109
- appearsIn(pullRequestText, source.citedAs))
183
+ appearsIn(pullRequestText, source.citedAs) ||
184
+ changedPaths.has(source.file)) {
185
+ const cited = itemsBySpec.get(source.file) ?? { written, items: [] };
186
+ cited.items.push(...source.items);
187
+ itemsBySpec.set(source.file, cited);
110
188
  continue;
189
+ }
111
190
  objections.push({
112
191
  objectionId: `requirementSourced:notNamed:${changeId}`,
113
192
  verifier: "requirementSourced",
@@ -115,10 +194,61 @@ export const requirementSourced = {
115
194
  change: changeId,
116
195
  source: written,
117
196
  }),
118
- evidence: `change ${changeId} states source \`${written}\`; neither the title nor the description names ${source.file}`,
197
+ evidence: `change ${changeId} states source \`${written}\`; neither the title nor the description names ${source.file}, and the diff does not change it`,
119
198
  suggestion: NOT_NAMED.suggestion,
120
199
  });
121
200
  }
201
+ // Every item of a list needs a change that names it by number. A list no change
202
+ // numbers at all is one objection, not one per item: the plan is not citing the
203
+ // list by item yet, and one answer covers that.
204
+ const objectToUnlinked = (items, linked, where, source) => {
205
+ if (items.length === 0)
206
+ return;
207
+ // A number the list does not hold names nothing.
208
+ if (!linked.some((n) => items.some((item) => item.number === n))) {
209
+ objections.push({
210
+ objectionId: `requirementSourced:unnumbered:${where}`,
211
+ verifier: "requirementSourced",
212
+ message: fillPlaceholders(UNNUMBERED.message, {
213
+ source,
214
+ count: String(items.length),
215
+ }),
216
+ evidence: `${where} lists ${items.length} item(s); no change names one as \`#<number>\``,
217
+ suggestion: fillPlaceholders(UNNUMBERED.suggestion, { source }),
218
+ });
219
+ return;
220
+ }
221
+ for (const item of items) {
222
+ if (linked.includes(item.number))
223
+ continue;
224
+ objections.push({
225
+ objectionId: `requirementSourced:specItem:${where}#${item.number}`,
226
+ verifier: "requirementSourced",
227
+ message: fillPlaceholders(ITEM_UNLINKED.message, {
228
+ source,
229
+ number: String(item.number),
230
+ item: item.text,
231
+ }),
232
+ evidence: `${where} lists item ${item.number}, "${item.text}"; no change names #${item.number}`,
233
+ suggestion: fillPlaceholders(ITEM_UNLINKED.suggestion, {
234
+ source,
235
+ number: String(item.number),
236
+ }),
237
+ });
238
+ }
239
+ };
240
+ for (const [file, { written, items }] of itemsBySpec) {
241
+ if (!PROSE_FILE.test(file))
242
+ continue;
243
+ const text = ctx.readSpecFile(file);
244
+ if (text === undefined)
245
+ continue;
246
+ objectToUnlinked(topLevelListItems(text), items, file, `spec:${written.slice(5).split("#")[0]}`);
247
+ }
248
+ // The description's own list is read the same way, when a change cites the
249
+ // description. Its sentences are not read.
250
+ if (descriptionCited)
251
+ objectToUnlinked(topLevelListItems(ctx.pullRequest.description), descriptionItems, "pr-description", "pr-description");
122
252
  for (const plannedTest of plannedTests) {
123
253
  if (normalizeCategory(plannedTest.scenario.category) !== CONFLICT_CATEGORY)
124
254
  continue;
@@ -14,6 +14,7 @@ import { stageGeneratedPaths, resolveOutputDir } from "../utils/gitStaging.js";
14
14
  import { isInsideExecutorWorkDir, executorWorkDirRefusal, generationTargets, } from "../utils/executorWorkDir.js";
15
15
  import { getTestsRepoDir } from "../utils/AnalysisStateManager.js";
16
16
  import { recordReuseHandOff } from "../tools/code-refactor/reuse-state.js";
17
+ import { isPomFirstTarget } from "../utils/reuseRouting.js";
17
18
  import { recordAssertionBaseline } from "../tools/code-refactor/assertion-state.js";
18
19
  import { persistGeneratedTest } from "../utils/generatedTestRecord.js";
19
20
  import { buildModularizeFirstNextSteps, isModularizeFirstHandOffTarget, reuseCodeArgsClause, } from "../prompts/reuse-hand-off.js";
@@ -166,6 +167,8 @@ export class TestGenerationService {
166
167
  // and the report can mark it — without ever owing it for a test that was not
167
168
  // told to run it. Keyed by the files this call wrote: a test generated into
168
169
  // the same directory without codeReuse owes nothing.
170
+ // Both reuse settings on: the page-object pass runs first (isPomFirstTarget).
171
+ const pomFirst = isPomFirstTarget(params.language, reuseTestType);
169
172
  await recordReuseHandOff(written, testType, params.language, effectiveFramework);
170
173
  // Browser tests: the assertion enhancer anchors new assertions to
171
174
  // selectors present in the test file, and after modularize → reuse
@@ -182,6 +185,7 @@ ${buildModularizeFirstNextSteps({
182
185
  language: params.language,
183
186
  framework: effectiveFramework,
184
187
  enhanceFirst,
188
+ pomFirst,
185
189
  })}
186
190
  `;
187
191
  }
@@ -0,0 +1,105 @@
1
+ import type { UnifiedAnalysisState } from "../../utils/AnalysisStateManager.js";
2
+ import { type TestExecutionStatus } from "../../types/TestExecution.js";
3
+ import type { HelperChangeKind } from "../../types/ReuseOutcome.js";
4
+ import type { ChangedHelperView } from "./helper-callers.js";
5
+ /** One changed helper as a caller meets it. */
6
+ export interface CalledChange {
7
+ /** Absolute path of the module it lives in — with several modules in scope, a
8
+ * helper NAME alone does not say which one this caller met. */
9
+ utilsFile: string;
10
+ helper: string;
11
+ change: HelperChangeKind;
12
+ }
13
+ /** The execution recorded for one caller of a changed helper. */
14
+ export interface CallerExecution {
15
+ /** Absolute path of the caller. */
16
+ file: string;
17
+ /** Present only when an execution ran the module as it stands. */
18
+ execution?: TestExecutionStatus;
19
+ /** This run delivered or edited the caller, so its own report row carries its
20
+ * execution. Without this, a missing `execution` here reads as "never run" for a
21
+ * file the run did run. */
22
+ deliveredByThisRun?: true;
23
+ }
24
+ /** A caller the run CAN execute that has no result against the changed module.
25
+ *
26
+ * Carries what running it needs, because the gate has it in hand — it reads the
27
+ * inventory entry to decide the file is runnable at all — and a remedy the agent has
28
+ * to go and look values up for is a round spent discovering them. All three are
29
+ * required on an inventory entry (`TestAnalysisResult`), and an owed caller always
30
+ * has one: a caller without an entry is never owed. */
31
+ export interface OwedCaller {
32
+ /** Absolute path of the caller. */
33
+ file: string;
34
+ /** The changed helpers it calls. */
35
+ helpers: CalledChange[];
36
+ language: string;
37
+ framework: string;
38
+ /** Not guessable from the path: SKYR-4435 records a shared module inventoried
39
+ * with `unknown`. */
40
+ testType: string;
41
+ }
42
+ export interface ChangedHelperGateParams {
43
+ views: ChangedHelperView[];
44
+ /** `existingTests` says which callers are inventoried and runnable;
45
+ * `testExecutions` holds the latest run of ANY file — the inventory's
46
+ * `executionAfter` is written for a pre-existing test only, and a caller this
47
+ * run created has its result nowhere else. */
48
+ state: Pick<UnifiedAnalysisState, "existingTests" | "testExecutions">;
49
+ /** Absolute paths of everything this run changed, across every checkout. */
50
+ changedFilesAbs: string[];
51
+ }
52
+ export interface ChangedHelperGateResult {
53
+ /** Per caller of a changed helper, by canonical path, the execution that ran the
54
+ * changed module. */
55
+ executions: Map<string, CallerExecution>;
56
+ /** Callers the run can execute that have no result against the changed module.
57
+ * Empty is the ordinary case. */
58
+ owed: OwedCaller[];
59
+ }
60
+ /**
61
+ * The report gate over the callers of a changed helper: each one the run CAN execute
62
+ * must have been run on the module as it stands. A caller that ran and FAILED is
63
+ * disclosed with its result and blocks nothing — the reviewer reads the red and
64
+ * decides, and SKYR-4451 adds the classification that decides for them.
65
+ *
66
+ * This is the retrofit gate's rule turned outward. A retrofit is a caller this run
67
+ * EDITED; a caller here is one it did not touch at all, which is why nothing ran it.
68
+ * The identity check differs accordingly (`freshExecution` is the same rule for
69
+ * both): a retrofit's execution must post-date the CALLER's last edit, and this one's
70
+ * must post-date the MODULE's — the caller is unchanged, and the bytes that changed
71
+ * under it are the module's.
72
+ *
73
+ * FAILS OPEN on every doubt, and the doubts are enumerated rather than inferred. It
74
+ * demands an execution only for a caller it has positive evidence about: the caller
75
+ * set is known, the file was inventoried as a Skyramp-generated test this run did not
76
+ * deliver, and the run is able to execute it. A helper whose callers could not be
77
+ * enumerated demands nothing; a false demand on a helper nobody calls is worse than
78
+ * a missing report line.
79
+ */
80
+ export declare function changedHelperGate(params: ChangedHelperGateParams): Promise<ChangedHelperGateResult>;
81
+ /**
82
+ * The refusal a set of unexecuted callers makes, and the exact call that clears it.
83
+ *
84
+ * GIVES the remedy rather than describing it. A sentence naming only the path and
85
+ * the phase produced a call missing arguments `skyramp_execute_test` requires, so the
86
+ * agent spent a round discovering which and could not always recover. The
87
+ * `unresolved-module` finding is the model: it hands over the literal header line to
88
+ * write, not a description of one. Here every value the server holds is printed per
89
+ * file, and the one it does not — the repository's own command for the file — is
90
+ * covered by the same instruction `skyramp_actions` gives for a before-run
91
+ * (`runTestCall`), so the two gates never describe the same call two ways.
92
+ *
93
+ * The arguments common to every caller are stated once above the list; a wall of
94
+ * eight calls × six arguments is its own way of being unreadable. A value is never
95
+ * dropped to shorten it.
96
+ *
97
+ * NOTHING HERE IS ABOUT A FAILING CALLER. This refuses on the ABSENCE of a result,
98
+ * which the agent can always remedy with one call per file. A caller that ran and
99
+ * failed is disclosed with its result and refuses nothing — see SKYR-4451.
100
+ */
101
+ export declare function unrunCallerRefusal(owed: OwedCaller[], ctx: {
102
+ repoRoot: string;
103
+ repository: string;
104
+ stateFile: string;
105
+ }): string | undefined;