@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
@@ -2,6 +2,7 @@ import { readFile } from "fs/promises";
2
2
  import * as path from "path";
3
3
  import { logger } from "../../utils/logger.js";
4
4
  import { isPomAwareTarget } from "../../utils/reuseRouting.js";
5
+ import { pageObjectCandidates } from "../../utils/pom-scope/ownership.js";
5
6
  import { selectScopedPoms, } from "../../utils/pom-scope/index.js";
6
7
  import { stripComments, stripStrings } from "../../utils/pom-scope/strip.js";
7
8
  import { escapeRegExp } from "../../utils/regex.js";
@@ -174,10 +175,16 @@ classification = {
174
175
  return undefined;
175
176
  try {
176
177
  const specContent = await readFile(params.testFile, "utf8");
177
- const { tier1, diagnostics } = await selectScopedPoms({
178
+ const scoped = await selectScopedPoms({
178
179
  testFile: params.testFile,
179
180
  specContent,
180
181
  });
182
+ const { diagnostics } = scoped;
183
+ // Both settings on: a free-function module is the in-house path's, so it is not
184
+ // a candidate this pass skipped — the same split discovery applied.
185
+ const tier1 = params.composed
186
+ ? await pageObjectCandidates(scoped.tier1)
187
+ : scoped.tier1;
181
188
  // Only fire on candidates found via conventional POM dirs (globs) — the selector-grep
182
189
  // discovery fallback also matches ordinary in-repo frontend source (app components
183
190
  // legitimately contain the spec's test-ids by construction), which is not evidence of
@@ -399,14 +399,14 @@ const CHAINING_SECTION = CONSUMER_MODE_ENABLED
399
399
  - \`parentStatusCode\`: expected HTTP status code for each parent provisioning call (e.g. \`{"id": "201"}\`). Not allowed with \`skipProvisionParents\`.`;
400
400
  export function registerContractTestTool(server) {
401
401
  server.registerTool(TOOL_NAME, {
402
- description: `${getPersonaPrefix()}Before calling this tool, you MUST output a <thinking> block that covers:
402
+ description: `${getPersonaPrefix()}Before calling this tool, verify these test inputs and requirements against the codebase, workspace config, API schema, or user input:
403
403
  1. The endpoint URL and HTTP method being tested
404
- 2. Whether the endpoint is a nested resource (URL contains a path parameter like \`{id}\`, \`{flow_id}\`, etc.) — if YES, decide: do I have the request body to provision the parent, or should I use skipProvisionParents?
405
- 3. Assertion planning — answer all four:
406
- a. Echo-back fields: which fields in the response are returned unchanged from the request body? (e.g. name, email, status) — these must be asserted with the exact sent value, not just not-null
407
- b. Computed fields: are there any numeric fields derived from other values? (e.g. total_amount, discount_amount) — these must be asserted with the formula, never a hardcoded number
408
- c. Test data isolation: does this test create its own resources via parentRequestData, or does it depend on pre-existing data? All IDs must come from provisioned resources, never hardcoded.
409
- d. Edge cases and business logic bugs: if \`skyramp_analyze_changes\` already ran and returned recommendations for this endpoint, revisit them now — did the recommendation phase flag any boundary conditions, invalid-input behaviors, or business logic bugs? Each flagged risk should map to a targeted assertion in this test. A test that doesn't target a known risk is low-value.
404
+ 2. For a nested resource (URL contains a path parameter like \`{id}\`, \`{flow_id}\`, etc.), use the request body to provision the parent, or use skipProvisionParents when it cannot be provisioned.
405
+ 3. Assertion requirements:
406
+ a. Echo-back fields: assert response fields returned unchanged from the request body (e.g. name, email, status) with the exact sent value, not just not-null.
407
+ b. Computed fields: assert numeric fields derived from other values (e.g. total_amount, discount_amount) with the formula, never a hardcoded number.
408
+ c. Test data isolation: create the test's own resources via parentRequestData. All IDs must come from provisioned resources, never hardcoded.
409
+ d. Edge cases and business logic bugs: if \`skyramp_analyze_changes\` already ran and returned recommendations for this endpoint, map each flagged boundary condition, invalid-input behavior, or business logic risk to a targeted assertion in this test.
410
410
  4. Each required parameter and what value it will take, with source (workspace config / diff / schema / user input)
411
411
  NEVER use a hardcoded ID (UUID or integer) as a path parameter value. If a real resource ID is needed and cannot be provisioned, use skipProvisionParents instead.
412
412
 
@@ -89,15 +89,15 @@ ${enhanceAssertionsStep("integration", "generated test file")}
89
89
  const TOOL_NAME = TOOL_INTEGRATION_TEST_GENERATION;
90
90
  export function registerIntegrationTestTool(server) {
91
91
  server.registerTool(TOOL_NAME, {
92
- description: `${getPersonaPrefix()}Before calling this tool, you MUST output a <thinking> block that covers:
92
+ description: `${getPersonaPrefix()}Before calling this tool, verify these test inputs and requirements against the codebase, workspace config, API schema, or user input:
93
93
  1. The endpoint URL(s) and HTTP method(s) involved in this multi-step workflow
94
- 2. Why an integration test (multi-step workflow validation) is the right choice for this intent
95
- 3. Assertion planning — answer all four for each step:
96
- a. Echo-back fields: which response fields are returned unchanged from the request body? List them by name — these get exact-value assertions, not just not-null checks
97
- b. Computed fields: are there derived numeric fields (e.g. total_amount = price × quantity)? Name the formula — these get formula-based assertions, never hardcoded numbers
98
- c. Chaining: which IDs in step N+1 come from step N's response? Name the JSON path (e.g. POST /orders response → order_id → GET /orders/{order_id}). Every ID must be dynamically chained — no hardcoded IDs anywhere in the test
99
- d. Edge cases and business logic bugs: if \`skyramp_analyze_changes\` already ran and returned recommendations for these endpoints, revisit them now — did the recommendation phase flag any boundary conditions, invalid-input behaviors, or business logic bugs? Each flagged risk should map to a targeted assertion in this test. A test that doesn't target a known risk is low-value.
100
- 4. Test data isolation: does this test create all its own resources from scratch, or does it depend on pre-existing database state? All prerequisite resources must be created within the test setup.
94
+ 2. The requested test exercises a multi-step workflow.
95
+ 3. Assertion requirements for each step:
96
+ a. Echo-back fields: assert response fields returned unchanged from the request body with the exact sent value, not just not-null.
97
+ b. Computed fields: assert derived numeric fields (e.g. total_amount = price × quantity) with the formula, never hardcoded numbers.
98
+ c. Chaining: use the relevant JSON path in step N's response for IDs in step N+1 (e.g. POST /orders response → order_id → GET /orders/{order_id}). Every ID must be dynamically chained — no hardcoded IDs anywhere in the test.
99
+ d. Edge cases and business logic bugs: if \`skyramp_analyze_changes\` already ran and returned recommendations for these endpoints, map each flagged boundary condition, invalid-input behavior, or business logic risk to a targeted assertion in this test.
100
+ 4. Test data isolation: create all prerequisite resources within the test setup, without depending on pre-existing database state.
101
101
  If any required parameter cannot be determined without guessing, STOP and ask the user before calling the tool.
102
102
 
103
103
  ---
@@ -101,10 +101,10 @@ export const uiTestSchema = {
101
101
  };
102
102
  export function registerUITestTool(server) {
103
103
  server.registerTool(TOOL_NAME, {
104
- description: `${getPersonaPrefix()}Before calling this tool, you MUST output a <thinking> block that covers:
104
+ description: `${getPersonaPrefix()}Before calling this tool, verify these test inputs against the Playwright trace, codebase, and filesystem:
105
105
  1. The user-facing flow(s) captured in the Playwright trace (pages visited, actions taken)
106
- 2. Why a UI test (Playwright-based interaction replay) is the right choice for this intent
107
- 3. Which assertions this test should validate (page content, element state, navigation results)
106
+ 2. The intended behavior is covered by the recorded Playwright interactions.
107
+ 3. The expected page content, element state, or navigation results the test must assert
108
108
  4. The absolute path to the trace zip and the output directory, with source confirmation
109
109
  If the trace zip path cannot be confirmed as an absolute path to an existing file, STOP and re-export before calling this tool.
110
110
 
@@ -25,8 +25,10 @@ import { budgetExcuseRefusals, citesBudget, countBudgetExcuseRefusal, MAX_BUDGET
25
25
  import { countUiDeliveryRefusal, MAX_UI_DELIVERY_REFUSALS, traceExportAttempts, uiDeliveryRefusals, } from "../playwright/traceExportStore.js";
26
26
  import { getReportLanguage, isEnforcedReportLanguage, findLanguageViolations, findLanguageNearMisses, reportLanguageDisplayName, } from "../utils/reportLanguage.js";
27
27
  import { canonicalTestPath, findAssertionRecordByFileName, rederiveAssertionOutcome, } from "./code-refactor/assertion-state.js";
28
- import { rederiveReuse, deriveReuseFromDelivered, REUSE_SUBMIT_MAX_REFUSALS, reuseChainSkipped, samePath, } from "./code-refactor/reuse-state.js";
28
+ import { canonPath, canonPathEvenIfGone, rederiveReuse, deriveReuseFromDelivered, REUSE_SUBMIT_MAX_REFUSALS, reuseChainSkipped, samePath, } from "./code-refactor/reuse-state.js";
29
+ import { removedModulesFrom, } from "./code-refactor/removed-modules.js";
29
30
  import { retrofitGate, } from "./code-refactor/retrofit-state.js";
31
+ import { changedHelpersForRow, deriveChangedHelpers, discloseChangedHelpers, } from "./code-refactor/changed-helper-report.js";
30
32
  import { onlyCommentLinesRemoved, stripGateMarkers, } from "./code-refactor/gate-markers.js";
31
33
  import { parseKeptInline } from "./code-refactor/reuse-outcome.js";
32
34
  import { hasUtilsHeader, locateUtilsFiles, parseUtilsAllows, realpath, utilsSpecForFile, } from "../utils/utils-verify/index.js";
@@ -705,21 +707,9 @@ function generatedPathFor(fileName, generated) {
705
707
  reason: `${matches.length} generated files match ${fileName} and this row does not say which, so its imports could not be read`,
706
708
  };
707
709
  }
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,
710
+ async function attachReuseOutcome(test, ctx,
714
711
  /** Declines this run removed from the delivered files. See stripDeliveredGateMarkers. */
715
712
  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
713
  /** The row's absolute path when the caller has it (a maintenance row does; a
724
714
  * newTestsCreated row carries a basename only). With it, a record or hand-off
725
715
  * that names a same-named spec in another directory is not this row's. */
@@ -733,6 +723,8 @@ specPath,
733
723
  * and the row states that reason. Omitting it altogether says the row has no
734
724
  * delivered file to read — a VERIFY row, or one this run never wrote. */
735
725
  deliveredPath) {
726
+ const { outcomes, retrofits, changedHelpers, removedModules, blocking, consumed, evaluated, } = ctx;
727
+ let { handOffs } = ctx;
736
728
  // POM records describe browser specs; a basename collision with an API test's
737
729
  // fileName must not attach them there. A utils-path record is attachable anywhere.
738
730
  const byBasename = outcomes?.[path.basename(test.fileName)];
@@ -783,14 +775,66 @@ deliveredPath) {
783
775
  // So answer the row from the file instead. Only where there is nothing else to
784
776
  // report: a row that already carries a record or a chain claim is untouched, and
785
777
  // this can neither overwrite a measured value nor refuse a report.
778
+ // The row's own spec path, from the record when one attached and from the
779
+ // delivered file otherwise. Both disclosures below answer from it: gating on a
780
+ // stored record instead dropped every retrofit from a row answered by the
781
+ // fallback.
782
+ const rowSpecPath = found?.testFilePath ??
783
+ (typeof deliveredPath === "string" ? deliveredPath : undefined);
784
+ const changedHelperViews = rowSpecPath
785
+ ? changedHelpersForRow(changedHelpers, rowSpecPath)
786
+ : [];
786
787
  const fallback = !derived && !chainSkipped && deliveredPath && isUtilsReuseEnabled()
787
788
  ? typeof deliveredPath === "string"
788
789
  ? await deriveReuseFromDelivered(deliveredPath)
789
790
  : { notDerived: deliveredPath.reason }
790
791
  : undefined;
791
- const reuse = derived || chainSkipped
792
+ const base = derived || chainSkipped
792
793
  ? { ...(derived ?? {}), ...(chainSkipped ? { chainSkipped } : {}) }
793
794
  : fallback;
795
+ // Two disclosures about a MODULE, layered over the assembled row rather than
796
+ // folded into `derived`, and outside the record branch, on purpose: a run that
797
+ // deleted or renamed the shared module has no helpers row to hang them on —
798
+ // verifyUtils locates nothing — and that is the case with the most to disclose.
799
+ // A row that would otherwise be answered by the import-derived fallback still
800
+ // gets that answer as well.
801
+ // - `changedHelpers`: "this run rewrote code you already had, and here is who
802
+ // calls it" — a statement the report has never made: it already names a
803
+ // module that was CREATED, and nothing named one that was changed.
804
+ // - `removedModules`: a module this run created beside this test and removed
805
+ // again; without it the row reads "nothing to reuse" over a run that built a
806
+ // module, rewired tests onto it and deleted it again. A row gets the modules
807
+ // its own record located — wherever they lived — and those in its own
808
+ // directory IN ITS OWN LANGUAGE, the tests the module was built beside: a
809
+ // Python row beside a TypeScript module was not one of them. Both sides
810
+ // canonical: the module path is a realpath from locateUtilsFiles, the spec
811
+ // path is as supplied, and on a symlinked checkout (macOS /tmp) the two
812
+ // never match raw.
813
+ const rowDir = rowSpecPath ? path.dirname(canonPath(rowSpecPath)) : undefined;
814
+ const rowLanguage = rowSpecPath
815
+ ? utilsSpecForFile(rowSpecPath)?.language
816
+ : undefined;
817
+ const listedByRecord = new Set((found?.utils?.modules ?? []).map((m) => canonPathEvenIfGone(m.file)));
818
+ const removedHere = removedModules.filter((m) => {
819
+ const file = canonPathEvenIfGone(m.file);
820
+ return (listedByRecord.has(file) ||
821
+ (path.dirname(file) === rowDir &&
822
+ rowLanguage !== undefined &&
823
+ utilsSpecForFile(file)?.language === rowLanguage));
824
+ });
825
+ const extras = {
826
+ ...(changedHelperViews.length > 0
827
+ ? {
828
+ changedHelpers: discloseChangedHelpers(changedHelperViews, changedHelpers.executions),
829
+ }
830
+ : {}),
831
+ ...(removedHere.length > 0
832
+ ? {
833
+ removedModules: removedHere.map(({ file: _file, ...entry }) => entry),
834
+ }
835
+ : {}),
836
+ };
837
+ const reuse = Object.keys(extras).length > 0 ? { ...(base ?? {}), ...extras } : base;
794
838
  if (!reuse)
795
839
  return test;
796
840
  // Pre-existing generated tests this spec's reuse pass rewired (SKYR-4276 A4): the
@@ -801,8 +845,6 @@ deliveredPath) {
801
845
  // path. Gating it on a stored record instead dropped every retrofit from a row
802
846
  // answered by the fallback — and the retrofit gate still refuses the report until
803
847
  // 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
848
  if (reuse.helpers && rowSpecPath) {
807
849
  const mine = retrofits.filter((r) => samePath(r.testFile, rowSpecPath));
808
850
  if (mine.length > 0)
@@ -1737,6 +1779,14 @@ export function registerSubmitReportTool(server) {
1737
1779
  // rows below; an unexecuted one refuses the report (inside the SKYR-3883 block,
1738
1780
  // which already enumerates the working tree).
1739
1781
  let retrofitViews = [];
1782
+ // The helpers this run changed in a module that already existed,
1783
+ // and the tests that call them. Empty until the working-tree scan below runs —
1784
+ // outside a testbot run there is no tree to compare against, and an empty set
1785
+ // discloses nothing and demands nothing, which is the right silence.
1786
+ let changedHelpers = {
1787
+ byDir: new Map(),
1788
+ executions: new Map(),
1789
+ };
1740
1790
  /** Absolute paths this run changed. Undefined when they cannot be read — every
1741
1791
  * reuse check falls back to acting on what it located. */
1742
1792
  let changedFilesAbs;
@@ -1749,8 +1799,18 @@ export function registerSubmitReportTool(server) {
1749
1799
  // verify against created/modified/untracked paths, not the eventual commit.
1750
1800
  if (isTestbotEnabled()) {
1751
1801
  const fullState = await stateManager.readFullState();
1752
- const repoRoot = fullState?.metadata?.repositoryPath;
1753
- if (repoRoot && repoRoot !== "unknown") {
1802
+ const metadata = fullState?.metadata;
1803
+ const repoRoot = metadata?.repositoryPath;
1804
+ if (metadata && repoRoot && repoRoot !== "unknown") {
1805
+ const { repository } = metadata;
1806
+ // Every checkout this run may have written to, the primary first. Listed
1807
+ // once, for the claim check, the changed-file scan and the changed-helper
1808
+ // derivation.
1809
+ const otherCheckouts = [
1810
+ getTestsRepoDir(),
1811
+ ...Object.values(fullState?.relatedRepos ?? {}).map((section) => section.repositoryPath),
1812
+ ];
1813
+ const checkoutRoots = [repoRoot, ...otherCheckouts];
1754
1814
  const dependencyRoots = collectDependencyVerificationRoots(repoRoot, getTestsRepoDir(), fullState?.relatedRepos);
1755
1815
  for (const dependencyRoot of dependencyRoots) {
1756
1816
  try {
@@ -1783,10 +1843,7 @@ export function registerSubmitReportTool(server) {
1783
1843
  // non-git one contributes nothing.
1784
1844
  const changedFiles = [
1785
1845
  ...(await listChangedFiles(repoRoot)),
1786
- ...(await listChangedFilesAcross([
1787
- getTestsRepoDir(),
1788
- ...Object.values(fullState?.relatedRepos ?? {}).map((section) => section.repositoryPath),
1789
- ])),
1846
+ ...(await listChangedFilesAcross(otherCheckouts)),
1790
1847
  ];
1791
1848
  // Primary verdicts only, by design: this checks UPDATEs, and
1792
1849
  // skyramp_actions downgrades every actionable related-repo verdict to
@@ -1831,17 +1888,41 @@ export function registerSubmitReportTool(server) {
1831
1888
  `Files with actual working-tree changes:\n${changedList || " (none)"}`);
1832
1889
  return errorResult;
1833
1890
  }
1834
- changedFilesAbs = await listChangedFilesAbs([
1835
- repoRoot,
1836
- getTestsRepoDir(),
1837
- ...Object.values(fullState?.relatedRepos ?? {}).map((section) => section.repositoryPath),
1838
- ]);
1891
+ changedFilesAbs = await listChangedFilesAbs(checkoutRoots);
1839
1892
  const gate = await retrofitGate(stateData, changedFilesAbs, repoRoot);
1840
1893
  retrofitViews = gate.views;
1841
1894
  if (gate.refusal) {
1842
1895
  errorResult = toolError(gate.refusal);
1843
1896
  return errorResult;
1844
1897
  }
1898
+ // A shared helper's change reaches every caller at once, and a caller the
1899
+ // run did not touch is never delivered — so nothing executed it and a
1900
+ // break in it reached the customer's continuous integration rather than
1901
+ // this report. Derived here, beside the retrofit gate, because both need
1902
+ // the same three things: the working tree's changed files, the test
1903
+ // inventory, and the repository root.
1904
+ // Derived from git, not from the reuse records, so a helper the
1905
+ // maintenance phase repaired with an UPDATE on the module is found the
1906
+ // same way as one the reuse pass rewrote. The refusal's remedy names
1907
+ // the repository section and state file `skyramp_execute_test` records
1908
+ // into.
1909
+ changedHelpers = await deriveChangedHelpers({
1910
+ roots: checkoutRoots,
1911
+ changedFilesAbs,
1912
+ state: stateData,
1913
+ repoRoot,
1914
+ repository,
1915
+ stateFile,
1916
+ });
1917
+ // An unexecuted caller refuses, because the remedy is one tool call the
1918
+ // agent can always make. A caller that RAN and FAILED does not: it is
1919
+ // disclosed with its result and the reviewer decides. Classifying it —
1920
+ // the helper's fault or the test's — is SKYR-4451's, and a cruder rule
1921
+ // stated here instead is the thing that ticket objected to.
1922
+ if (changedHelpers.refusal) {
1923
+ errorResult = toolError(changedHelpers.refusal);
1924
+ return errorResult;
1925
+ }
1845
1926
  }
1846
1927
  catch (err) {
1847
1928
  // Never block a valid report because verification itself failed (path not a
@@ -1920,8 +2001,18 @@ export function registerSubmitReportTool(server) {
1920
2001
  const consumedDeclines = [];
1921
2002
  /** Files whose stored declines a re-derivation actually measured on this call. */
1922
2003
  const evaluatedDeclineFiles = new Set();
2004
+ const reuseRowContext = {
2005
+ outcomes: stateData.reuseOutcomes,
2006
+ handOffs: stateData.reuseHandOffs,
2007
+ retrofits: retrofitViews,
2008
+ changedHelpers,
2009
+ removedModules: await removedModulesFrom(stateData.reuseOutcomes),
2010
+ blocking: reuseBlocking,
2011
+ consumed: consumedDeclines,
2012
+ evaluated: evaluatedDeclineFiles,
2013
+ };
1923
2014
  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 ?? {}));
2015
+ const row = await attachReuseOutcome(rest, reuseRowContext, declinesBySpec.get(path.basename(rest.fileName)) ?? [], undefined, generatedPathFor(rest.fileName, stateData.generatedTests ?? {}));
1925
2016
  return attachAssertionOutcome(row, stateData.assertionOutcomes ?? {}, assertionCheckouts);
1926
2017
  }));
1927
2018
  const maintenanceRows = testMaintenance
@@ -1952,7 +2043,7 @@ export function registerSubmitReportTool(server) {
1952
2043
  ? { editedFileName: path.basename(pomFile) }
1953
2044
  : {}),
1954
2045
  ...(assertions ? { assertions } : {}),
1955
- }, stateData.reuseOutcomes, stateData.reuseHandOffs, retrofitViews, reuseBlocking, declinesBySpec.get(path.basename(testFilePath)) ?? [], consumedDeclines, evaluatedDeclineFiles, testFilePath,
2046
+ }, reuseRowContext, declinesBySpec.get(path.basename(testFilePath)) ?? [], testFilePath,
1956
2047
  // Only where the run edited the file and left a test standing.
1957
2048
  // VERIFY and IGNORE read a repository-owned test and changed nothing,
1958
2049
  // 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), {
@@ -579,7 +587,7 @@ const registerTestPlanSchema = {
579
587
  blocker: z
580
588
  .string()
581
589
  .optional()
582
- .describe("What stopped this run from writing the test: a service that is not running, a paired branch that no longer exists, the one credential the run holds. An objection about a change or a defect with no test closes only with this. A reason the change is not worth testing is not a blocker."),
590
+ .describe("What stopped this run from writing the test: a service that is not running, a paired branch that no longer exists, the one credential the run holds. An objection about a change or a defect with no test, or about a value a page and API change refuses that no UI test names, closes only with this. A reason the change is not worth testing is not a blocker."),
583
591
  })
584
592
  .strict())
585
593
  .default([])
@@ -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
  }