@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
@@ -27,6 +27,7 @@ import { getReportLanguage, isEnforcedReportLanguage, findLanguageViolations, fi
27
27
  import { canonicalTestPath, findAssertionRecordByFileName, rederiveAssertionOutcome, } from "./code-refactor/assertion-state.js";
28
28
  import { rederiveReuse, deriveReuseFromDelivered, REUSE_SUBMIT_MAX_REFUSALS, reuseChainSkipped, samePath, } from "./code-refactor/reuse-state.js";
29
29
  import { retrofitGate, } from "./code-refactor/retrofit-state.js";
30
+ import { changedHelpersForRow, deriveChangedHelpers, discloseChangedHelpers, } from "./code-refactor/changed-helper-report.js";
30
31
  import { onlyCommentLinesRemoved, stripGateMarkers, } from "./code-refactor/gate-markers.js";
31
32
  import { parseKeptInline } from "./code-refactor/reuse-outcome.js";
32
33
  import { hasUtilsHeader, locateUtilsFiles, parseUtilsAllows, realpath, utilsSpecForFile, } from "../utils/utils-verify/index.js";
@@ -705,21 +706,9 @@ function generatedPathFor(fileName, generated) {
705
706
  reason: `${matches.length} generated files match ${fileName} and this row does not say which, so its imports could not be read`,
706
707
  };
707
708
  }
708
- async function attachReuseOutcome(test, outcomes, handOffs, retrofits,
709
- // A blocking verdict measured on the delivered files is collected here rather
710
- // than written into the row: the caller refuses the report on it. Required, not
711
- // defaulted — a caller that forgot it would ship the report with no refusal and
712
- // no compiler error. See the refusal below the row map.
713
- blocking,
709
+ async function attachReuseOutcome(test, ctx,
714
710
  /** Declines this run removed from the delivered files. See stripDeliveredGateMarkers. */
715
711
  carriedDeclines,
716
- /** Filled with the carried declines a finding consumed. What consumed nothing is
717
- * retired: permission that covers no fault authorises the NEXT one instead. */
718
- consumed,
719
- /** Filled with the files whose declines this pass actually MEASURED. Only those may
720
- * be retired: a pass that read nothing consumes nothing, and deleting on that
721
- * silence drops permission while the fault it covers is still in the file. */
722
- evaluated,
723
712
  /** The row's absolute path when the caller has it (a maintenance row does; a
724
713
  * newTestsCreated row carries a basename only). With it, a record or hand-off
725
714
  * that names a same-named spec in another directory is not this row's. */
@@ -733,6 +722,8 @@ specPath,
733
722
  * and the row states that reason. Omitting it altogether says the row has no
734
723
  * delivered file to read — a VERIFY row, or one this run never wrote. */
735
724
  deliveredPath) {
725
+ const { outcomes, retrofits, changedHelpers, blocking, consumed, evaluated } = ctx;
726
+ let { handOffs } = ctx;
736
727
  // POM records describe browser specs; a basename collision with an API test's
737
728
  // fileName must not attach them there. A utils-path record is attachable anywhere.
738
729
  const byBasename = outcomes?.[path.basename(test.fileName)];
@@ -783,14 +774,39 @@ deliveredPath) {
783
774
  // So answer the row from the file instead. Only where there is nothing else to
784
775
  // report: a row that already carries a record or a chain claim is untouched, and
785
776
  // this can neither overwrite a measured value nor refuse a report.
777
+ // The row's own spec path, from the record when one attached and from the
778
+ // delivered file otherwise. Both disclosures below answer from it: gating on a
779
+ // stored record instead dropped every retrofit from a row answered by the
780
+ // fallback.
781
+ const rowSpecPath = found?.testFilePath ??
782
+ (typeof deliveredPath === "string" ? deliveredPath : undefined);
783
+ const changedHelperViews = rowSpecPath
784
+ ? changedHelpersForRow(changedHelpers, rowSpecPath)
785
+ : [];
786
786
  const fallback = !derived && !chainSkipped && deliveredPath && isUtilsReuseEnabled()
787
787
  ? typeof deliveredPath === "string"
788
788
  ? await deriveReuseFromDelivered(deliveredPath)
789
789
  : { notDerived: deliveredPath.reason }
790
790
  : undefined;
791
- const reuse = derived || chainSkipped
791
+ const base = derived || chainSkipped
792
792
  ? { ...(derived ?? {}), ...(chainSkipped ? { chainSkipped } : {}) }
793
793
  : fallback;
794
+ // Helpers this run changed under callers it never touched. Attached to the row
795
+ // itself rather than inside `helpers`, and outside the record branch, on purpose:
796
+ // a run that deleted or renamed the shared module has no helpers row to hang this
797
+ // on — verifyUtils locates nothing — and that is the case with the most to
798
+ // disclose, since every helper went at once. It is layered OVER the assembled row
799
+ // rather than folded into `derived`, so a row that would otherwise be answered by
800
+ // the import-derived fallback still gets that answer as well.
801
+ const reuse = changedHelperViews.length > 0
802
+ ? {
803
+ ...(base ?? {}),
804
+ // "This run rewrote code you already had, and here is who calls it" — a
805
+ // statement the report has never made: it already names a module that was
806
+ // CREATED, and nothing named one that was changed.
807
+ changedHelpers: discloseChangedHelpers(changedHelperViews, changedHelpers.executions),
808
+ }
809
+ : base;
794
810
  if (!reuse)
795
811
  return test;
796
812
  // Pre-existing generated tests this spec's reuse pass rewired (SKYR-4276 A4): the
@@ -801,8 +817,6 @@ deliveredPath) {
801
817
  // path. Gating it on a stored record instead dropped every retrofit from a row
802
818
  // answered by the fallback — and the retrofit gate still refuses the report until
803
819
  // each one has run, so the run was blocked on a disclosure the report never made.
804
- const rowSpecPath = found?.testFilePath ??
805
- (typeof deliveredPath === "string" ? deliveredPath : undefined);
806
820
  if (reuse.helpers && rowSpecPath) {
807
821
  const mine = retrofits.filter((r) => samePath(r.testFile, rowSpecPath));
808
822
  if (mine.length > 0)
@@ -1737,6 +1751,14 @@ export function registerSubmitReportTool(server) {
1737
1751
  // rows below; an unexecuted one refuses the report (inside the SKYR-3883 block,
1738
1752
  // which already enumerates the working tree).
1739
1753
  let retrofitViews = [];
1754
+ // The helpers this run changed in a module that already existed,
1755
+ // and the tests that call them. Empty until the working-tree scan below runs —
1756
+ // outside a testbot run there is no tree to compare against, and an empty set
1757
+ // discloses nothing and demands nothing, which is the right silence.
1758
+ let changedHelpers = {
1759
+ byDir: new Map(),
1760
+ executions: new Map(),
1761
+ };
1740
1762
  /** Absolute paths this run changed. Undefined when they cannot be read — every
1741
1763
  * reuse check falls back to acting on what it located. */
1742
1764
  let changedFilesAbs;
@@ -1749,8 +1771,18 @@ export function registerSubmitReportTool(server) {
1749
1771
  // verify against created/modified/untracked paths, not the eventual commit.
1750
1772
  if (isTestbotEnabled()) {
1751
1773
  const fullState = await stateManager.readFullState();
1752
- const repoRoot = fullState?.metadata?.repositoryPath;
1753
- if (repoRoot && repoRoot !== "unknown") {
1774
+ const metadata = fullState?.metadata;
1775
+ const repoRoot = metadata?.repositoryPath;
1776
+ if (metadata && repoRoot && repoRoot !== "unknown") {
1777
+ const { repository } = metadata;
1778
+ // Every checkout this run may have written to, the primary first. Listed
1779
+ // once, for the claim check, the changed-file scan and the changed-helper
1780
+ // derivation.
1781
+ const otherCheckouts = [
1782
+ getTestsRepoDir(),
1783
+ ...Object.values(fullState?.relatedRepos ?? {}).map((section) => section.repositoryPath),
1784
+ ];
1785
+ const checkoutRoots = [repoRoot, ...otherCheckouts];
1754
1786
  const dependencyRoots = collectDependencyVerificationRoots(repoRoot, getTestsRepoDir(), fullState?.relatedRepos);
1755
1787
  for (const dependencyRoot of dependencyRoots) {
1756
1788
  try {
@@ -1783,10 +1815,7 @@ export function registerSubmitReportTool(server) {
1783
1815
  // non-git one contributes nothing.
1784
1816
  const changedFiles = [
1785
1817
  ...(await listChangedFiles(repoRoot)),
1786
- ...(await listChangedFilesAcross([
1787
- getTestsRepoDir(),
1788
- ...Object.values(fullState?.relatedRepos ?? {}).map((section) => section.repositoryPath),
1789
- ])),
1818
+ ...(await listChangedFilesAcross(otherCheckouts)),
1790
1819
  ];
1791
1820
  // Primary verdicts only, by design: this checks UPDATEs, and
1792
1821
  // skyramp_actions downgrades every actionable related-repo verdict to
@@ -1831,17 +1860,41 @@ export function registerSubmitReportTool(server) {
1831
1860
  `Files with actual working-tree changes:\n${changedList || " (none)"}`);
1832
1861
  return errorResult;
1833
1862
  }
1834
- changedFilesAbs = await listChangedFilesAbs([
1835
- repoRoot,
1836
- getTestsRepoDir(),
1837
- ...Object.values(fullState?.relatedRepos ?? {}).map((section) => section.repositoryPath),
1838
- ]);
1863
+ changedFilesAbs = await listChangedFilesAbs(checkoutRoots);
1839
1864
  const gate = await retrofitGate(stateData, changedFilesAbs, repoRoot);
1840
1865
  retrofitViews = gate.views;
1841
1866
  if (gate.refusal) {
1842
1867
  errorResult = toolError(gate.refusal);
1843
1868
  return errorResult;
1844
1869
  }
1870
+ // A shared helper's change reaches every caller at once, and a caller the
1871
+ // run did not touch is never delivered — so nothing executed it and a
1872
+ // break in it reached the customer's continuous integration rather than
1873
+ // this report. Derived here, beside the retrofit gate, because both need
1874
+ // the same three things: the working tree's changed files, the test
1875
+ // inventory, and the repository root.
1876
+ // Derived from git, not from the reuse records, so a helper the
1877
+ // maintenance phase repaired with an UPDATE on the module is found the
1878
+ // same way as one the reuse pass rewrote. The refusal's remedy names
1879
+ // the repository section and state file `skyramp_execute_test` records
1880
+ // into.
1881
+ changedHelpers = await deriveChangedHelpers({
1882
+ roots: checkoutRoots,
1883
+ changedFilesAbs,
1884
+ state: stateData,
1885
+ repoRoot,
1886
+ repository,
1887
+ stateFile,
1888
+ });
1889
+ // An unexecuted caller refuses, because the remedy is one tool call the
1890
+ // agent can always make. A caller that RAN and FAILED does not: it is
1891
+ // disclosed with its result and the reviewer decides. Classifying it —
1892
+ // the helper's fault or the test's — is SKYR-4451's, and a cruder rule
1893
+ // stated here instead is the thing that ticket objected to.
1894
+ if (changedHelpers.refusal) {
1895
+ errorResult = toolError(changedHelpers.refusal);
1896
+ return errorResult;
1897
+ }
1845
1898
  }
1846
1899
  catch (err) {
1847
1900
  // Never block a valid report because verification itself failed (path not a
@@ -1920,8 +1973,17 @@ export function registerSubmitReportTool(server) {
1920
1973
  const consumedDeclines = [];
1921
1974
  /** Files whose stored declines a re-derivation actually measured on this call. */
1922
1975
  const evaluatedDeclineFiles = new Set();
1976
+ const reuseRowContext = {
1977
+ outcomes: stateData.reuseOutcomes,
1978
+ handOffs: stateData.reuseHandOffs,
1979
+ retrofits: retrofitViews,
1980
+ changedHelpers,
1981
+ blocking: reuseBlocking,
1982
+ consumed: consumedDeclines,
1983
+ evaluated: evaluatedDeclineFiles,
1984
+ };
1923
1985
  const sanitizedNewTests = await Promise.all(dedupedNewTests.map(async ({ scenarioFile: _sf, traceFile: _tf, frontendTrace: _ft, ...rest }) => {
1924
- const row = await attachReuseOutcome(rest, stateData.reuseOutcomes, stateData.reuseHandOffs, retrofitViews, reuseBlocking, declinesBySpec.get(path.basename(rest.fileName)) ?? [], consumedDeclines, evaluatedDeclineFiles, undefined, generatedPathFor(rest.fileName, stateData.generatedTests ?? {}));
1986
+ const row = await attachReuseOutcome(rest, reuseRowContext, declinesBySpec.get(path.basename(rest.fileName)) ?? [], undefined, generatedPathFor(rest.fileName, stateData.generatedTests ?? {}));
1925
1987
  return attachAssertionOutcome(row, stateData.assertionOutcomes ?? {}, assertionCheckouts);
1926
1988
  }));
1927
1989
  const maintenanceRows = testMaintenance
@@ -1952,7 +2014,7 @@ export function registerSubmitReportTool(server) {
1952
2014
  ? { editedFileName: path.basename(pomFile) }
1953
2015
  : {}),
1954
2016
  ...(assertions ? { assertions } : {}),
1955
- }, stateData.reuseOutcomes, stateData.reuseHandOffs, retrofitViews, reuseBlocking, declinesBySpec.get(path.basename(testFilePath)) ?? [], consumedDeclines, evaluatedDeclineFiles, testFilePath,
2017
+ }, reuseRowContext, declinesBySpec.get(path.basename(testFilePath)) ?? [], testFilePath,
1956
2018
  // Only where the run edited the file and left a test standing.
1957
2019
  // VERIFY and IGNORE read a repository-owned test and changed nothing,
1958
2020
  // and DELETE removed it — none of the three has reuse to state, so a
@@ -13,6 +13,7 @@ import { REBASELINE_SNAPSHOT_NAME_RE, looksLikeOnDiskBaselineName, } from "../..
13
13
  import { buildRenameStrategy, buildFileRenameStrategy, buildUpdateStrategy, buildRebaselineStrategy, buildRegenerateStrategy, buildDeleteStrategy, buildUpdateFileInstruction, buildRegenerateFileInstruction, } from "../../prompts/test-maintenance/actionsInstructions.js";
14
14
  import { buildModularizeFirstNextSteps, isMaintenanceRegenerable, isModularizeFirstHandOffTarget, regenerateCodeReuse, } from "../../prompts/reuse-hand-off.js";
15
15
  import { recordReuseHandOff } from "../code-refactor/reuse-state.js";
16
+ import { isPomFirstTarget } from "../../utils/reuseRouting.js";
16
17
  import { isPlanOnlyMode } from "../../utils/planOnlyMode.js";
17
18
  import { runTestCall } from "../../skills/runTestSkill.js";
18
19
  import { formatOwedFiles, testsOwedBeforeRun } from "./testsOwedBeforeRun.js";
@@ -612,6 +613,12 @@ export function registerActionsTool(server) {
612
613
  if (!isModularizeFirstHandOffTarget(testType, language))
613
614
  return undefined;
614
615
  const framework = cataloged?.framework ?? "";
616
+ // Both reuse settings on: a maintained browser test is not composed here.
617
+ // The maintenance step edits the customer's page objects directly and never
618
+ // runs the page-object pass, so arming the pair would move the steps it just
619
+ // pointed at page-object members into a shared module. Generation composes.
620
+ if (isPomFirstTarget(language, testType))
621
+ return undefined;
615
622
  await recordReuseHandOff([rec.testFilePath], testType, language, framework, args.stateFile, "maintenance");
616
623
  return buildModularizeFirstNextSteps({
617
624
  testType,
@@ -823,6 +823,7 @@ export declare function buildRegistration(params: RegisterTestPlanParams, regist
823
823
  * case-sensitive one, so that is the answer a real run gets.
824
824
  */
825
825
  export declare function makeCitedFileCheck(state: RunState): (relativePath: string, repository?: string) => boolean;
826
+ export declare function makeSpecFileReader(state: RunState): (relativePath: string) => string | undefined;
826
827
  /**
827
828
  * The route patterns that render a changed frontend file, across every repository
828
829
  * of the run.
@@ -8,6 +8,7 @@ import { StateManager, resolveOwnRunStatePath, } from "../../utils/AnalysisState
8
8
  import { toolError } from "../../utils/utils.js";
9
9
  import { TESTBOT_SUPPORTED_TEST_TYPES } from "../../types/TestTypes.js";
10
10
  import { changedFileNames, computeBranchDiff, } from "../../utils/branchDiff.js";
11
+ import { collectChangedRuns } from "../../utils/changedRuns.js";
11
12
  import { LOCKFILE, NOT_SOURCE_DIR, } from "../../recommendation/verifiers/endpointGrounded.js";
12
13
  import { hasUITestTypeLabel, isHTTPStep, } from "../../recommendation/subjectStep.js";
13
14
  import { STEP_METHOD_DESCRIPTION, isUiStepVerb, normalizeStepMethod, stepMethodSchema, } from "../../types/StepMethod.js";
@@ -15,7 +16,7 @@ import { registerPlan } from "../../recommendation/registerPlan.js";
15
16
  import { targetElementSchema } from "../submitReportTool.js";
16
17
  import { capturedBlueprintDigests } from "../../playwright/blueprintDigestStore.js";
17
18
  import { pullRequestText } from "../../recommendation/pullRequestText.js";
18
- import { fileExistsWithinRoot } from "../../utils/containedPath.js";
19
+ import { fileExistsWithinRoot, readFileWithinRoot, } from "../../utils/containedPath.js";
19
20
  import { createScreenRouteResolver } from "../../utils/screenRoutes.js";
20
21
  import { categoryMenu, SCENARIO_CATEGORIES, } from "../../types/TestRecommendation.js";
21
22
  const TOOL_NAME = TOOL_REGISTER_TEST_PLAN;
@@ -397,10 +398,17 @@ const registerTestPlanSchema = {
397
398
  .describe("The requirement in the words the pull request uses, when `source` is `pr-title` or `pr-description`. CHECKED on a change a `requirement_conflict` test cites: the quote must appear in the title or description."),
398
399
  source: z
399
400
  .string()
400
- .refine((value) => /^(pr-title|pr-description|diff|spec:\S.*)$/i.test(value.trim()), {
401
+ .refine((value) => /^(pr-title|pr-description(#\d+(,\d+)*)?|diff|spec:\S.*)$/i.test(value.trim()), {
401
402
  message: "source must be `pr-title`, `pr-description`, `diff`, or `spec:<path>`",
402
403
  })
403
- .describe("Where you read it: `pr-title`, `pr-description`, `spec:<path>` for a requirements file the description names, or `diff`."),
404
+ .describe("Where you read it: `pr-title`, `pr-description`, `spec:<path>` for a requirements file the description names or the diff adds, or `diff`. If the requirement is an item of a numbered or bulleted list, add its number: `spec:<path>#11` or `pr-description#3` (`#7,8` when one change covers two items). Number a bulleted list from 1."),
405
+ lines: z
406
+ .array(z
407
+ .string()
408
+ .regex(/^.+:\d+(-\d+)?$/, "a `lines` entry is `path:12` or `path:12-20`"))
409
+ .min(1)
410
+ .optional()
411
+ .describe("The changed lines this change is, as `path:12` or `path:12-20` in the new file's numbering, the path from the repository root. Every run of changed lines in a file a planned test covers belongs to some change, and that change states in a case what a test must see."),
404
412
  surfaces: z
405
413
  .array(z.enum(["api", "page"]))
406
414
  .min(1)
@@ -434,7 +442,7 @@ const registerTestPlanSchema = {
434
442
  message: "expectedFrom must be `pr-title`, `pr-description`, `code`, `spec:<path>`, or `convention:<file:line>`",
435
443
  })
436
444
  .optional()
437
- .describe("Where you read `expectedValue` or `derived`: `pr-title`, `pr-description`, `spec:<path>` for a requirements file the description names, `convention:<file:line>` for a rule the application already follows in code this pull request did not change, or `code` for the code under test. `code` draws an objection — it is the one source that agrees with a defect — so name it only when nothing else states the value. A value read from the pull request has to be WRITTEN there — in the quoted sentence or elsewhere in the title or description. If the pull request states the rule and never writes the number, leave `expectedValue` out and put the rule in `derived`. A case that declares `expectedValue` or `derived` and names no source here draws an objection."),
445
+ .describe("Where you read `expectedValue` or `derived`: `pr-title`, `pr-description`, `spec:<path>` for a requirements file the description names, `convention:<file:line>` for a rule the application already follows in code this pull request did not change, or `code` for the code under test. `code` draws an objection — it is the one source that agrees with a defect — so name it only when nothing else states the value. On a change whose `source` is `diff` and whose `lines` name changed lines in a file a planned test covers, `code` is accepted: the diff is the only source such a change has. A value read from the pull request has to be WRITTEN there — in the quoted sentence or elsewhere in the title or description. If the pull request states the rule and never writes the number, leave `expectedValue` out and put the rule in `derived`. A case that declares `expectedValue` or `derived` and names no source here draws an objection."),
438
446
  })
439
447
  .strict()
440
448
  .refine((entry) => (entry.value !== undefined) !== (entry.absent === true), {
@@ -645,20 +653,8 @@ function repoSections(state) {
645
653
  })),
646
654
  ];
647
655
  }
648
- /**
649
- * Whether a cited repository file EXISTS inside the checkout. Reads no content,
650
- * and never throws: a bad citation is an objection, not a crashed tool.
651
- *
652
- * CONFINEMENT IS THIS FUNCTION'S JOB. The sibling verifier's string check misses
653
- * `file://` and `http://` URLs, a drive-relative `C:routes.rb` and a
654
- * percent-encoded `..`, so the containment in `containedPath` is the only thing
655
- * between an agent-written path and a read outside the checkout.
656
- *
657
- * CASE IS THE FILESYSTEM'S RULE: a case-only misspelling passes on a
658
- * case-insensitive checkout and is not found on a case-sensitive one. CI is the
659
- * case-sensitive one, so that is the answer a real run gets.
660
- */
661
- export function makeCitedFileCheck(state) {
656
+ /** The real path of each repository's checkout, keyed by `owner/repo`. */
657
+ function checkoutPathByRepo(state) {
662
658
  // Keyed by `owner/repo`, like `makeScreenRouteResolver`: two repositories can hold
663
659
  // the same relative path.
664
660
  const rootsByRepo = new Map();
@@ -674,6 +670,23 @@ export function makeCitedFileCheck(state) {
674
670
  // A repository path that no longer resolves holds no readable file.
675
671
  }
676
672
  }
673
+ return rootsByRepo;
674
+ }
675
+ /**
676
+ * Whether a cited repository file EXISTS inside the checkout. Reads no content,
677
+ * and never throws: a bad citation is an objection, not a crashed tool.
678
+ *
679
+ * CONFINEMENT IS THIS FUNCTION'S JOB. The sibling verifier's string check misses
680
+ * `file://` and `http://` URLs, a drive-relative `C:routes.rb` and a
681
+ * percent-encoded `..`, so the containment in `containedPath` is the only thing
682
+ * between an agent-written path and a read outside the checkout.
683
+ *
684
+ * CASE IS THE FILESYSTEM'S RULE: a case-only misspelling passes on a
685
+ * case-insensitive checkout and is not found on a case-sensitive one. CI is the
686
+ * case-sensitive one, so that is the answer a real run gets.
687
+ */
688
+ export function makeCitedFileCheck(state) {
689
+ const rootsByRepo = checkoutPathByRepo(state);
677
690
  return (relativePath, repository) => {
678
691
  if (typeof relativePath !== "string")
679
692
  return false;
@@ -688,6 +701,19 @@ export function makeCitedFileCheck(state) {
688
701
  return [...new Set(rootsByRepo.values())].some((root) => fileExistsWithinRoot(root, cited));
689
702
  };
690
703
  }
704
+ /** A requirements file is a page of prose; past this it is not one. */
705
+ const SPEC_FILE_MAX_BYTES = 256 * 1024;
706
+ export function makeSpecFileReader(state) {
707
+ const roots = [...new Set(checkoutPathByRepo(state).values())];
708
+ return (relativePath) => {
709
+ for (const root of roots) {
710
+ const text = readFileWithinRoot(root, relativePath, SPEC_FILE_MAX_BYTES);
711
+ if (text !== undefined)
712
+ return text;
713
+ }
714
+ return undefined;
715
+ };
716
+ }
691
717
  /**
692
718
  * The route patterns that render a changed frontend file, across every repository
693
719
  * of the run.
@@ -719,14 +745,34 @@ export function makeScreenRouteResolver(state) {
719
745
  /** Which files each repository's branch diff changed. NAMES ONLY, never persisted.
720
746
  * Lockfiles and non-source directories are dropped, so a planned test cannot cite
721
747
  * `package-lock.json` and nothing demands a test for one. */
748
+ /** The runs of changed lines, read from the full diff against the ref the name
749
+ * listing resolved, so both describe the same comparison. A failure costs only
750
+ * the changed-lines check: the file names still reach every other one. */
751
+ async function changedRunsAgainst(repositoryPath, baseBranch) {
752
+ try {
753
+ const diff = await computeBranchDiff(repositoryPath, baseBranch, false);
754
+ return collectChangedRuns(diff.diffContent).filter((run) => !(LOCKFILE.test(run.file) || NOT_SOURCE_DIR.test(run.file)));
755
+ }
756
+ catch (error) {
757
+ logger.warning("Verify-time diff failed; the changed-lines check sees no changed lines", {
758
+ repositoryPath,
759
+ error: error instanceof Error ? error.message : String(error),
760
+ });
761
+ return [];
762
+ }
763
+ }
722
764
  async function changedFilesForVerification(section) {
723
765
  const repositoryPath = section.repositoryPath;
724
766
  if (!repositoryPath || repositoryPath === "unknown")
725
767
  return undefined;
726
768
  const baseBranch = section.data?.repositoryAnalysis?.diff?.baseBranch;
727
769
  try {
728
- const files = changedFileNames(await computeBranchDiff(repositoryPath, baseBranch, false, true)).filter((file) => !(LOCKFILE.test(file.path) || NOT_SOURCE_DIR.test(file.path)));
729
- return files.length > 0 ? files : undefined;
770
+ const diff = await computeBranchDiff(repositoryPath, baseBranch, false, true);
771
+ const files = changedFileNames(diff).filter((file) => !(LOCKFILE.test(file.path) || NOT_SOURCE_DIR.test(file.path)));
772
+ return {
773
+ files: files.length > 0 ? files : undefined,
774
+ runs: await changedRunsAgainst(repositoryPath, diff.baseBranch),
775
+ };
730
776
  }
731
777
  catch (error) {
732
778
  // A repository git cannot read yields no changed files, which
@@ -756,10 +802,11 @@ export async function buildVerifyContext(state) {
756
802
  // Per repository, so a planned test is checked against the one it names. Every
757
803
  // planned test names one; a single-repository run names that one repository.
758
804
  return {
759
- changedFiles: Object.fromEntries(sections.map((section, i) => [section.repo, listings[i] ?? []])),
805
+ changedFiles: Object.fromEntries(sections.map((section, i) => [section.repo, listings[i]?.files ?? []])),
760
806
  reposWithoutDiff: sections
761
- .filter((_s, i) => !listings[i]?.length)
807
+ .filter((_s, i) => !listings[i]?.files?.length)
762
808
  .map((s) => s.repo),
809
+ changedRuns: Object.fromEntries(sections.map((section, i) => [section.repo, listings[i]?.runs ?? []])),
763
810
  // From the process, not the state file: the browser tools and this one are
764
811
  // registered on the same server, so captures and reader share a process.
765
812
  uiCaptures: capturedBlueprintDigests(),
@@ -772,6 +819,7 @@ export async function buildVerifyContext(state) {
772
819
  removedUiElements: sections.flatMap((section) => section.data?.uiContext?.removedElements ?? []),
773
820
  retiredUiElements: sections.flatMap((section) => section.data?.uiContext?.retiredElements ?? []),
774
821
  citedFileExists: makeCitedFileCheck(state),
822
+ readSpecFile: makeSpecFileReader(state),
775
823
  routesRenderingFile: makeScreenRouteResolver(state),
776
824
  };
777
825
  }
@@ -251,15 +251,15 @@ export declare const endpointInteractionSchema: z.ZodObject<{
251
251
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
252
252
  queryParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
253
253
  }, "strip", z.ZodTypeAny, {
254
+ queryParams?: Record<string, any> | undefined;
254
255
  body?: Record<string, any> | undefined;
255
256
  contentType?: string | undefined;
256
257
  headers?: Record<string, string> | undefined;
257
- queryParams?: Record<string, any> | undefined;
258
258
  }, {
259
+ queryParams?: Record<string, any> | undefined;
259
260
  body?: Record<string, any> | undefined;
260
261
  contentType?: string | undefined;
261
262
  headers?: Record<string, string> | undefined;
262
- queryParams?: Record<string, any> | undefined;
263
263
  }>;
264
264
  response: z.ZodObject<{
265
265
  statusCode: z.ZodNumber;
@@ -314,10 +314,10 @@ export declare const endpointInteractionSchema: z.ZodObject<{
314
314
  type: "error" | "success" | "edge-case";
315
315
  description: string;
316
316
  request: {
317
+ queryParams?: Record<string, any> | undefined;
317
318
  body?: Record<string, any> | undefined;
318
319
  contentType?: string | undefined;
319
320
  headers?: Record<string, string> | undefined;
320
- queryParams?: Record<string, any> | undefined;
321
321
  };
322
322
  response: {
323
323
  description: string;
@@ -336,10 +336,10 @@ export declare const endpointInteractionSchema: z.ZodObject<{
336
336
  type: "error" | "success" | "edge-case";
337
337
  description: string;
338
338
  request: {
339
+ queryParams?: Record<string, any> | undefined;
339
340
  body?: Record<string, any> | undefined;
340
341
  contentType?: string | undefined;
341
342
  headers?: Record<string, string> | undefined;
342
- queryParams?: Record<string, any> | undefined;
343
343
  };
344
344
  response: {
345
345
  description: string;
@@ -390,15 +390,15 @@ export declare const endpointMethodSchema: z.ZodObject<{
390
390
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
391
391
  queryParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
392
392
  }, "strip", z.ZodTypeAny, {
393
+ queryParams?: Record<string, any> | undefined;
393
394
  body?: Record<string, any> | undefined;
394
395
  contentType?: string | undefined;
395
396
  headers?: Record<string, string> | undefined;
396
- queryParams?: Record<string, any> | undefined;
397
397
  }, {
398
+ queryParams?: Record<string, any> | undefined;
398
399
  body?: Record<string, any> | undefined;
399
400
  contentType?: string | undefined;
400
401
  headers?: Record<string, string> | undefined;
401
- queryParams?: Record<string, any> | undefined;
402
402
  }>;
403
403
  response: z.ZodObject<{
404
404
  statusCode: z.ZodNumber;
@@ -453,10 +453,10 @@ export declare const endpointMethodSchema: z.ZodObject<{
453
453
  type: "error" | "success" | "edge-case";
454
454
  description: string;
455
455
  request: {
456
+ queryParams?: Record<string, any> | undefined;
456
457
  body?: Record<string, any> | undefined;
457
458
  contentType?: string | undefined;
458
459
  headers?: Record<string, string> | undefined;
459
- queryParams?: Record<string, any> | undefined;
460
460
  };
461
461
  response: {
462
462
  description: string;
@@ -475,10 +475,10 @@ export declare const endpointMethodSchema: z.ZodObject<{
475
475
  type: "error" | "success" | "edge-case";
476
476
  description: string;
477
477
  request: {
478
+ queryParams?: Record<string, any> | undefined;
478
479
  body?: Record<string, any> | undefined;
479
480
  contentType?: string | undefined;
480
481
  headers?: Record<string, string> | undefined;
481
- queryParams?: Record<string, any> | undefined;
482
482
  };
483
483
  response: {
484
484
  description: string;
@@ -512,10 +512,10 @@ export declare const endpointMethodSchema: z.ZodObject<{
512
512
  type: "error" | "success" | "edge-case";
513
513
  description: string;
514
514
  request: {
515
+ queryParams?: Record<string, any> | undefined;
515
516
  body?: Record<string, any> | undefined;
516
517
  contentType?: string | undefined;
517
518
  headers?: Record<string, string> | undefined;
518
- queryParams?: Record<string, any> | undefined;
519
519
  };
520
520
  response: {
521
521
  description: string;
@@ -551,10 +551,10 @@ export declare const endpointMethodSchema: z.ZodObject<{
551
551
  type: "error" | "success" | "edge-case";
552
552
  description: string;
553
553
  request: {
554
+ queryParams?: Record<string, any> | undefined;
554
555
  body?: Record<string, any> | undefined;
555
556
  contentType?: string | undefined;
556
557
  headers?: Record<string, string> | undefined;
557
- queryParams?: Record<string, any> | undefined;
558
558
  };
559
559
  response: {
560
560
  description: string;
@@ -632,15 +632,15 @@ export declare const enrichedEndpointSchema: z.ZodObject<{
632
632
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
633
633
  queryParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
634
634
  }, "strip", z.ZodTypeAny, {
635
+ queryParams?: Record<string, any> | undefined;
635
636
  body?: Record<string, any> | undefined;
636
637
  contentType?: string | undefined;
637
638
  headers?: Record<string, string> | undefined;
638
- queryParams?: Record<string, any> | undefined;
639
639
  }, {
640
+ queryParams?: Record<string, any> | undefined;
640
641
  body?: Record<string, any> | undefined;
641
642
  contentType?: string | undefined;
642
643
  headers?: Record<string, string> | undefined;
643
- queryParams?: Record<string, any> | undefined;
644
644
  }>;
645
645
  response: z.ZodObject<{
646
646
  statusCode: z.ZodNumber;
@@ -695,10 +695,10 @@ export declare const enrichedEndpointSchema: z.ZodObject<{
695
695
  type: "error" | "success" | "edge-case";
696
696
  description: string;
697
697
  request: {
698
+ queryParams?: Record<string, any> | undefined;
698
699
  body?: Record<string, any> | undefined;
699
700
  contentType?: string | undefined;
700
701
  headers?: Record<string, string> | undefined;
701
- queryParams?: Record<string, any> | undefined;
702
702
  };
703
703
  response: {
704
704
  description: string;
@@ -717,10 +717,10 @@ export declare const enrichedEndpointSchema: z.ZodObject<{
717
717
  type: "error" | "success" | "edge-case";
718
718
  description: string;
719
719
  request: {
720
+ queryParams?: Record<string, any> | undefined;
720
721
  body?: Record<string, any> | undefined;
721
722
  contentType?: string | undefined;
722
723
  headers?: Record<string, string> | undefined;
723
- queryParams?: Record<string, any> | undefined;
724
724
  };
725
725
  response: {
726
726
  description: string;
@@ -754,10 +754,10 @@ export declare const enrichedEndpointSchema: z.ZodObject<{
754
754
  type: "error" | "success" | "edge-case";
755
755
  description: string;
756
756
  request: {
757
+ queryParams?: Record<string, any> | undefined;
757
758
  body?: Record<string, any> | undefined;
758
759
  contentType?: string | undefined;
759
760
  headers?: Record<string, string> | undefined;
760
- queryParams?: Record<string, any> | undefined;
761
761
  };
762
762
  response: {
763
763
  description: string;
@@ -793,10 +793,10 @@ export declare const enrichedEndpointSchema: z.ZodObject<{
793
793
  type: "error" | "success" | "edge-case";
794
794
  description: string;
795
795
  request: {
796
+ queryParams?: Record<string, any> | undefined;
796
797
  body?: Record<string, any> | undefined;
797
798
  contentType?: string | undefined;
798
799
  headers?: Record<string, string> | undefined;
799
- queryParams?: Record<string, any> | undefined;
800
800
  };
801
801
  response: {
802
802
  description: string;
@@ -819,7 +819,6 @@ export declare const enrichedEndpointSchema: z.ZodObject<{
819
819
  }>, "many">;
820
820
  }, "strip", z.ZodTypeAny, {
821
821
  path: string;
822
- resourceGroup: string;
823
822
  pathParams: {
824
823
  type: string;
825
824
  name: string;
@@ -827,6 +826,7 @@ export declare const enrichedEndpointSchema: z.ZodObject<{
827
826
  description?: string | undefined;
828
827
  constraints?: string | undefined;
829
828
  }[];
829
+ resourceGroup: string;
830
830
  methods: {
831
831
  method: string;
832
832
  description: string;
@@ -843,10 +843,10 @@ export declare const enrichedEndpointSchema: z.ZodObject<{
843
843
  type: "error" | "success" | "edge-case";
844
844
  description: string;
845
845
  request: {
846
+ queryParams?: Record<string, any> | undefined;
846
847
  body?: Record<string, any> | undefined;
847
848
  contentType?: string | undefined;
848
849
  headers?: Record<string, string> | undefined;
849
- queryParams?: Record<string, any> | undefined;
850
850
  };
851
851
  response: {
852
852
  description: string;
@@ -869,7 +869,6 @@ export declare const enrichedEndpointSchema: z.ZodObject<{
869
869
  }[];
870
870
  }, {
871
871
  path: string;
872
- resourceGroup: string;
873
872
  pathParams: {
874
873
  type: string;
875
874
  name: string;
@@ -877,6 +876,7 @@ export declare const enrichedEndpointSchema: z.ZodObject<{
877
876
  description?: string | undefined;
878
877
  constraints?: string | undefined;
879
878
  }[];
879
+ resourceGroup: string;
880
880
  methods: {
881
881
  method: string;
882
882
  description: string;
@@ -893,10 +893,10 @@ export declare const enrichedEndpointSchema: z.ZodObject<{
893
893
  type: "error" | "success" | "edge-case";
894
894
  description: string;
895
895
  request: {
896
+ queryParams?: Record<string, any> | undefined;
896
897
  body?: Record<string, any> | undefined;
897
898
  contentType?: string | undefined;
898
899
  headers?: Record<string, string> | undefined;
899
- queryParams?: Record<string, any> | undefined;
900
900
  };
901
901
  response: {
902
902
  description: string;