@skyramp/mcp 0.3.8 → 0.4.0-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 (229) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -1
  2. package/build/commands/commandLibrary.js +3 -3
  3. package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
  4. package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
  5. package/build/commands/testThisEndpointCommand.js +35 -19
  6. package/build/index.js +9 -3
  7. package/build/playwright/blueprintDigest.d.ts +15 -0
  8. package/build/playwright/blueprintDigest.js +152 -0
  9. package/build/playwright/blueprintDigestStore.d.ts +31 -0
  10. package/build/playwright/blueprintDigestStore.js +117 -0
  11. package/build/playwright/registerPlaywrightTools.js +60 -12
  12. package/build/playwright/traceRecordingPrompt.js +8 -7
  13. package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
  14. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
  15. package/build/prompts/promptAssets.d.ts +20 -0
  16. package/build/prompts/promptAssets.js +55 -0
  17. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
  18. package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
  19. package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
  20. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
  21. package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
  22. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
  23. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
  24. package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
  25. package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
  26. package/build/prompts/test-recommendation/recommendationSections.js +67 -309
  27. package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
  28. package/build/prompts/test-recommendation/recommendationShared.js +49 -155
  29. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
  30. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
  31. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
  32. package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
  33. package/build/prompts/testbot/planDeclarations.d.ts +6 -0
  34. package/build/prompts/testbot/planDeclarations.js +9 -0
  35. package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
  36. package/build/prompts/testbot/testbot-prompts.js +256 -381
  37. package/build/recommendation/answers.d.ts +35 -0
  38. package/build/recommendation/answers.js +96 -0
  39. package/build/recommendation/registerPlan.d.ts +49 -0
  40. package/build/recommendation/registerPlan.js +117 -0
  41. package/build/recommendation/runVerifiers.d.ts +10 -0
  42. package/build/recommendation/runVerifiers.js +49 -0
  43. package/build/recommendation/subjectStep.d.ts +42 -0
  44. package/build/recommendation/subjectStep.js +86 -0
  45. package/build/recommendation/types.d.ts +163 -0
  46. package/build/recommendation/types.js +20 -0
  47. package/build/recommendation/verifierContracts.d.ts +382 -0
  48. package/build/recommendation/verifierContracts.js +263 -0
  49. package/build/recommendation/verifiers/changedFile.d.ts +2 -0
  50. package/build/recommendation/verifiers/changedFile.js +82 -0
  51. package/build/recommendation/verifiers/citedPath.d.ts +12 -0
  52. package/build/recommendation/verifiers/citedPath.js +35 -0
  53. package/build/recommendation/verifiers/coverage.d.ts +7 -0
  54. package/build/recommendation/verifiers/coverage.js +617 -0
  55. package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
  56. package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
  57. package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
  58. package/build/recommendation/verifiers/endpointGrounded.js +128 -0
  59. package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
  60. package/build/recommendation/verifiers/existingCoverage.js +51 -0
  61. package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
  62. package/build/recommendation/verifiers/expectedOutcome.js +105 -0
  63. package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
  64. package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
  65. package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
  66. package/build/recommendation/verifiers/reportedCategory.js +84 -0
  67. package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
  68. package/build/recommendation/verifiers/screenRoute.js +118 -0
  69. package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
  70. package/build/recommendation/verifiers/statedDifference.js +140 -0
  71. package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
  72. package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
  73. package/build/resources/analysisResources.js +1 -114
  74. package/build/resources/testbotResource.js +23 -13
  75. package/build/services/ModularizationService.js +2 -1
  76. package/build/services/TestDiscoveryService.d.ts +3 -72
  77. package/build/services/TestDiscoveryService.js +10 -303
  78. package/build/services/containerEnv.d.ts +1 -1
  79. package/build/services/containerEnv.js +12 -0
  80. package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
  81. package/build/skills/fixTestImportErrorsSkill.js +20 -0
  82. package/build/toolNames.d.ts +1 -0
  83. package/build/toolNames.js +1 -0
  84. package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
  85. package/build/tools/code-refactor/modularizationTool.js +2 -1
  86. package/build/tools/executeSkyrampTestTool.d.ts +80 -0
  87. package/build/tools/executeSkyrampTestTool.js +246 -19
  88. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
  89. package/build/tools/generate-tests/generateContractRestTool.js +3 -3
  90. package/build/tools/generate-tests/planGuard.d.ts +2 -2
  91. package/build/tools/generate-tests/planGuard.js +78 -18
  92. package/build/tools/one-click/oneClickTool.d.ts +0 -1
  93. package/build/tools/one-click/oneClickTool.js +0 -5
  94. package/build/tools/submitReportTool.d.ts +48 -42
  95. package/build/tools/submitReportTool.js +576 -193
  96. package/build/tools/test-management/actionsTool.js +72 -4
  97. package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
  98. package/build/tools/test-management/analyzeChangesTool.js +212 -1219
  99. package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
  100. package/build/tools/test-management/index.d.ts +1 -0
  101. package/build/tools/test-management/index.js +1 -0
  102. package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
  103. package/build/tools/test-management/registerTestPlanTool.js +609 -542
  104. package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
  105. package/build/tools/test-management/resolveScreenTool.js +289 -0
  106. package/build/types/BlueprintDigest.d.ts +34 -0
  107. package/build/types/BlueprintDigest.js +1 -0
  108. package/build/types/RepositoryAnalysis.d.ts +20 -1559
  109. package/build/types/RepositoryAnalysis.js +2 -58
  110. package/build/types/StepMethod.d.ts +40 -0
  111. package/build/types/StepMethod.js +77 -0
  112. package/build/types/TestAnalysis.d.ts +12 -0
  113. package/build/types/TestExecution.d.ts +4 -0
  114. package/build/types/TestRecommendation.d.ts +24 -24
  115. package/build/types/TestRecommendation.js +91 -89
  116. package/build/types/TestbotPromptOptions.d.ts +0 -4
  117. package/build/types/TestbotReport.d.ts +64 -2
  118. package/build/utils/AnalysisStateManager.d.ts +79 -113
  119. package/build/utils/AnalysisStateManager.js +147 -57
  120. package/build/utils/assertion-verify/api-shared-lints.js +1 -1
  121. package/build/utils/assertion-verify/metrics.js +85 -36
  122. package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
  123. package/build/utils/assertion-verify/ui-lints.js +32 -0
  124. package/build/utils/branchDiff.d.ts +63 -31
  125. package/build/utils/branchDiff.js +242 -94
  126. package/build/utils/containedPath.d.ts +18 -0
  127. package/build/utils/containedPath.js +73 -0
  128. package/build/utils/dartRouteExtractor.d.ts +18 -34
  129. package/build/utils/dartRouteExtractor.js +101 -173
  130. package/build/utils/featureFlags.d.ts +12 -0
  131. package/build/utils/featureFlags.js +14 -0
  132. package/build/utils/frontendSelectors.d.ts +48 -27
  133. package/build/utils/frontendSelectors.js +241 -80
  134. package/build/utils/pathMatching.d.ts +2 -4
  135. package/build/utils/pathMatching.js +2 -4
  136. package/build/utils/planMatchKeys.d.ts +38 -47
  137. package/build/utils/planMatchKeys.js +143 -81
  138. package/build/utils/rebaselineSnapshots.d.ts +24 -0
  139. package/build/utils/rebaselineSnapshots.js +65 -0
  140. package/build/utils/removedUiElements.d.ts +22 -0
  141. package/build/utils/removedUiElements.js +106 -0
  142. package/build/utils/reportVerification.d.ts +2 -6
  143. package/build/utils/reportVerification.js +61 -2
  144. package/build/utils/screenRoutes.d.ts +66 -0
  145. package/build/utils/screenRoutes.js +727 -0
  146. package/build/utils/sourceRouteExtractor.js +320 -112
  147. package/build/utils/testFileClassification.d.ts +11 -2
  148. package/build/utils/testFileClassification.js +44 -2
  149. package/build/utils/testFixtures.d.ts +5 -0
  150. package/build/utils/testFixtures.js +13 -0
  151. package/build/utils/utils.d.ts +0 -1
  152. package/build/utils/utils.js +0 -11
  153. package/build/utils/versions.d.ts +3 -3
  154. package/build/utils/versions.js +1 -1
  155. package/build/workspace/workspace.d.ts +12 -12
  156. package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
  157. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
  158. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
  159. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
  160. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
  161. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
  162. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  163. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  167. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  168. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
  170. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
  171. package/node_modules/playwright/package.json +1 -1
  172. package/package.json +4 -3
  173. package/plugin/.claude-plugin/plugin.json +8 -0
  174. package/plugin/plugin.json +6 -0
  175. package/plugin/prompts/declaring-a-plan.md +20 -0
  176. package/plugin/prompts/generate-tests/context-fetching.md +4 -0
  177. package/plugin/prompts/generate-tests/execution-plan.md +63 -0
  178. package/plugin/prompts/generate-tests/generation.md +108 -0
  179. package/plugin/prompts/generate-tests/path-parameters.md +1 -0
  180. package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
  181. package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
  182. package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
  183. package/plugin/prompts/plan-tests.md +42 -0
  184. package/plugin/prompts/testbot-task1.md +82 -0
  185. package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
  186. package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
  187. package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
  188. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
  189. package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
  190. package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
  191. package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
  192. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
  193. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
  194. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
  195. package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
  196. package/build/recommendation/budgeters/shared.d.ts +0 -32
  197. package/build/recommendation/budgeters/shared.js +0 -246
  198. package/build/recommendation/discriminators.d.ts +0 -37
  199. package/build/recommendation/discriminators.js +0 -379
  200. package/build/recommendation/diversity.d.ts +0 -47
  201. package/build/recommendation/diversity.js +0 -101
  202. package/build/recommendation/planRanker.d.ts +0 -65
  203. package/build/recommendation/planRanker.js +0 -83
  204. package/build/recommendation/testFixtures.d.ts +0 -25
  205. package/build/recommendation/testFixtures.js +0 -45
  206. package/build/types/FrontendIntegration.d.ts +0 -28
  207. package/build/types/FrontendIntegration.js +0 -22
  208. package/build/types/Recommendation.d.ts +0 -146
  209. package/build/types/Recommendation.js +0 -74
  210. package/build/utils/changedRoutes.d.ts +0 -29
  211. package/build/utils/changedRoutes.js +0 -87
  212. package/build/utils/frontendIntegration.d.ts +0 -9
  213. package/build/utils/frontendIntegration.js +0 -243
  214. package/build/utils/importerHop.d.ts +0 -135
  215. package/build/utils/importerHop.js +0 -489
  216. package/build/utils/pathAffinityClassification.d.ts +0 -49
  217. package/build/utils/pathAffinityClassification.js +0 -180
  218. package/build/utils/pythonMountPrefixes.d.ts +0 -25
  219. package/build/utils/pythonMountPrefixes.js +0 -347
  220. package/build/utils/repoScanner.d.ts +0 -34
  221. package/build/utils/repoScanner.js +0 -300
  222. package/build/utils/routeParsers.d.ts +0 -95
  223. package/build/utils/routeParsers.js +0 -951
  224. package/build/utils/scenarioDrafting.d.ts +0 -92
  225. package/build/utils/scenarioDrafting.js +0 -951
  226. package/build/utils/subjectEndpoints.d.ts +0 -19
  227. package/build/utils/subjectEndpoints.js +0 -98
  228. package/build/utils/uiPageEnumerator.d.ts +0 -172
  229. package/build/utils/uiPageEnumerator.js +0 -474
@@ -1,246 +0,0 @@
1
- import { TestType } from "../../types/TestTypes.js";
2
- import { inferScenarioType, prioritizeAttackSurfaceBundles } from "../diversity.js";
3
- import { externalDedupKeys, scenarioCoverageKeys, isAttackSurfaceSecurityBoundary, } from "../../prompts/test-recommendation/recommendationShared.js";
4
- import { isFlawTargetingCategory } from "../../types/TestRecommendation.js";
5
- import { logger } from "../../utils/logger.js";
6
- /**
7
- * What each PR class GUARANTEES in GENERATE (SKYR-4275). These are floors, not
8
- * quotas: the slots above them are open to either kind, decided by `pick`.
9
- * An open slot goes to whichever candidate `pick` chooses on rank, so a backend
10
- * candidate takes one when it outranks the UI candidates, not merely because it
11
- * exists. Measured against main on random mixed pools at `maxGenerate` 5 with no
12
- * caught bugs: 35% get fewer backend tests than main did, average 3.53 to 3.24.
13
- *
14
- * Backend-only floors nothing on purpose: of 86 backend-only diffs in the eval
15
- * corpus, ZERO generated a UI test, so a floor there would guarantee what
16
- * already happens every time.
17
- *
18
- * At one slot there is no room for a floor plus an open slot, so each class
19
- * keeps that slot for its own kind.
20
- *
21
- * The floors always sum to at most `maxGenerate`, which is what lets
22
- * `applyFloors` satisfy both without one starving the other.
23
- */
24
- export function generateFloors(ctx) {
25
- // Number.isFinite, not Math.max: `Math.max(0, NaN)` is NaN, which compares
26
- // false against 0 and would fall through to the class branches below.
27
- const max = Number.isFinite(ctx.maxGenerate) ? Math.max(0, Math.floor(ctx.maxGenerate)) : 0;
28
- if (max === 0)
29
- return { ui: 0, backend: 0 };
30
- if (ctx.isUIOnlyPR)
31
- return { ui: max === 1 ? 1 : max - 1, backend: 0 };
32
- if (ctx.hasFrontendChanges)
33
- return max === 1 ? { ui: 1, backend: 0 } : { ui: 1, backend: 1 };
34
- return { ui: 0, backend: 0 };
35
- }
36
- /**
37
- * Raise a chosen GENERATE set to meet the floors, by swapping rather than
38
- * re-picking. `pick` runs ONCE over the whole pool, so its protected-first
39
- * ordering, its one-slot-per-test-type coverage and attack-surface bundle
40
- * adjacency are all settled before this runs. Picking each floor from its own
41
- * slice instead would compute those guarantees per slice and collapse them —
42
- * measured at 25% of mixed-PR inputs losing a test type, 8.3% losing contract
43
- * coverage outright.
44
- *
45
- * A floor is raised only as far as the pool allows. Satisfying one floor cannot
46
- * starve the other, because `generateFloors` never returns two that sum above
47
- * the budget — `generateFloors.test.ts` pins that invariant.
48
- */
49
- function applyFloors(generate, slotOrdered, floors, isUI) {
50
- const isBackend = (item) => !isUI(item);
51
- const result = [...generate];
52
- for (const [want, wanted] of [
53
- [floors.ui, isUI],
54
- [floors.backend, isBackend],
55
- ]) {
56
- const spare = slotOrdered.filter((item) => wanted(item) && !result.includes(item));
57
- let shortfall = Math.min(want, result.filter(wanted).length + spare.length) - result.filter(wanted).length;
58
- // Swap from the END: `pick` returns its slots in preference order, so the
59
- // last one it chose is the one this PR class needs least.
60
- for (let i = result.length - 1; i >= 0 && shortfall > 0; i--) {
61
- if (wanted(result[i]))
62
- continue;
63
- result[i] = spare.shift();
64
- shortfall--;
65
- }
66
- }
67
- return result;
68
- }
69
- /**
70
- * Split candidates by whether an external test already covers them. Protected
71
- * `bug_caught` / `requirement_conflict` / attack-surface scenarios always count
72
- * as uncovered — they require semantic flaw coverage the external test may not
73
- * provide.
74
- *
75
- * A candidate is covered only when the set holds EVERY one of its keys. One
76
- * match is not coverage: a scenario exercising two changed endpoints would lose
77
- * the endpoint the external test does not reach (SKYR-4214). An empty key list
78
- * carries no information, so it never removes anything.
79
- *
80
- * The covered group is held back, not discarded. `runBudget` moves it into
81
- * ADDITIONAL when GENERATE's backend half would otherwise be empty (SKYR-4024),
82
- * and records it as dropped, with a reason, when it does not.
83
- */
84
- function partitionByExternalCoverage(ranked, externalCoverage) {
85
- if (externalCoverage.size === 0)
86
- return { uncovered: ranked, covered: [] };
87
- const uncovered = [];
88
- const covered = [];
89
- for (const item of ranked) {
90
- const keys = externalDedupKeys(item.scenario);
91
- if (keys.length === 0 || !keys.every((key) => externalCoverage.has(key))) {
92
- uncovered.push(item);
93
- continue;
94
- }
95
- if (isFlawTargetingCategory(item.scenario.category) || isAttackSurfaceSecurityBoundary(item.scenario)) {
96
- logger.info(`External dedup: preserving "${item.scenario.scenarioName}" (${keys.join(", ")}) — protected bug/requirement-conflict/attack-surface scenario requires semantic flaw coverage`);
97
- uncovered.push(item);
98
- continue;
99
- }
100
- logger.info(`External dedup: "${item.scenario.scenarioName}" (${keys.join(", ")}) covered by an external test — eligible for the reserve`);
101
- covered.push({ item, keys });
102
- }
103
- return { uncovered, covered };
104
- }
105
- /**
106
- * Shared budgeting pipeline. All Budgeters run the same external-dedup,
107
- * attack-surface prioritization, and ADDITIONAL set-difference; they differ
108
- * ONLY in how they pick the GENERATE items from the slot-ordered list (`pick`).
109
- *
110
- * With `pick = roundRobinByType` this reproduces the pre-refactor selection in
111
- * diffExecutionPlan.ts exactly.
112
- */
113
- export function runBudget(ranked, rawCtx, pick) {
114
- // SKYR-4292: maxTotal is the budget; maxGenerate is how much of it to generate.
115
- // Every producer clamps upstream (getTestbotPrompt, computeScoredCandidates,
116
- // buildScopeAssessmentSection), but a state file is the tool boundary and can
117
- // carry maxGenerate > maxTotal — the UI-only branch then reserved maxGenerate
118
- // slots and planned more items than the total. Clamp once here so every
119
- // budgeter agrees with the prompt. It must stay ABOVE generateFloors(ctx):
120
- // the floors are computed from the clamped budget, and moving the clamp
121
- // below them would let the floors sum past maxTotal again (review on #835).
122
- const ctx = { ...rawCtx, maxGenerate: Math.min(rawCtx.maxGenerate, rawCtx.maxTotal) };
123
- const dropped = [];
124
- const floors = generateFloors(ctx);
125
- const { uncovered, covered } = partitionByExternalCoverage(ranked, ctx.externalCoverage);
126
- const slotOrdered = prioritizeAttackSurfaceBundles(uncovered);
127
- const isUICandidate = (item) => inferScenarioType(item.scenario) === TestType.UI;
128
- // ONE pick over the whole pool — the shape the backend pick always had — so
129
- // `pick` decides protected-first order, per-type coverage and bundle adjacency
130
- // exactly once. `applyFloors` then corrects only the UI/backend mix.
131
- const budget = Number.isFinite(ctx.maxGenerate) ? Math.max(0, Math.floor(ctx.maxGenerate)) : 0;
132
- const picked = pick(slotOrdered, Math.min(budget, slotOrdered.length));
133
- const generate = applyFloors(picked, slotOrdered, floors, isUICandidate);
134
- // SKYR-4024. A covered candidate is one an existing test already reaches, so
135
- // generating a second test for it is wrong — maintenance updates the existing
136
- // one. But dropping it outright removed it from the RECOMMENDATIONS too, and an
137
- // empty GENERATE is authoritative downstream. The reserve surfaces it as
138
- // ADDITIONAL instead. The emptiness test reads the backend half: on a mixed PR
139
- // one UI candidate fills the reserved slot while every covered backend
140
- // candidate would otherwise vanish.
141
- //
142
- // Three conditions keep the reserve shut where recommending nothing is right.
143
- // A UI-only PR is left alone. A zero budget is an explicit abstention, so it
144
- // must not be talked out of recommending nothing. And the PR must not touch a
145
- // test file of its own: coverage is read from the working tree, and a test
146
- // file without the Skyramp marker counts as external, so a PR that adds its
147
- // own tests would have that coverage used as grounds to recommend duplicates
148
- // of it.
149
- //
150
- // One pick over the whole pool leaves no "backend half" to count, so the
151
- // emptiness test reproduces what that half used to mean per class. On a mixed
152
- // PR the UI slot must not read as backend coverage, or a covered backend
153
- // candidate vanishes behind it. On a backend-only PR the pick always drew from
154
- // the whole pool, so a non-empty GENERATE closed the reserve even when every
155
- // item was UI — keep that, or an all-UI pool opens a reserve it never opened.
156
- const backendChosen = generate.filter((item) => !isUICandidate(item));
157
- const backendHalfEmpty = ctx.hasFrontendChanges
158
- ? backendChosen.length === 0
159
- : generate.length === 0;
160
- // One per-class behaviour DID change here, deliberately. On a mixed PR at a
161
- // budget of 1 the old gate also required `backendGenerateCount > 0`, which is
162
- // 0 for that class, so a covered backend candidate was dropped outright. The
163
- // reserve exists because dropping removes a candidate from the
164
- // RECOMMENDATIONS too (SKYR-4024), and a budget of 1 is the case where that
165
- // costs most — there is no second slot to find it later. It is now
166
- // recommended instead of dropped. `reserveOpensForMixedPRAtBudgetOne` in
167
- // shared.test.ts pins it, so a future change has to mean it.
168
- const spendReserve = backendHalfEmpty &&
169
- !ctx.isUIOnlyPR &&
170
- budget > 0 &&
171
- covered.length > 0 &&
172
- !ctx.diffChangesTestFiles;
173
- if (spendReserve) {
174
- logger.info(`External dedup: GENERATE empty after dedup — recommending ${covered.length} held-back candidate(s) as ADDITIONAL rather than approving an empty plan`);
175
- }
176
- else if (covered.length > 0) {
177
- // SKYR-4214. The reserve stayed shut, so these really are dropped — say so,
178
- // with the reason, instead of leaving the agent an unexplained short list.
179
- const reason = !backendHalfEmpty
180
- ? `the backend half of GENERATE is filled (${backendChosen.length || generate.length})`
181
- : ctx.isUIOnlyPR
182
- ? "a UI-only PR keeps its slots for UI"
183
- : budget === 0
184
- ? "the budget is zero"
185
- : "the PR changes test files of its own";
186
- logger.info(`External dedup: dropping ${covered.length} covered candidate(s) — reserve shut because ${reason}`);
187
- for (const { item, keys } of covered) {
188
- dropped.push({
189
- candidateId: item.candidateId,
190
- reason: `covered by an external test (${keys.join(", ")})`,
191
- });
192
- }
193
- }
194
- // ADDITIONAL = everything not chosen for GENERATE, in rank order, minus any
195
- // whose subject endpoints are ALL already covered by GENERATE, then capped at
196
- // the remaining budget. All, not any: an item that also covers an endpoint no
197
- // GENERATE item reaches is not a duplicate. An empty key list never removes
198
- // an item.
199
- //
200
- // The duplicate filter runs BEFORE the cap. In the other order a duplicate
201
- // took a slot and then vacated it, and nothing moved up: eval run
202
- // 32543498147, fixture cc15-org-reviewer-role, filled 19 of a maxTotal of 20
203
- // while 16 candidates carried the reason "beyond the maxTotal budget of 20".
204
- // A candidate that is both a duplicate and past the cap now reports the
205
- // duplicate — the stronger reason, and the only one that does not depend on
206
- // where the cap happened to fall.
207
- const additionalSource = spendReserve
208
- ? [...slotOrdered, ...prioritizeAttackSurfaceBundles(covered.map((c) => c.item))]
209
- : slotOrdered;
210
- const chosen = new Set(generate);
211
- const notChosen = additionalSource.filter((it) => !chosen.has(it));
212
- const remainingBudget = Math.max(0, ctx.maxTotal - generate.length);
213
- const generatedCoverage = new Set(generate.flatMap((item) => scenarioCoverageKeys(item.scenario)));
214
- const notDuplicate = notChosen.filter((item) => {
215
- const keys = scenarioCoverageKeys(item.scenario);
216
- if (keys.length === 0 || !keys.every((key) => generatedCoverage.has(key)))
217
- return true;
218
- // Same exemption the EXTERNAL dedup above already makes, for the same
219
- // reason: a flaw-targeting scenario (a caught bug, or a requirement the code
220
- // contradicts) or an attack-surface boundary asserts a specific defect, and
221
- // sharing an endpoint with a GENERATE item is not evidence that the defect is
222
- // covered. Without it a PR with two defects in one handler had its second one
223
- // deleted here — the first took a GENERATE slot, and the key is method +
224
- // resource + test type + interaction + status, which two tests on one
225
- // endpoint share.
226
- if (isFlawTargetingCategory(item.scenario.category) || isAttackSurfaceSecurityBoundary(item.scenario)) {
227
- logger.info(`GENERATE dedup: preserving "${item.scenario.scenarioName}" (${keys.join(", ")}) — protected bug/requirement-conflict/attack-surface scenario requires semantic flaw coverage`);
228
- return true;
229
- }
230
- dropped.push({
231
- candidateId: item.candidateId,
232
- reason: `covered by GENERATE (${keys.join(", ")})`,
233
- });
234
- return false;
235
- });
236
- const additional = notDuplicate.slice(0, remainingBudget);
237
- for (const item of notDuplicate.slice(remainingBudget)) {
238
- dropped.push({
239
- candidateId: item.candidateId,
240
- reason: `beyond the maxTotal budget of ${ctx.maxTotal}`,
241
- });
242
- }
243
- // Budgeting itself produces no demotions; the register-plan selection stage
244
- // (planRanker.selectPlan) fills this channel from discriminator verification.
245
- return { generate, additional, reservedUISlots: floors.ui, demotions: [], dropped };
246
- }
@@ -1,37 +0,0 @@
1
- import { DraftedScenario } from "../types/RepositoryAnalysis.js";
2
- import { DiscriminatorKind } from "../types/Recommendation.js";
3
- /**
4
- * A discriminator claim the agent (or server) attaches to a candidate: the
5
- * structural bug-shape (`kind`) plus the verbatim diff snippet the test probes.
6
- */
7
- export interface DiscriminatorClaim {
8
- kind: DiscriminatorKind;
9
- changedCodeAnchor: string;
10
- }
11
- /** Result of verifying a claim against a scenario's steps and the PR diff. */
12
- export interface DiscriminatorValidation {
13
- verified: boolean;
14
- /** One-sentence, human-readable reason present ONLY when `verified` is false;
15
- * it is returned to the LLM to help it fix the claim. */
16
- reason?: string;
17
- }
18
- /**
19
- * Verify a declared discriminator against the candidate's `steps[]` and the PR
20
- * diff. Mirrors the classification precedent (`recommendationShared.ts`): the
21
- * claim is checked STRUCTURALLY, never by fuzzy keyword scans of prose. A failed
22
- * check returns `verified: false` with a one-sentence reason (the claim is
23
- * demoted, never used to reject the candidate). NEVER throws — malformed or
24
- * partial input is treated as unverified.
25
- *
26
- * Two gates, both required to verify:
27
- * 1. `changedCodeAnchor` must be a non-trivial string (>= 8 chars after trim)
28
- * occurring verbatim in `diffText` — grounds the claim in the real change.
29
- * 2. The `kind`-specific structural predicate must hold over `steps[]`.
30
- *
31
- * `reposWithoutDiff` names repos in the run whose section carries no branch
32
- * diff (e.g. a related repo that fell back to its default branch, SKYR-4203).
33
- * An anchor those repos' changes could contain cannot be disproven, so an
34
- * unmatched anchor then fails with a "could not be checked" reason instead of
35
- * the "not grounded" one — still unverified (no boost), but distinguishable.
36
- */
37
- export declare function validateDiscriminator(scenario: DraftedScenario, declared: DiscriminatorClaim, diffText: string, reposWithoutDiff?: string[]): DiscriminatorValidation;
@@ -1,379 +0,0 @@
1
- import { DiscriminatorKind } from "../types/Recommendation.js";
2
- const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
3
- const MIN_ANCHOR_LENGTH = 8;
4
- const MIN_NEAR_MISS_LENGTH = 3;
5
- /**
6
- * Verify a declared discriminator against the candidate's `steps[]` and the PR
7
- * diff. Mirrors the classification precedent (`recommendationShared.ts`): the
8
- * claim is checked STRUCTURALLY, never by fuzzy keyword scans of prose. A failed
9
- * check returns `verified: false` with a one-sentence reason (the claim is
10
- * demoted, never used to reject the candidate). NEVER throws — malformed or
11
- * partial input is treated as unverified.
12
- *
13
- * Two gates, both required to verify:
14
- * 1. `changedCodeAnchor` must be a non-trivial string (>= 8 chars after trim)
15
- * occurring verbatim in `diffText` — grounds the claim in the real change.
16
- * 2. The `kind`-specific structural predicate must hold over `steps[]`.
17
- *
18
- * `reposWithoutDiff` names repos in the run whose section carries no branch
19
- * diff (e.g. a related repo that fell back to its default branch, SKYR-4203).
20
- * An anchor those repos' changes could contain cannot be disproven, so an
21
- * unmatched anchor then fails with a "could not be checked" reason instead of
22
- * the "not grounded" one — still unverified (no boost), but distinguishable.
23
- */
24
- export function validateDiscriminator(scenario, declared, diffText, reposWithoutDiff = []) {
25
- try {
26
- const anchorResult = verifyAnchor(declared?.changedCodeAnchor, diffText, reposWithoutDiff);
27
- if (!anchorResult.verified)
28
- return anchorResult;
29
- const steps = Array.isArray(scenario?.steps) ? scenario.steps : [];
30
- switch (declared?.kind) {
31
- case DiscriminatorKind.BOUNDARY_EQUALITY:
32
- return verifyBoundaryEquality(steps);
33
- case DiscriminatorKind.MULTI_RECORD_AGGREGATE:
34
- return verifyMultiRecordAggregate(steps);
35
- case DiscriminatorKind.NEGATIVE_MATCH:
36
- return verifyNegativeMatch(steps);
37
- case DiscriminatorKind.STATE_TRANSITION:
38
- return verifyStateTransition(steps);
39
- default:
40
- return {
41
- verified: false,
42
- reason: `Unknown discriminator kind "${String(declared?.kind)}"; expected one of boundary_equality, multi_record_aggregate, negative_match, state_transition.`,
43
- };
44
- }
45
- }
46
- catch {
47
- // Defensive: any malformed input is treated as unverified, never a throw.
48
- return { verified: false, reason: "Discriminator claim could not be verified against the scenario steps." };
49
- }
50
- }
51
- // ── Anchor grounding ──
52
- function verifyAnchor(rawAnchor, diffText, reposWithoutDiff = []) {
53
- const anchor = typeof rawAnchor === "string" ? rawAnchor.trim() : "";
54
- if (anchor.length < MIN_ANCHOR_LENGTH) {
55
- return {
56
- verified: false,
57
- reason: `changedCodeAnchor "${String(rawAnchor ?? "")}" is too short (needs >= ${MIN_ANCHOR_LENGTH} non-whitespace chars) to ground the claim in the diff.`,
58
- };
59
- }
60
- const diff = typeof diffText === "string" ? diffText : "";
61
- // A statement wrapped across diff lines carries +/- prefixes and indentation
62
- // between its fragments, so a multi-line anchor can never match the raw diff
63
- // text — compare whitespace-collapsed forms with diff markers stripped, or
64
- // legitimate multi-line anchors are systematically false-demoted.
65
- if (!diff.includes(anchor) && !normalizeForAnchorMatch(diff).includes(normalizeForAnchorMatch(anchor))) {
66
- if (reposWithoutDiff.length > 0) {
67
- return {
68
- verified: false,
69
- reason: `changedCodeAnchor "${anchor}" was not found in the available PR diffs, and no branch diff exists for ` +
70
- `${reposWithoutDiff.join(", ")} (likely checked out on the default branch), so the claim could not be checked against those changes.`,
71
- };
72
- }
73
- return {
74
- verified: false,
75
- reason: `changedCodeAnchor "${anchor}" does not occur verbatim in the PR diff, so the discriminator claim is not grounded in the actual change.`,
76
- };
77
- }
78
- return { verified: true };
79
- }
80
- /** Strip per-line diff +/- markers and collapse all whitespace runs to single
81
- * spaces so anchors spanning wrapped/indented lines still match. */
82
- function normalizeForAnchorMatch(text) {
83
- return text
84
- .split("\n")
85
- .map((line) => line.replace(/^[+\- ]/, ""))
86
- .join(" ")
87
- .replace(/\s+/g, " ")
88
- .trim();
89
- }
90
- // ── Structural predicates ──
91
- /**
92
- * boundary_equality: some step carries two DISTINCT fields holding EQUAL values
93
- * in requestBody/queryParams (probing an on-boundary case, e.g.
94
- * install_date == termination_date) AND that step is either an accept-side probe
95
- * (2xx) or a mirrored reject-side probe (4xx with interactionType "error").
96
- */
97
- function verifyBoundaryEquality(steps) {
98
- for (const step of steps) {
99
- const leaves = [];
100
- collectLeaves(step?.requestBody, leaves);
101
- collectLeaves(step?.queryParams, leaves);
102
- if (hasDistinctFieldEqualValue(leaves) && satisfiesBoundaryStatus(step)) {
103
- return { verified: true };
104
- }
105
- }
106
- return {
107
- verified: false,
108
- reason: "boundary_equality unverified: no step sets two distinct fields to an equal value on an accept-side (2xx) or mirrored reject-side (4xx error) request — the on-boundary case is not exercised.",
109
- };
110
- }
111
- /**
112
- * multi_record_aggregate: >= 2 POST steps on the same (param-normalized) resource
113
- * path occurring before an asserting step, OR any step asserting a numeric value
114
- * >= 2 in expectedResponseFields/bodyMustInclude (e.g. account_count == 2).
115
- */
116
- function verifyMultiRecordAggregate(steps) {
117
- const ordered = orderedSteps(steps);
118
- // (b) any step asserting a numeric value >= 2.
119
- for (const step of ordered) {
120
- const assertions = [...toStringArray(step?.expectedResponseFields), ...toStringArray(step?.bodyMustInclude)];
121
- if (assertions.some((s) => maxNumberIn(s) >= 2))
122
- return { verified: true };
123
- }
124
- // (a) >= 2 same-path POSTs before an asserting step.
125
- for (let i = 0; i < ordered.length; i++) {
126
- if (!isAssertStep(ordered[i]))
127
- continue;
128
- const counts = new Map();
129
- for (let j = 0; j < i; j++) {
130
- const prior = ordered[j];
131
- if ((prior?.method ?? "").toUpperCase() !== "POST")
132
- continue;
133
- const key = normalizePath(prior?.path ?? "");
134
- const next = (counts.get(key) ?? 0) + 1;
135
- counts.set(key, next);
136
- if (next >= 2)
137
- return { verified: true };
138
- }
139
- }
140
- return {
141
- verified: false,
142
- reason: "multi_record_aggregate unverified: found neither >= 2 same-resource POST steps before an assertion nor an assertion of a numeric count >= 2 — the aggregation over multiple records is not exercised.",
143
- };
144
- }
145
- /**
146
- * negative_match: a step asserting empty/zero/404 whose query/body carries a
147
- * value that is a strict substring or superstring of a value seeded in an
148
- * earlier step (a near-miss that must NOT match).
149
- */
150
- function verifyNegativeMatch(steps) {
151
- const ordered = orderedSteps(steps);
152
- for (let i = 0; i < ordered.length; i++) {
153
- const step = ordered[i];
154
- if (!isNegativeAssertStep(step))
155
- continue;
156
- const nearMissValues = [];
157
- collectStringValues(step?.requestBody, nearMissValues);
158
- collectStringValues(step?.queryParams, nearMissValues);
159
- const seeded = [];
160
- for (let j = 0; j < i; j++) {
161
- collectStringValues(ordered[j]?.requestBody, seeded);
162
- collectStringValues(ordered[j]?.queryParams, seeded);
163
- }
164
- if (nearMissValues.some((q) => seeded.some((s) => isStrictNearMiss(q, s)))) {
165
- return { verified: true };
166
- }
167
- }
168
- return {
169
- verified: false,
170
- reason: "negative_match unverified: no empty/zero/404 assertion queries a strict near-miss (substring or superstring) of a value seeded in an earlier step — the false-positive boundary is not probed.",
171
- };
172
- }
173
- /**
174
- * state_transition: >= 2 mutating steps (POST/PUT/PATCH/DELETE) on the same
175
- * resource — same param-normalized path, or linked via chainsFrom — with a
176
- * DIFFERENT expectedStatusCode or interactionType.
177
- */
178
- function verifyStateTransition(steps) {
179
- const mutating = steps.filter((s) => MUTATING_METHODS.has((s?.method ?? "").toUpperCase()));
180
- // Same normalized path.
181
- const groups = new Map();
182
- for (const step of mutating) {
183
- const key = normalizePath(step?.path ?? "");
184
- if (!groups.has(key))
185
- groups.set(key, []);
186
- groups.get(key).push(step);
187
- }
188
- for (const group of groups.values()) {
189
- if (group.length >= 2 && groupHasDivergentOutcome(group))
190
- return { verified: true };
191
- }
192
- // Linked via chainsFrom (a mutating step chaining off another mutating step).
193
- const byOrder = new Map();
194
- for (const step of steps) {
195
- if (typeof step?.order === "number")
196
- byOrder.set(step.order, step);
197
- }
198
- for (const step of mutating) {
199
- for (const sourceStep of toChainSourceSteps(step?.chainsFrom)) {
200
- const src = byOrder.get(sourceStep);
201
- if (src &&
202
- MUTATING_METHODS.has((src.method ?? "").toUpperCase()) &&
203
- (src.expectedStatusCode !== step.expectedStatusCode || src.interactionType !== step.interactionType)) {
204
- return { verified: true };
205
- }
206
- }
207
- }
208
- return {
209
- verified: false,
210
- reason: "state_transition unverified: no two mutating steps on the same resource diverge in expectedStatusCode or interactionType — the state machine's differing outcomes are not exercised.",
211
- };
212
- }
213
- /** Recursively collect leaf (field, value) pairs for string/number values. */
214
- function collectLeaves(value, out) {
215
- if (value === null || value === undefined || typeof value !== "object")
216
- return;
217
- if (Array.isArray(value)) {
218
- for (const item of value)
219
- collectLeaves(item, out);
220
- return;
221
- }
222
- for (const [key, v] of Object.entries(value)) {
223
- if (v === null || v === undefined)
224
- continue;
225
- if (typeof v === "object") {
226
- collectLeaves(v, out);
227
- }
228
- else if (typeof v === "string" || typeof v === "number") {
229
- out.push({ field: key, value: v });
230
- }
231
- }
232
- }
233
- /** Recursively collect leaf string values (used for near-miss substring scans). */
234
- function collectStringValues(value, out) {
235
- if (value === null || value === undefined)
236
- return;
237
- if (typeof value === "string") {
238
- if (value.length > 0)
239
- out.push(value);
240
- return;
241
- }
242
- if (typeof value !== "object")
243
- return;
244
- if (Array.isArray(value)) {
245
- for (const item of value)
246
- collectStringValues(item, out);
247
- return;
248
- }
249
- for (const v of Object.values(value))
250
- collectStringValues(v, out);
251
- }
252
- /** True if two leaves have different field names but an equal (non-trivial) value. */
253
- function hasDistinctFieldEqualValue(leaves) {
254
- for (let a = 0; a < leaves.length; a++) {
255
- for (let b = a + 1; b < leaves.length; b++) {
256
- const la = leaves[a];
257
- const lb = leaves[b];
258
- if (la.field === lb.field)
259
- continue;
260
- if (la.value !== lb.value)
261
- continue;
262
- if (typeof la.value === "string" && la.value.length === 0)
263
- continue;
264
- return true;
265
- }
266
- }
267
- return false;
268
- }
269
- function satisfiesBoundaryStatus(step) {
270
- const status = typeof step?.expectedStatusCode === "number" ? step.expectedStatusCode : 0;
271
- const accept = status >= 200 && status < 300;
272
- const reject = status >= 400 && status < 500 && step?.interactionType === "error";
273
- return accept || reject;
274
- }
275
- function isAssertStep(step) {
276
- return (toStringArray(step?.expectedResponseFields).length > 0 ||
277
- toStringArray(step?.bodyMustInclude).length > 0 ||
278
- (step?.responseBody !== null && typeof step?.responseBody === "object" && Object.keys(step.responseBody).length > 0));
279
- }
280
- function isNegativeAssertStep(step) {
281
- if (step?.expectedStatusCode === 404)
282
- return true;
283
- const assertions = [...toStringArray(step?.expectedResponseFields), ...toStringArray(step?.bodyMustInclude)];
284
- return assertsEmptyOrZero(assertions, step?.responseBody);
285
- }
286
- /**
287
- * True when the step asserts an empty or zero result, reading the assertion
288
- * strings AND `responseBody`. An EMPTY ARRAY body is itself the "no rows came
289
- * back" assertion, and can be the only empty-result signal a step carries.
290
- *
291
- * `{}` deliberately does NOT count: an unspecified body is filled in as `{}`
292
- * (ScenarioGenerationService), so it cannot be told apart from a body the agent
293
- * never wrote. `isAssertStep` above already reads `{}` as asserting nothing.
294
- */
295
- function assertsEmptyOrZero(strings, responseBody) {
296
- if (Array.isArray(responseBody) && responseBody.length === 0)
297
- return true;
298
- return strings.some((raw) => {
299
- const s = raw.toLowerCase();
300
- if (/\bcount\b/.test(s) && /\b0\b/.test(s))
301
- return true;
302
- if (/(^|\D)0\s+results?\b/.test(s))
303
- return true;
304
- if (s.includes("empty"))
305
- return true;
306
- if (s.includes("no results") || s.includes("no result"))
307
- return true;
308
- if (s.includes("[]"))
309
- return true;
310
- return false;
311
- });
312
- }
313
- /** Strict near-miss: one string strictly contains the other (different length). */
314
- function isStrictNearMiss(a, b) {
315
- if (a === b)
316
- return false;
317
- if (Math.min(a.length, b.length) < MIN_NEAR_MISS_LENGTH)
318
- return false;
319
- return a.includes(b) || b.includes(a);
320
- }
321
- function groupHasDivergentOutcome(group) {
322
- const statuses = new Set(group.map((s) => s?.expectedStatusCode));
323
- const interactions = new Set(group.map((s) => s?.interactionType));
324
- return statuses.size > 1 || interactions.size > 1;
325
- }
326
- /** Largest number appearing in a string, or -Infinity when none. */
327
- function maxNumberIn(text) {
328
- const matches = text.match(/-?\d+(?:\.\d+)?/g);
329
- if (!matches)
330
- return Number.NEGATIVE_INFINITY;
331
- return Math.max(...matches.map(Number));
332
- }
333
- /**
334
- * Normalize a path so param segments collapse: numeric ids, `{brace}` params,
335
- * `:colon` params and long hex/uuid segments all become `:p`.
336
- */
337
- function normalizePath(path) {
338
- const pathPart = (path ?? "").split("?")[0];
339
- return pathPart
340
- .split("/")
341
- .map((seg) => {
342
- if (seg === "")
343
- return seg;
344
- if (/^\{.*\}$/.test(seg))
345
- return ":p";
346
- if (/^:.+/.test(seg))
347
- return ":p";
348
- if (/^\d+$/.test(seg))
349
- return ":p";
350
- if (/^[0-9a-fA-F-]{16,}$/.test(seg))
351
- return ":p";
352
- return seg.toLowerCase();
353
- })
354
- .join("/");
355
- }
356
- /** Steps sorted by their `order` field, falling back to array position. */
357
- function orderedSteps(steps) {
358
- return steps
359
- .map((step, index) => ({ step, index }))
360
- .sort((a, b) => {
361
- const ao = typeof a.step?.order === "number" ? a.step.order : a.index;
362
- const bo = typeof b.step?.order === "number" ? b.step.order : b.index;
363
- if (ao !== bo)
364
- return ao - bo;
365
- return a.index - b.index;
366
- })
367
- .map((entry) => entry.step);
368
- }
369
- function toStringArray(value) {
370
- if (!Array.isArray(value))
371
- return [];
372
- return value.filter((v) => typeof v === "string");
373
- }
374
- function toChainSourceSteps(chainsFrom) {
375
- if (!chainsFrom)
376
- return [];
377
- const refs = Array.isArray(chainsFrom) ? chainsFrom : [chainsFrom];
378
- return refs.map((r) => r?.sourceStep).filter((s) => typeof s === "number");
379
- }