@galaxyproject/orbit 0.0.0 → 0.9.0

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 (451) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/LICENSE +21 -0
  3. package/README.md +760 -5
  4. package/bin/legacy-mcp-config.js +363 -0
  5. package/bin/orbit.js +810 -0
  6. package/bin/pi-extension-path.d.ts +4 -0
  7. package/bin/pi-extension-path.js +44 -0
  8. package/bin/provider-auth.d.ts +22 -0
  9. package/bin/provider-auth.js +77 -0
  10. package/bin/thinking-pref.js +38 -0
  11. package/bin/update-check.d.ts +15 -0
  12. package/bin/update-check.js +137 -0
  13. package/extensions/cli-update/index.ts +32 -0
  14. package/extensions/orbit/activity-hooks.ts +141 -0
  15. package/extensions/orbit/activity.ts +116 -0
  16. package/extensions/orbit/agent-dir.ts +11 -0
  17. package/extensions/orbit/auto-resume.ts +352 -0
  18. package/extensions/orbit/config.ts +3 -0
  19. package/extensions/orbit/confusables.ts +1 -0
  20. package/extensions/orbit/context.ts +957 -0
  21. package/extensions/orbit/dashboard-commands.ts +191 -0
  22. package/extensions/orbit/dashboard-store.ts +473 -0
  23. package/extensions/orbit/dashboard-tools.ts +1185 -0
  24. package/extensions/orbit/empty-completion.ts +99 -0
  25. package/extensions/orbit/evidence-gate.ts +1276 -0
  26. package/extensions/orbit/evidence-override-command.ts +294 -0
  27. package/extensions/orbit/exec-guard/bash-risk.ts +1408 -0
  28. package/extensions/orbit/exec-guard/gate.ts +181 -0
  29. package/extensions/orbit/exec-guard/guardian-config.ts +54 -0
  30. package/extensions/orbit/exec-guard/index.ts +1 -0
  31. package/extensions/orbit/exec-guard/model-tier.ts +36 -0
  32. package/extensions/orbit/exec-guard/path-jail.ts +54 -0
  33. package/extensions/orbit/exec-guard/policy.ts +342 -0
  34. package/extensions/orbit/exec-guard/runtime-state.ts +15 -0
  35. package/extensions/orbit/exec-guard/sensitive-read.ts +525 -0
  36. package/extensions/orbit/exec-guard/types.ts +42 -0
  37. package/extensions/orbit/execution-commands.ts +134 -0
  38. package/extensions/orbit/feedback-command.ts +81 -0
  39. package/extensions/orbit/feedback.ts +96 -0
  40. package/extensions/orbit/galaxy-api.test.ts +122 -0
  41. package/extensions/orbit/galaxy-api.ts +606 -0
  42. package/extensions/orbit/galaxy-artifact-link-guidance.ts +5 -0
  43. package/extensions/orbit/galaxy-destructive-gate.ts +113 -0
  44. package/extensions/orbit/galaxy-enrich.ts +1108 -0
  45. package/extensions/orbit/galaxy-failure.ts +101 -0
  46. package/extensions/orbit/galaxy-fixture.ts +126 -0
  47. package/extensions/orbit/galaxy-history-guard.ts +233 -0
  48. package/extensions/orbit/galaxy-job-block.ts +347 -0
  49. package/extensions/orbit/galaxy-job-ops.ts +288 -0
  50. package/extensions/orbit/galaxy-live-source.ts +929 -0
  51. package/extensions/orbit/galaxy-markdown-adapter.ts +248 -0
  52. package/extensions/orbit/galaxy-op-policy.ts +332 -0
  53. package/extensions/orbit/galaxy-ops-tools.ts +326 -0
  54. package/extensions/orbit/galaxy-output-store.ts +80 -0
  55. package/extensions/orbit/galaxy-output.ts +346 -0
  56. package/extensions/orbit/galaxy-page-binding.ts +222 -0
  57. package/extensions/orbit/galaxy-page-markdown-guidance.ts +15 -0
  58. package/extensions/orbit/galaxy-page-ops.ts +159 -0
  59. package/extensions/orbit/galaxy-page-sync.test.ts +417 -0
  60. package/extensions/orbit/galaxy-page-sync.ts +318 -0
  61. package/extensions/orbit/galaxy-pages-api.ts +203 -0
  62. package/extensions/orbit/galaxy-pages-sync.ts +409 -0
  63. package/extensions/orbit/galaxy-poll-guard.ts +189 -0
  64. package/extensions/orbit/galaxy-poller.ts +706 -0
  65. package/extensions/orbit/galaxy-provenance.ts +623 -0
  66. package/extensions/orbit/galaxy-reconcile.ts +956 -0
  67. package/extensions/orbit/galaxy-recovery.ts +176 -0
  68. package/extensions/orbit/galaxy-repeat-guard.ts +55 -0
  69. package/extensions/orbit/galaxy-submission-capture.ts +707 -0
  70. package/extensions/orbit/galaxy-submission.ts +502 -0
  71. package/extensions/orbit/galaxy-tool-source.ts +78 -0
  72. package/extensions/orbit/galaxy-tool-wrap.ts +234 -0
  73. package/extensions/orbit/galaxy-udt-block.ts +127 -0
  74. package/extensions/orbit/galaxy-upload-tus.ts +334 -0
  75. package/extensions/orbit/galaxy-upload.ts +300 -0
  76. package/extensions/orbit/galaxy-version-capture.ts +65 -0
  77. package/extensions/orbit/git.ts +173 -0
  78. package/extensions/orbit/harness-block-fields.ts +597 -0
  79. package/extensions/orbit/index.ts +593 -0
  80. package/extensions/orbit/init-gate.ts +284 -0
  81. package/extensions/orbit/instructions-command.ts +136 -0
  82. package/extensions/orbit/invocation-failure-hint.ts +57 -0
  83. package/extensions/orbit/iwc-plan-check.ts +94 -0
  84. package/extensions/orbit/lesson-command.ts +303 -0
  85. package/extensions/orbit/lesson-hint.ts +245 -0
  86. package/extensions/orbit/lesson-nudge.ts +106 -0
  87. package/extensions/orbit/lessons/compose.ts +177 -0
  88. package/extensions/orbit/lessons/counters.ts +120 -0
  89. package/extensions/orbit/lessons/enabled.ts +116 -0
  90. package/extensions/orbit/lessons/matcher.ts +346 -0
  91. package/extensions/orbit/lessons/paths.ts +286 -0
  92. package/extensions/orbit/lessons/pi-event-contract.ts +105 -0
  93. package/extensions/orbit/lessons/propose-replay.ts +103 -0
  94. package/extensions/orbit/lessons/propose-tool.ts +222 -0
  95. package/extensions/orbit/lessons/propose.ts +502 -0
  96. package/extensions/orbit/lessons/replay.ts +89 -0
  97. package/extensions/orbit/lessons/reproduction-index.ts +83 -0
  98. package/extensions/orbit/lessons/search-tool.ts +99 -0
  99. package/extensions/orbit/lessons/search.ts +136 -0
  100. package/extensions/orbit/lessons/store.ts +415 -0
  101. package/extensions/orbit/lessons/text.ts +36 -0
  102. package/extensions/orbit/lessons/types.ts +69 -0
  103. package/extensions/orbit/lessons/user-dir.ts +24 -0
  104. package/extensions/orbit/lessons/user-lesson.ts +88 -0
  105. package/extensions/orbit/lessons/wrapper.ts +61 -0
  106. package/extensions/orbit/lessons-command.ts +147 -0
  107. package/extensions/orbit/local-exec.ts +39 -0
  108. package/extensions/orbit/mcp-output.ts +179 -0
  109. package/extensions/orbit/mcp-servers.test.ts +24 -0
  110. package/extensions/orbit/mcp-servers.ts +25 -0
  111. package/extensions/orbit/model-script.ts +351 -0
  112. package/extensions/orbit/notebook-anchors.ts +342 -0
  113. package/extensions/orbit/notebook-writer.ts +863 -0
  114. package/extensions/orbit/observation-allowlists.ts +1109 -0
  115. package/extensions/orbit/observation-replay.ts +148 -0
  116. package/extensions/orbit/observation-triggers.ts +563 -0
  117. package/extensions/orbit/observation-ui.ts +175 -0
  118. package/extensions/orbit/observations-command.ts +421 -0
  119. package/extensions/orbit/observations-config.ts +200 -0
  120. package/extensions/orbit/observations.ts +1381 -0
  121. package/extensions/orbit/profiles.ts +270 -0
  122. package/extensions/orbit/progress-updates.ts +95 -0
  123. package/extensions/orbit/proposal-commands.ts +882 -0
  124. package/extensions/orbit/provenance-fs-file.ts +59 -0
  125. package/extensions/orbit/provenance-fs.ts +32 -0
  126. package/extensions/orbit/provenance-memory-fs.ts +59 -0
  127. package/extensions/orbit/record-store-file.ts +177 -0
  128. package/extensions/orbit/record-store-page.ts +77 -0
  129. package/extensions/orbit/record-store-seam.ts +47 -0
  130. package/extensions/orbit/record-store.ts +149 -0
  131. package/extensions/orbit/registry-carrier.ts +107 -0
  132. package/extensions/orbit/registry-eligibility.ts +95 -0
  133. package/extensions/orbit/registry-evaluation.ts +403 -0
  134. package/extensions/orbit/registry-evaluator.ts +519 -0
  135. package/extensions/orbit/registry-import.ts +91 -0
  136. package/extensions/orbit/registry-lock.ts +221 -0
  137. package/extensions/orbit/registry-memory-fs.ts +42 -0
  138. package/extensions/orbit/registry-page-carrier.ts +103 -0
  139. package/extensions/orbit/registry-proposal.ts +1348 -0
  140. package/extensions/orbit/registry-runtime.ts +234 -0
  141. package/extensions/orbit/registry-schema.ts +735 -0
  142. package/extensions/orbit/registry-submitter.ts +59 -0
  143. package/extensions/orbit/registry-templates.ts +346 -0
  144. package/extensions/orbit/registry.ts +476 -0
  145. package/extensions/orbit/sandbox/index.ts +120 -0
  146. package/extensions/orbit/sandbox/sandbox-bash.ts +93 -0
  147. package/extensions/orbit/sandbox/sandbox-config.ts +87 -0
  148. package/extensions/orbit/sandbox/sandbox-init-message.ts +28 -0
  149. package/extensions/orbit/secret-redaction.ts +247 -0
  150. package/extensions/orbit/session-index/cwd.ts +18 -0
  151. package/extensions/orbit/session-index/db.ts +56 -0
  152. package/extensions/orbit/session-index/indexer.ts +154 -0
  153. package/extensions/orbit/session-index/is-enabled.ts +17 -0
  154. package/extensions/orbit/session-index/parse.ts +254 -0
  155. package/extensions/orbit/session-index/query.ts +209 -0
  156. package/extensions/orbit/session-index/schema.ts +72 -0
  157. package/extensions/orbit/session-index/tools.ts +152 -0
  158. package/extensions/orbit/session-lifecycle.ts +321 -0
  159. package/extensions/orbit/skill-triggers.ts +159 -0
  160. package/extensions/orbit/skills-command.ts +48 -0
  161. package/extensions/orbit/skills-discovery.ts +435 -0
  162. package/extensions/orbit/skills.ts +84 -0
  163. package/extensions/orbit/sra-import-gate.ts +77 -0
  164. package/extensions/orbit/state.ts +265 -0
  165. package/extensions/orbit/submission-replay.ts +147 -0
  166. package/extensions/orbit/sync-command.ts +157 -0
  167. package/extensions/orbit/teams/critic-parser.ts +93 -0
  168. package/extensions/orbit/teams/dispatcher.ts +210 -0
  169. package/extensions/orbit/teams/is-enabled.ts +22 -0
  170. package/extensions/orbit/teams/tool.ts +187 -0
  171. package/extensions/orbit/teams/types.ts +57 -0
  172. package/extensions/orbit/teams/validate.ts +42 -0
  173. package/extensions/orbit/tester-id-command.ts +133 -0
  174. package/extensions/orbit/tools-sync.ts +279 -0
  175. package/extensions/orbit/tools.ts +1300 -0
  176. package/extensions/orbit/types.ts +41 -0
  177. package/extensions/orbit/ui-bridge.ts +55 -0
  178. package/extensions/orbit/ulid.ts +60 -0
  179. package/extensions/orbit/user-instructions.ts +388 -0
  180. package/extensions/orbit/vendor/skills/_catalog.json +68 -0
  181. package/extensions/orbit/vendor/skills/_manifest.json +1106 -0
  182. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/SKILL.md +112 -0
  183. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_provenance.json +218 -0
  184. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_required_tools.json +33 -0
  185. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_verify.json +4 -0
  186. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/add.json +50 -0
  187. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-extract.json +51 -0
  188. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-next-step.json +42 -0
  189. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-validate.json +140 -0
  190. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/list.json +32 -0
  191. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/galaxy-tool-job-failure-reference.md +137 -0
  192. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/open-requirements-ledger.md +173 -0
  193. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-tool-summary.schema.json +175 -0
  194. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
  195. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/SKILL.md +82 -0
  196. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_provenance.json +133 -0
  197. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_required_tools.json +16 -0
  198. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_verify.json +4 -0
  199. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/cli/draft-validate.json +140 -0
  200. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/galaxy-workflow-draft-format.md +135 -0
  201. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
  202. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
  203. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/SKILL.md +84 -0
  204. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_provenance.json +166 -0
  205. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_verify.json +29 -0
  206. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/galaxy-workflow-testability-design.md +136 -0
  207. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  208. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-test-data-conventions.md +308 -0
  209. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-asserts-idioms.md +246 -0
  210. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-workflow-test-architecture.md +151 -0
  211. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
  212. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/summary-galaxy-workflow.schema.json +687 -0
  213. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/tests-format.schema.json +7328 -0
  214. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/SKILL.md +127 -0
  215. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_provenance.json +198 -0
  216. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_required_tools.json +16 -0
  217. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_verify.json +4 -0
  218. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/cli/convert.json +123 -0
  219. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/galaxy-data-flow-draft-contract.md +129 -0
  220. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  221. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-test-data-conventions.md +308 -0
  222. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/open-requirements-ledger.md +173 -0
  223. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/SKILL.md +73 -0
  224. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_provenance.json +160 -0
  225. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_verify.json +4 -0
  226. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.md +43 -0
  227. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.yml +1250 -0
  228. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-tool-job-failure-reference.md +137 -0
  229. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
  230. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  231. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/nextflow-operators-to-galaxy-collection-recipes.md +98 -0
  232. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-asserts-idioms.md +246 -0
  233. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-workflow-test-architecture.md +151 -0
  234. package/extensions/orbit/vendor/skills/discover-shed-tool/SKILL.md +174 -0
  235. package/extensions/orbit/vendor/skills/discover-shed-tool/_provenance.json +130 -0
  236. package/extensions/orbit/vendor/skills/discover-shed-tool/_required_tools.json +18 -0
  237. package/extensions/orbit/vendor/skills/discover-shed-tool/_verify.json +17 -0
  238. package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-revisions.json +48 -0
  239. package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-search.json +95 -0
  240. package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-versions.json +39 -0
  241. package/extensions/orbit/vendor/skills/discover-shed-tool/references/notes/component-tool-shed-search.md +230 -0
  242. package/extensions/orbit/vendor/skills/discover-shed-tool/references/schemas/galaxy-tool-discovery.schema.json +231 -0
  243. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/SKILL.md +93 -0
  244. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_provenance.json +201 -0
  245. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_required_tools.json +28 -0
  246. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_verify.json +17 -0
  247. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/planemo.md +44 -0
  248. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/validate-tests.json +40 -0
  249. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/galaxy-workflow-testability-design.md +136 -0
  250. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  251. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-test-data-conventions.md +308 -0
  252. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
  253. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
  254. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
  255. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/tests-format.schema.json +7328 -0
  256. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/SKILL.md +81 -0
  257. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_provenance.json +93 -0
  258. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_verify.json +17 -0
  259. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
  260. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/schemas/summary-galaxy-workflow.schema.json +687 -0
  261. package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/SKILL.md +81 -0
  262. package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/_assembly.json +23 -0
  263. package/extensions/orbit/vendor/skills/run-workflow-test/SKILL.md +79 -0
  264. package/extensions/orbit/vendor/skills/run-workflow-test/_provenance.json +125 -0
  265. package/extensions/orbit/vendor/skills/run-workflow-test/_required_tools.json +28 -0
  266. package/extensions/orbit/vendor/skills/run-workflow-test/_verify.json +4 -0
  267. package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/planemo.md +44 -0
  268. package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/validate-tests.json +40 -0
  269. package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
  270. package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
  271. package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
  272. package/extensions/orbit/vendor/skills/run-workflow-test/references/schemas/tests-format.schema.json +7328 -0
  273. package/extensions/orbit/vendor/skills/skills/LICENSE +21 -0
  274. package/extensions/orbit/vendor/skills/skills/collection-manipulation/README.md +39 -0
  275. package/extensions/orbit/vendor/skills/skills/collection-manipulation/SKILL.md +892 -0
  276. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/api-patterns.md +927 -0
  277. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/apply-rules.md +1482 -0
  278. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/test-patterns.md +996 -0
  279. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/tools.md +609 -0
  280. package/extensions/orbit/vendor/skills/skills/galaxy-data-libraries/SKILL.md +170 -0
  281. package/extensions/orbit/vendor/skills/skills/galaxy-integration/README.md +162 -0
  282. package/extensions/orbit/vendor/skills/skills/galaxy-integration/SKILL.md +99 -0
  283. package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/README.md +119 -0
  284. package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/tool-checking.md +419 -0
  285. package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/workflow-testing.md +617 -0
  286. package/extensions/orbit/vendor/skills/skills/galaxy-integration/galaxy-integration.md +614 -0
  287. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/SKILL.md +144 -0
  288. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/average_col3.ipynb +68 -0
  289. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/extract_sample_metadata.ipynb +190 -0
  290. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/variant_annotation.ipynb +816 -0
  291. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/vcp_variant_map.ipynb +443 -0
  292. package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/README.md +165 -0
  293. package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/galaxy_tool_checker.py +521 -0
  294. package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/SKILL.md +128 -0
  295. package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/gotchas.md +135 -0
  296. package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/history-access.md +132 -0
  297. package/extensions/orbit/vendor/skills/skills/reproduciblify/SKILL.md +146 -0
  298. package/extensions/orbit/vendor/skills/skills/reproduciblify/references/directives.yml +496 -0
  299. package/extensions/orbit/vendor/skills/skills/udt-authoring/SKILL.md +245 -0
  300. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/01-minimal-echo.yml +27 -0
  301. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/02-single-data-in-out.yml +27 -0
  302. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/03-multiple-data-js.yml +28 -0
  303. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/04-select-integer-boolean.yml +41 -0
  304. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/05-resource-requirements.yml +25 -0
  305. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/06-biocontainer-tool.yml +38 -0
  306. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/07-inline-python-script.yml +56 -0
  307. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/08-configfile-script.yml +74 -0
  308. package/extensions/orbit/vendor/skills/skills/udt-authoring/references/common-mistakes.md +76 -0
  309. package/extensions/orbit/vendor/skills/skills/udt-authoring/references/schema-reference.md +155 -0
  310. package/extensions/orbit/vendor/skills/skills/udt-authoring/references/templating.md +196 -0
  311. package/extensions/orbit/vendor/skills/skills/udt-authoring/scripts/validate.py +109 -0
  312. package/extensions/orbit/vendor/skills/skills/workflow-reports/README.md +71 -0
  313. package/extensions/orbit/vendor/skills/skills/workflow-reports/SKILL.md +190 -0
  314. package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/histology-staining.md +145 -0
  315. package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/tissue-microarray-analysis.md +176 -0
  316. package/extensions/orbit/vendor/skills/skills/workflow-reports/references/directives.md +415 -0
  317. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/SKILL.md +98 -0
  318. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_provenance.json +93 -0
  319. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_required_tools.json +28 -0
  320. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_verify.json +17 -0
  321. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/convert.json +123 -0
  322. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/foundry.md +25 -0
  323. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/validate.json +142 -0
  324. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/schemas/summary-galaxy-workflow.schema.json +687 -0
  325. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/SKILL.md +74 -0
  326. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_provenance.json +71 -0
  327. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_required_tools.json +16 -0
  328. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_verify.json +4 -0
  329. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/cli/validate.json +142 -0
  330. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
  331. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-testability-design.md +136 -0
  332. package/extensions/orbit/vendor-skills.ts +283 -0
  333. package/extensions/orbit/workspace-state-dir.ts +46 -0
  334. package/extensions/orbit-handoff/index.ts +73 -0
  335. package/extensions/orbit-handoff/orbit-launcher.ts +142 -0
  336. package/extensions/whats-new/index.ts +98 -0
  337. package/lessons/LICENSE +17 -0
  338. package/lessons/README.md +117 -0
  339. package/lessons/SCHEMA.md +187 -0
  340. package/lessons/build-snapshot.mjs +311 -0
  341. package/lessons/data/downloaded-file-is-not-what-its-extension-says.md +61 -0
  342. package/lessons/galaxy-api/403-history-is-a-hard-stop.md +57 -0
  343. package/lessons/galaxy-api/collection-into-single-dataset-input.md +61 -0
  344. package/lessons/galaxy-api/connectedvalue-in-command-line.md +62 -0
  345. package/lessons/galaxy-api/hid-is-not-an-id.md +59 -0
  346. package/lessons/galaxy-api/invoke-workflow-inputs-not-params.md +60 -0
  347. package/lessons/galaxy-api/repeat-param-pipe-keys.md +63 -0
  348. package/lessons/galaxy-api/run-tool-returns-on-submit.md +61 -0
  349. package/lessons/galaxy-tools/reference-index-not-on-server.md +62 -0
  350. package/lessons/log.md +42 -0
  351. package/lessons/reproduction/input-population-mismatch.md +59 -0
  352. package/lessons/reproduction/methods-text-vs-executed-parameters.md +62 -0
  353. package/lessons/snapshot.json +1004 -0
  354. package/lessons/stats/de-contrast-direction-and-sample-labels.md +63 -0
  355. package/lessons/stats/na-coerced-to-zero-in-filters.md +59 -0
  356. package/lessons/validate.mjs +1219 -0
  357. package/package.json +103 -5
  358. package/shared/agent-prompt.d.ts +8 -0
  359. package/shared/agent-prompt.js +15 -0
  360. package/shared/approval-prompt.d.ts +4 -0
  361. package/shared/approval-prompt.js +61 -0
  362. package/shared/brain-env.d.ts +14 -0
  363. package/shared/brain-env.js +108 -0
  364. package/shared/brain-exit.d.ts +20 -0
  365. package/shared/brain-exit.js +149 -0
  366. package/shared/capability-profile.d.ts +16 -0
  367. package/shared/capability-profile.js +75 -0
  368. package/shared/config-source.d.ts +13 -0
  369. package/shared/config-source.js +17 -0
  370. package/shared/confusables.d.ts +3 -0
  371. package/shared/confusables.js +93 -0
  372. package/shared/custom-provider.d.ts +45 -0
  373. package/shared/custom-provider.js +169 -0
  374. package/shared/dashboard-contract.d.ts +123 -0
  375. package/shared/dashboard-contract.js +560 -0
  376. package/shared/dashboard-layout-store.d.ts +44 -0
  377. package/shared/dashboard-layout-store.js +281 -0
  378. package/shared/feedback-contract.d.ts +61 -0
  379. package/shared/feedback-contract.js +146 -0
  380. package/shared/galaxy-artifact-links.d.ts +30 -0
  381. package/shared/galaxy-artifact-links.js +166 -0
  382. package/shared/galaxy-credentials.d.ts +26 -0
  383. package/shared/galaxy-credentials.js +71 -0
  384. package/shared/galaxy-destructive.d.ts +21 -0
  385. package/shared/galaxy-destructive.js +269 -0
  386. package/shared/galaxy-guidance.d.ts +2 -0
  387. package/shared/galaxy-guidance.js +53 -0
  388. package/shared/galaxy-live-contract.d.ts +92 -0
  389. package/shared/galaxy-live-contract.js +180 -0
  390. package/shared/galaxy-page-cells.d.ts +2 -0
  391. package/shared/galaxy-page-cells.js +22 -0
  392. package/shared/galaxy-tool-family.d.ts +5 -0
  393. package/shared/galaxy-tool-family.js +27 -0
  394. package/shared/galaxy-url.d.ts +1 -0
  395. package/shared/galaxy-url.js +48 -0
  396. package/shared/gtn.d.ts +31 -0
  397. package/shared/gtn.js +253 -0
  398. package/shared/handled-prompt.d.ts +3 -0
  399. package/shared/handled-prompt.js +26 -0
  400. package/shared/known-provider.d.ts +11 -0
  401. package/shared/known-provider.js +89 -0
  402. package/shared/lesson-hint-marker.d.ts +7 -0
  403. package/shared/lesson-hint-marker.js +32 -0
  404. package/shared/lesson-rules.d.ts +150 -0
  405. package/shared/lesson-rules.js +203 -0
  406. package/shared/malformed-object-ids.d.ts +1 -0
  407. package/shared/malformed-object-ids.js +37 -0
  408. package/shared/markdown-sections.d.ts +10 -0
  409. package/shared/markdown-sections.js +118 -0
  410. package/shared/notebook-fences.d.ts +12 -0
  411. package/shared/notebook-fences.js +53 -0
  412. package/shared/observation-contract.d.ts +91 -0
  413. package/shared/observation-contract.js +544 -0
  414. package/shared/orbit-config.d.ts +226 -0
  415. package/shared/orbit-config.js +189 -0
  416. package/shared/orbit-env.d.ts +13 -0
  417. package/shared/orbit-env.js +124 -0
  418. package/shared/orbit-shell-contract.d.ts +74 -0
  419. package/shared/orbit-shell-contract.js +30 -0
  420. package/shared/page-content-hash.d.ts +1 -0
  421. package/shared/page-content-hash.js +13 -0
  422. package/shared/pi-default-model.d.ts +7 -0
  423. package/shared/pi-default-model.js +41 -0
  424. package/shared/prompt-blocks.d.ts +87 -0
  425. package/shared/prompt-blocks.js +898 -0
  426. package/shared/provider-auth-caps.d.ts +22 -0
  427. package/shared/provider-auth-caps.js +57 -0
  428. package/shared/random.d.ts +2 -0
  429. package/shared/random.js +15 -0
  430. package/shared/record-excerpt.d.ts +4 -0
  431. package/shared/record-excerpt.js +16 -0
  432. package/shared/redirect-guard.d.ts +36 -0
  433. package/shared/redirect-guard.js +235 -0
  434. package/shared/skills-pin.d.ts +8 -0
  435. package/shared/skills-pin.js +9 -0
  436. package/shared/skills-router.d.ts +17 -0
  437. package/shared/skills-router.js +63 -0
  438. package/shared/sra-import.d.ts +34 -0
  439. package/shared/sra-import.js +206 -0
  440. package/shared/state-dir.d.ts +21 -0
  441. package/shared/state-dir.js +240 -0
  442. package/shared/team-dispatch-contract.d.ts +28 -0
  443. package/shared/team-dispatch-contract.js +1 -0
  444. package/shared/truncate-middle.d.ts +11 -0
  445. package/shared/truncate-middle.js +68 -0
  446. package/shared/version-compare.d.ts +10 -0
  447. package/shared/version-compare.js +74 -0
  448. package/shared/whats-new.d.ts +31 -0
  449. package/shared/whats-new.js +83 -0
  450. package/shared/wsl.d.ts +10 -0
  451. package/shared/wsl.js +24 -0
@@ -0,0 +1,1348 @@
1
+ /**
2
+ * Proposals and approvals (registry design v3 §4-§5): the model proposes a
3
+ * concrete run, the user approves it, the harness freezes it.
4
+ *
5
+ * A proposal is the model's one write. It lives in the notebook as a
6
+ * `loom-proposal` block and is untrusted from end to end: anything in it can
7
+ * have been typed by the model or edited by hand. An approval is the
8
+ * harness's: the template fetched from Galaxy and frozen, the version
9
+ * resolved, the Spec hashed, and an `approval` recorded on a fresh attempt in
10
+ * the signed registry. The block may echo a `spec_revision` afterwards for
11
+ * the reader's benefit, but nothing here ever reads one back; the registry,
12
+ * not the block, says what was approved.
13
+ *
14
+ * Revoke on edit is "the hash no longer matches", literally: the Spec is
15
+ * rebuilt from the block as it reads now, against what was frozen at
16
+ * approval, and hashed. A different `spec_revision` means the proposal no
17
+ * longer says what the user approved.
18
+ *
19
+ * Same coupling rule as the rest of the trusted core: Node builtins and the
20
+ * registry's own modules only. Fetching templates is the caller's job.
21
+ */
22
+
23
+ import type { RegistryStore } from "./registry";
24
+ import {
25
+ canonicalJson,
26
+ normalizeServerUrl,
27
+ parseSpec,
28
+ sha256Hex,
29
+ specRevision,
30
+ type Attempt,
31
+ type AttemptId,
32
+ type Predicate,
33
+ type Registry,
34
+ type Spec,
35
+ } from "./registry-schema";
36
+ import type { TemplateSnapshot } from "./registry-submitter";
37
+
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+ // The proposal
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+
42
+ export type TargetKind = Spec["target"]["kind"];
43
+ export type InputSrc = Spec["inputs"][number]["src"];
44
+
45
+ /** What a proposal names. `version` may be "unpinned"; a Spec's never is. */
46
+ export interface ProposalTarget {
47
+ kind: TargetKind;
48
+ workflow_id?: string;
49
+ tool_id?: string;
50
+ tool_uuid?: string;
51
+ version: string;
52
+ }
53
+
54
+ export interface ProposalInput {
55
+ slot: string;
56
+ src: InputSrc;
57
+ id: string;
58
+ }
59
+
60
+ export interface ProposalOverride {
61
+ param: string;
62
+ value: unknown;
63
+ rationale: string;
64
+ }
65
+
66
+ export interface Proposal {
67
+ proposalId: string;
68
+ stepAnchor: string;
69
+ label?: string;
70
+ target: ProposalTarget;
71
+ serverUrl: string;
72
+ historyId: string;
73
+ inputs: ProposalInput[];
74
+ overrides: ProposalOverride[];
75
+ predicate: Predicate;
76
+ /** Ids of assertions whose definitions get frozen into the Spec. */
77
+ assertions: string[];
78
+ /** The template the proposal was validated against when it was written. */
79
+ templateDigest?: string;
80
+ createdAt?: string;
81
+ }
82
+
83
+ export const UNPINNED = "unpinned";
84
+
85
+ /** `prop-` and six lowercase Crockford characters: short enough to type. */
86
+ export const PROPOSAL_ID_RE = /^prop-[0-9a-hjkmnp-tv-z]{6}$/;
87
+
88
+ const CROCKFORD = "0123456789abcdefghjkmnpqrstvwxyz";
89
+
90
+ export function newProposalId(random: (n: number) => Uint8Array, taken: Set<string>): string {
91
+ for (let tries = 0; tries < 100; tries++) {
92
+ const bytes = random(6);
93
+ let id = "prop-";
94
+ for (const b of bytes) id += CROCKFORD[b % 32];
95
+ if (!taken.has(id)) return id;
96
+ }
97
+ throw new Error("could not mint an unused proposal id");
98
+ }
99
+
100
+ /** The id field a target of this kind is named by. */
101
+ export function targetIdField(kind: TargetKind): "workflow_id" | "tool_id" | "tool_uuid" {
102
+ return kind === "workflow" ? "workflow_id" : kind === "tool" ? "tool_id" : "tool_uuid";
103
+ }
104
+
105
+ export function targetId(t: { kind: TargetKind } & Partial<ProposalTarget>): string | undefined {
106
+ return t[targetIdField(t.kind)];
107
+ }
108
+
109
+ // ─────────────────────────────────────────────────────────────────────────────
110
+ // Parsing the block's fields
111
+ // ─────────────────────────────────────────────────────────────────────────────
112
+
113
+ const MAX_FIELD = 4096;
114
+ const ID_TOKEN = /^[A-Za-z0-9._:/+@~-]{1,512}$/;
115
+
116
+ /**
117
+ * Turn a block's raw `key: value` fields into a Proposal, or say what is
118
+ * wrong with it. Collection fields are single-line JSON, like the harness
119
+ * fields on the other block types. Unknown keys are ignored, so a block
120
+ * written by a newer Orbit still reads. `spec_revision` is never read: it is
121
+ * display only, and the registry is the truth.
122
+ */
123
+ export function parseProposalFields(get: (key: string) => string | undefined): {
124
+ proposal: Proposal | null;
125
+ errors: string[];
126
+ } {
127
+ const errors: string[] = [];
128
+ const text = (key: string, required = true): string | undefined => {
129
+ const v = get(key);
130
+ if (v === undefined || v === "") {
131
+ if (required) errors.push(`${key} is missing`);
132
+ return undefined;
133
+ }
134
+ if (v.length > MAX_FIELD) {
135
+ errors.push(`${key} is too long`);
136
+ return undefined;
137
+ }
138
+ return v;
139
+ };
140
+ const json = (key: string): unknown => {
141
+ const raw = text(key);
142
+ if (raw === undefined) return undefined;
143
+ try {
144
+ return JSON.parse(raw);
145
+ } catch {
146
+ errors.push(`${key} is not valid JSON`);
147
+ return undefined;
148
+ }
149
+ };
150
+
151
+ const proposalId = text("proposal_id");
152
+ if (proposalId !== undefined && !ID_TOKEN.test(proposalId)) {
153
+ errors.push("proposal_id has characters a proposal id cannot");
154
+ }
155
+ const stepAnchor = text("step_anchor");
156
+ const serverUrl = text("server_url");
157
+ const historyId = text("history_id");
158
+ if (historyId !== undefined && !ID_TOKEN.test(historyId)) {
159
+ errors.push("history_id is not an id");
160
+ }
161
+ const target = parseTarget(json("target"), errors);
162
+ const inputs = parseInputs(json("inputs"), errors);
163
+ const overrides = parseOverrides(json("overrides"), errors);
164
+ const predicate = parsePredicate(json("predicate"), errors);
165
+ const assertionsRaw = get("assertions") === undefined ? [] : json("assertions");
166
+ const assertions = parseStringList(assertionsRaw, "assertions", errors);
167
+ const templateDigest = text("template_digest", false);
168
+ if (templateDigest !== undefined && !/^[0-9a-f]{64}$/.test(templateDigest)) {
169
+ errors.push("template_digest is not a sha256 digest");
170
+ }
171
+
172
+ if (
173
+ errors.length > 0 ||
174
+ !proposalId ||
175
+ !stepAnchor ||
176
+ !serverUrl ||
177
+ !historyId ||
178
+ !target ||
179
+ !inputs ||
180
+ !overrides ||
181
+ !predicate ||
182
+ !assertions
183
+ ) {
184
+ return { proposal: null, errors };
185
+ }
186
+ const proposal: Proposal = {
187
+ proposalId,
188
+ stepAnchor,
189
+ target,
190
+ serverUrl,
191
+ historyId,
192
+ inputs,
193
+ overrides,
194
+ predicate,
195
+ assertions,
196
+ };
197
+ const label = text("label", false);
198
+ if (label !== undefined) proposal.label = label;
199
+ if (templateDigest !== undefined) proposal.templateDigest = templateDigest;
200
+ const createdAt = text("created_at", false);
201
+ if (createdAt !== undefined) proposal.createdAt = createdAt;
202
+ return { proposal, errors };
203
+ }
204
+
205
+ function isRecord(v: unknown): v is Record<string, unknown> {
206
+ return v !== null && typeof v === "object" && !Array.isArray(v);
207
+ }
208
+
209
+ export function parseTarget(v: unknown, errors: string[]): ProposalTarget | null {
210
+ if (v === undefined) return null;
211
+ if (!isRecord(v)) {
212
+ errors.push("target must be an object");
213
+ return null;
214
+ }
215
+ const kind = v.kind;
216
+ if (kind !== "workflow" && kind !== "tool" && kind !== "udt") {
217
+ errors.push("target.kind must be workflow, tool or udt");
218
+ return null;
219
+ }
220
+ const field = targetIdField(kind);
221
+ const id = v[field];
222
+ if (typeof id !== "string" || !ID_TOKEN.test(id)) {
223
+ errors.push(`target.${field} is required for a ${kind}`);
224
+ return null;
225
+ }
226
+ for (const other of ["workflow_id", "tool_id", "tool_uuid"] as const) {
227
+ if (other !== field && v[other] !== undefined) {
228
+ errors.push(`target.${other} does not belong on a ${kind}`);
229
+ return null;
230
+ }
231
+ }
232
+ const version = v.version === undefined ? UNPINNED : v.version;
233
+ if (typeof version !== "string" || version === "" || version.length > 256) {
234
+ errors.push(`target.version must be a version or "${UNPINNED}"`);
235
+ return null;
236
+ }
237
+ return { kind, [field]: id, version } as ProposalTarget;
238
+ }
239
+
240
+ export function parseInputs(v: unknown, errors: string[]): ProposalInput[] | null {
241
+ if (v === undefined) return null;
242
+ if (!Array.isArray(v)) {
243
+ errors.push("inputs must be a list");
244
+ return null;
245
+ }
246
+ const out: ProposalInput[] = [];
247
+ for (const [i, x] of v.entries()) {
248
+ if (
249
+ !isRecord(x) ||
250
+ typeof x.slot !== "string" ||
251
+ x.slot === "" ||
252
+ x.slot.length > 512 ||
253
+ (x.src !== "hda" && x.src !== "hdca" && x.src !== "ldda") ||
254
+ typeof x.id !== "string" ||
255
+ !ID_TOKEN.test(x.id)
256
+ ) {
257
+ errors.push(`inputs[${i}] needs a slot, a src of hda/hdca/ldda, and a dataset id`);
258
+ return null;
259
+ }
260
+ out.push({ slot: x.slot, src: x.src, id: x.id });
261
+ }
262
+ return out;
263
+ }
264
+
265
+ export function parseOverrides(v: unknown, errors: string[]): ProposalOverride[] | null {
266
+ if (v === undefined) return null;
267
+ if (!Array.isArray(v)) {
268
+ errors.push("overrides must be a list");
269
+ return null;
270
+ }
271
+ const out: ProposalOverride[] = [];
272
+ for (const [i, x] of v.entries()) {
273
+ if (!isRecord(x) || typeof x.param !== "string" || x.param === "" || x.param.length > 512) {
274
+ errors.push(`overrides[${i}] needs a param name`);
275
+ return null;
276
+ }
277
+ if (typeof x.rationale !== "string" || x.rationale.trim() === "") {
278
+ errors.push(`overrides[${i}] (${x.param}) needs a rationale`);
279
+ return null;
280
+ }
281
+ if (!("value" in x)) {
282
+ errors.push(`overrides[${i}] (${x.param}) needs a value`);
283
+ return null;
284
+ }
285
+ try {
286
+ canonicalJson(x.value);
287
+ } catch {
288
+ errors.push(`overrides[${i}] (${x.param}) has a value JSON cannot hold`);
289
+ return null;
290
+ }
291
+ out.push({ param: x.param, value: x.value, rationale: x.rationale });
292
+ }
293
+ return out;
294
+ }
295
+
296
+ export function parsePredicate(v: unknown, errors: string[]): Predicate | null {
297
+ if (v === undefined) return null;
298
+ if (!isRecord(v)) {
299
+ errors.push("predicate must be an object");
300
+ return null;
301
+ }
302
+ const nonNegInt = (n: unknown) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
303
+ switch (v.kind) {
304
+ case "manual":
305
+ return { kind: "manual" };
306
+ case "exists_with_ext":
307
+ if (typeof v.ext !== "string" || v.ext === "") break;
308
+ if (v.min_count !== undefined && !nonNegInt(v.min_count)) break;
309
+ return v.min_count === undefined
310
+ ? { kind: "exists_with_ext", ext: v.ext }
311
+ : { kind: "exists_with_ext", ext: v.ext, min_count: v.min_count as number };
312
+ case "count_eq":
313
+ if (!nonNegInt(v.count)) break;
314
+ if (v.ext !== undefined && (typeof v.ext !== "string" || v.ext === "")) break;
315
+ return v.ext === undefined
316
+ ? { kind: "count_eq", count: v.count as number }
317
+ : { kind: "count_eq", count: v.count as number, ext: v.ext as string };
318
+ case "assertions_pass": {
319
+ const ids = parseStringList(v.ids, "predicate.ids", errors);
320
+ if (!ids) return null;
321
+ if (ids.length === 0) {
322
+ errors.push("an assertions_pass predicate needs at least one assertion id");
323
+ return null;
324
+ }
325
+ return { kind: "assertions_pass", ids };
326
+ }
327
+ }
328
+ errors.push(
329
+ "predicate must be {kind: manual}, {kind: exists_with_ext, ext, min_count?}, " +
330
+ "{kind: count_eq, count, ext?} or {kind: assertions_pass, ids}",
331
+ );
332
+ return null;
333
+ }
334
+
335
+ function parseStringList(v: unknown, what: string, errors: string[]): string[] | null {
336
+ if (v === undefined) return null;
337
+ if (!Array.isArray(v) || v.some((x) => typeof x !== "string" || x === "" || x.length > 256)) {
338
+ errors.push(`${what} must be a list of ids`);
339
+ return null;
340
+ }
341
+ return v as string[];
342
+ }
343
+
344
+ // ─────────────────────────────────────────────────────────────────────────────
345
+ // Templates
346
+ // ─────────────────────────────────────────────────────────────────────────────
347
+
348
+ /** One dataset input a template accepts. */
349
+ export interface TemplateSlot {
350
+ name: string;
351
+ /** Shown beside the canonical name, when the template has a friendlier one. */
352
+ label?: string;
353
+ collection: boolean;
354
+ required: boolean;
355
+ /** True when any variant takes several datasets. */
356
+ multiple: boolean;
357
+ /**
358
+ * The slot as each conditional branch declares it. A tool can name the same
359
+ * input in several branches with different settings -- NanoPlot's
360
+ * `mode|reads|files` takes one dataset under `mode|choice=batch` and several
361
+ * under `combined` -- so which one applies depends on what the proposal picks.
362
+ */
363
+ variants: SlotVariant[];
364
+ }
365
+
366
+ /** One branch's declaration of a slot, and the selector values that pick it. */
367
+ export interface SlotVariant {
368
+ when: BranchChoice[];
369
+ collection: boolean;
370
+ multiple: boolean;
371
+ /** Datatype extensions this branch takes; empty when the template doesn't say. */
372
+ accepts: string[];
373
+ }
374
+
375
+ /** A conditional's selector, the value that picks this branch, and its default. */
376
+ export interface BranchChoice {
377
+ param: string;
378
+ value: string;
379
+ default?: string;
380
+ }
381
+
382
+ /** What a frozen template body says a proposal may name. */
383
+ export interface TemplateShape {
384
+ version: string;
385
+ slots: Map<string, TemplateSlot>;
386
+ /** Non-dataset parameter names, in Galaxy's flat `a|b` form; repeats as `name_#`. */
387
+ params: Set<string>;
388
+ /** Repeat names, so `name_3|x` can be read as `name_#|x`. */
389
+ repeats: Set<string>;
390
+ /** Labels that name exactly one slot or parameter, for canonicalizing. */
391
+ aliases: Map<string, string>;
392
+ /** Each conditional selector's values, where the template lists them. */
393
+ choices: Map<string, Set<string>>;
394
+ /** Boolean selectors' true and false values, which branches are named by. */
395
+ booleans: Map<string, { t: string; f: string }>;
396
+ }
397
+
398
+ function emptyShape(version: string): TemplateShape {
399
+ return {
400
+ version,
401
+ slots: new Map(),
402
+ params: new Set(),
403
+ repeats: new Set(),
404
+ aliases: new Map(),
405
+ choices: new Map(),
406
+ booleans: new Map(),
407
+ };
408
+ }
409
+
410
+ /**
411
+ * Read a tool's `io_details` input list (or a user-defined tool's inputs,
412
+ * which use the same `type` vocabulary) into slots and parameter names.
413
+ *
414
+ * A dataset input is required when it isn't optional and sits outside any
415
+ * conditional or repeat. Inside one, whether it is needed depends on choices
416
+ * the proposal makes, and refusing on a guess would be worse than letting
417
+ * Galaxy say so at submission.
418
+ */
419
+ /**
420
+ * A group's members. Galaxy's `io_details` calls them `inputs` (and a
421
+ * conditional's branches `cases`, its selector `test_param`); a user-defined
422
+ * tool's YAML definition calls them `parameters` (`whens`, `test_parameter`).
423
+ */
424
+ function children(group: Record<string, unknown>): unknown {
425
+ return group.inputs ?? group.parameters;
426
+ }
427
+
428
+ /** A selector or case value as text; Galaxy gives booleans for a boolean selector. */
429
+ /** A template's accepted formats: a list, or a comma-separated string (a user-defined tool's `format`). */
430
+ function formatList(...vs: unknown[]): string[] {
431
+ const out = new Set<string>();
432
+ for (const v of vs) {
433
+ const items = typeof v === "string" ? v.split(",") : Array.isArray(v) ? v : [];
434
+ for (const x of items) if (typeof x === "string" && x.trim() !== "") out.add(x.trim());
435
+ }
436
+ return [...out];
437
+ }
438
+
439
+ function choiceText(v: unknown): string | undefined {
440
+ if (typeof v === "string") return v;
441
+ if (typeof v === "boolean" || typeof v === "number") return String(v);
442
+ return undefined;
443
+ }
444
+
445
+ /** Galaxy's `string_as_bool`: these spellings are true and anything else is false. */
446
+ const galaxyTrue = (v: unknown) => ["true", "yes", "on", "1"].includes(String(v).toLowerCase());
447
+
448
+ /**
449
+ * A selector's value as its branches name it. A boolean's value becomes its
450
+ * truevalue or falsevalue the way Galaxy reads it, so `"True"` is true and
451
+ * any string it doesn't know, its own truevalue included, is false.
452
+ */
453
+ function selectorText(shape: TemplateShape, selector: string, v: unknown): string | undefined {
454
+ const b = shape.booleans.get(selector);
455
+ if (b && v !== undefined) return galaxyTrue(v) ? b.t : b.f;
456
+ return choiceText(v);
457
+ }
458
+
459
+ function walkToolInputs(
460
+ inputs: unknown,
461
+ shape: TemplateShape,
462
+ prefix: string,
463
+ conditional: boolean,
464
+ branch: BranchChoice[] = [],
465
+ ): void {
466
+ if (!Array.isArray(inputs)) return;
467
+ for (const raw of inputs) {
468
+ if (!isRecord(raw) || typeof raw.name !== "string" || raw.name === "") continue;
469
+ const name = `${prefix}${raw.name}`;
470
+ const type = raw.type;
471
+ if (type === "data" || type === "data_collection") {
472
+ const variant: SlotVariant = {
473
+ when: branch,
474
+ collection: type === "data_collection",
475
+ multiple: raw.multiple === true,
476
+ accepts: formatList(raw.extensions, raw.format),
477
+ };
478
+ const seen = shape.slots.get(name);
479
+ if (seen) {
480
+ seen.variants.push(variant);
481
+ seen.multiple ||= variant.multiple;
482
+ continue;
483
+ }
484
+ shape.slots.set(name, {
485
+ name,
486
+ ...(typeof raw.label === "string" && raw.label !== "" ? { label: raw.label } : {}),
487
+ collection: type === "data_collection",
488
+ required: !conditional && raw.optional !== true,
489
+ multiple: variant.multiple,
490
+ variants: [variant],
491
+ });
492
+ } else if (type === "conditional") {
493
+ const testRaw = raw.test_param ?? raw.test_parameter;
494
+ const test = isRecord(testRaw) ? testRaw : null;
495
+ const selector = test && typeof test.name === "string" ? `${name}|${test.name}` : null;
496
+ if (selector) shape.params.add(selector);
497
+ if (selector && test?.type === "boolean") {
498
+ shape.booleans.set(selector, {
499
+ t: choiceText(test.truevalue) ?? "true",
500
+ f: choiceText(test.falsevalue) ?? "false",
501
+ });
502
+ }
503
+ const byDefault = selector
504
+ ? selectorText(shape, selector, test?.value ?? test?.default)
505
+ : undefined;
506
+ const casesRaw = raw.cases ?? raw.whens;
507
+ const cases = Array.isArray(casesRaw) ? casesRaw : [];
508
+ // A select can offer values no case names (Galaxy runs those with no
509
+ // extra inputs), so the options count too.
510
+ const values = new Set<string>();
511
+ for (const c of cases) {
512
+ const v = isRecord(c) ? choiceText(c.value ?? c.discriminator) : undefined;
513
+ if (v !== undefined) values.add(v);
514
+ }
515
+ for (const o of Array.isArray(test?.options) ? test.options : []) {
516
+ const v = choiceText(Array.isArray(o) ? o[1] : isRecord(o) ? o.value : undefined);
517
+ if (v !== undefined) values.add(v);
518
+ }
519
+ // The same selector path can sit under several parent branches.
520
+ if (selector && values.size > 0) {
521
+ const all = shape.choices.get(selector) ?? new Set<string>();
522
+ for (const v of values) all.add(v);
523
+ shape.choices.set(selector, all);
524
+ }
525
+ for (const c of cases) {
526
+ if (!isRecord(c)) continue;
527
+ const value = choiceText(c.value ?? c.discriminator);
528
+ const choice: BranchChoice[] =
529
+ selector && value !== undefined
530
+ ? [
531
+ {
532
+ param: selector,
533
+ value,
534
+ ...(byDefault !== undefined ? { default: byDefault } : {}),
535
+ },
536
+ ]
537
+ : [];
538
+ walkToolInputs(children(c), shape, `${name}|`, true, [...branch, ...choice]);
539
+ }
540
+ } else if (type === "repeat") {
541
+ shape.repeats.add(name);
542
+ walkToolInputs(children(raw), shape, `${name}_#|`, true, branch);
543
+ } else if (type === "section") {
544
+ walkToolInputs(children(raw), shape, `${name}|`, conditional, branch);
545
+ } else if (type === "upload_dataset") {
546
+ continue;
547
+ } else {
548
+ shape.params.add(name);
549
+ }
550
+ }
551
+ }
552
+
553
+ function workflowShape(body: Record<string, unknown>, version: string): TemplateShape {
554
+ const shape = emptyShape(version);
555
+ const labels = new Map<string, string[]>();
556
+ const slots = Array.isArray(body.slots) ? body.slots : [];
557
+ for (const s of slots) {
558
+ if (!isRecord(s) || typeof s.step_index !== "number") continue;
559
+ const name = String(s.step_index);
560
+ const label = typeof s.label === "string" && s.label !== "" ? s.label : undefined;
561
+ if (label) labels.set(label, [...(labels.get(label) ?? []), name]);
562
+ if (s.input_type === "data" || s.input_type === "data_collection") {
563
+ shape.slots.set(name, {
564
+ name,
565
+ ...(label ? { label } : {}),
566
+ collection: s.input_type === "data_collection",
567
+ required: s.optional !== true,
568
+ multiple: false,
569
+ variants: [
570
+ {
571
+ when: [],
572
+ collection: s.input_type === "data_collection",
573
+ multiple: false,
574
+ accepts: formatList(s.accepted_formats, s.acceptable_extensions),
575
+ },
576
+ ],
577
+ });
578
+ } else if (s.input_type === "parameter") {
579
+ shape.params.add(name);
580
+ }
581
+ }
582
+ for (const [label, names] of labels) if (names.length === 1) shape.aliases.set(label, names[0]);
583
+ return shape;
584
+ }
585
+
586
+ /** A user-defined tool's definition: the record's `representation`, if it has one. */
587
+ export function udtDefinition(body: unknown): Record<string, unknown> | null {
588
+ if (!isRecord(body)) return null;
589
+ return isRecord(body.representation) ? body.representation : body;
590
+ }
591
+
592
+ /**
593
+ * Read a frozen template body. The bodies are what `/approve` freezes:
594
+ * the tool's `io_details` description, `{workflow, slots}` for a workflow
595
+ * (Galaxy's details at the version plus the run form's slots), or the
596
+ * user-defined tool record from Galaxy.
597
+ */
598
+ export function templateShape(kind: TargetKind, snapshot: TemplateSnapshot): TemplateShape {
599
+ const body = snapshot.body;
600
+ if (kind === "workflow") {
601
+ return workflowShape(isRecord(body) ? body : {}, snapshot.version);
602
+ }
603
+ const shape = emptyShape(snapshot.version);
604
+ const source = kind === "udt" ? udtDefinition(body) : isRecord(body) ? body : null;
605
+ walkToolInputs(source?.inputs, shape, "", false);
606
+ for (const slot of shape.slots.values()) {
607
+ if (slot.label && !shape.aliases.has(slot.label)) shape.aliases.set(slot.label, slot.name);
608
+ }
609
+ return shape;
610
+ }
611
+
612
+ /** A parameter name in the template's own spelling, with repeat indices folded. */
613
+ function foldRepeats(param: string, repeats: Set<string>): string {
614
+ const parts = param.split("|");
615
+ let prefix = "";
616
+ return parts
617
+ .map((part) => {
618
+ const m = part.match(/^(.+)_(\d+)$/);
619
+ const folded = m && repeats.has(`${prefix}${m[1]}`) ? `${m[1]}_#` : part;
620
+ prefix += `${folded}|`;
621
+ return folded;
622
+ })
623
+ .join("|");
624
+ }
625
+
626
+ /** The template slot a proposal's slot name means, with repeat indices folded. */
627
+ export function slotFor(shape: TemplateShape, name: string): TemplateSlot | undefined {
628
+ if (name.includes("_#")) return undefined;
629
+ return shape.slots.get(name) ?? shape.slots.get(foldRepeats(name, shape.repeats));
630
+ }
631
+
632
+ // ─────────────────────────────────────────────────────────────────────────────
633
+ // Validation
634
+ // ─────────────────────────────────────────────────────────────────────────────
635
+
636
+ /**
637
+ * Canonical slot and parameter names: a workflow slot named by its label
638
+ * becomes its step index. Returns a copy; the input is untouched. Anything
639
+ * that doesn't resolve is left as written for `validateProposal` to report.
640
+ */
641
+ export function canonicalizeProposal(proposal: Proposal, shape: TemplateShape): Proposal {
642
+ const canon = (name: string) =>
643
+ shape.slots.has(name) || shape.params.has(name) ? name : (shape.aliases.get(name) ?? name);
644
+ return {
645
+ ...proposal,
646
+ inputs: proposal.inputs.map((i) => ({ ...i, slot: canon(i.slot) })),
647
+ overrides: proposal.overrides.map((o) => ({ ...o, param: canon(o.param) })),
648
+ };
649
+ }
650
+
651
+ function describeSlots(shape: TemplateShape): string {
652
+ const names = [...shape.slots.values()].map(
653
+ (s) => `${s.name}${s.label && s.label !== s.name ? ` (${s.label})` : ""}`,
654
+ );
655
+ return names.length > 0 ? names.join(", ") : "none";
656
+ }
657
+
658
+ /**
659
+ * A collection on a slot that takes one dataset. Says what would be accepted,
660
+ * so a model can change its call instead of resending the same one.
661
+ */
662
+ function collectionOnDatasetSlot(slotName: string, shape: TemplateShape): string {
663
+ const takers = [...shape.slots.values()]
664
+ .filter((s) => s.collection || s.multiple)
665
+ .map((s) => s.name);
666
+ return (
667
+ `input slot "${slotName}" takes one dataset, not a collection, and a workflow ` +
668
+ `input doesn't map over one here. ` +
669
+ `Give it the id of one dataset (src hda)` +
670
+ (takers.length > 0
671
+ ? `, or give the collection (src hdca) to a slot that takes one: ${takers.join(", ")}`
672
+ : "; nothing in this template takes a collection")
673
+ );
674
+ }
675
+
676
+ /**
677
+ * The variants of a slot this proposal's selector choices pick: a branch
678
+ * applies when each selector on its path is overridden to its value or, left
679
+ * alone, defaults to it. Empty when the choices pick a branch that doesn't
680
+ * declare the slot, which Galaxy would drop.
681
+ */
682
+ export function variantsFor(
683
+ proposal: Proposal,
684
+ shape: TemplateShape,
685
+ slot: TemplateSlot,
686
+ /** The input's name as given, so a selector is read from its own repeat instance. */
687
+ inputName: string = slot.name,
688
+ ): SlotVariant[] {
689
+ const given = new Map<string, unknown>(proposal.overrides.map((o) => [o.param, o.value]));
690
+ const inputParts = inputName.split("|");
691
+ // `rows_#|mode|choice` for the input `rows_1|mode|reads` is `rows_1|mode|choice`.
692
+ const instance = (param: string) =>
693
+ param
694
+ .split("|")
695
+ .map((part, i) => {
696
+ const m = inputParts[i]?.match(/^(.+)_(\d+)$/);
697
+ return part.endsWith("_#") && m && `${m[1]}_#` === part ? inputParts[i] : part;
698
+ })
699
+ .join("|");
700
+ const picked = slot.variants.filter((v) =>
701
+ v.when.every((c) => {
702
+ const param = instance(c.param);
703
+ const value = given.has(param) ? selectorText(shape, c.param, given.get(param)) : c.default;
704
+ return value === undefined || value === c.value;
705
+ }),
706
+ );
707
+ return picked;
708
+ }
709
+
710
+ /** The selector choices under which a slot is used, for a refusal. */
711
+ function slotBranches(slot: TemplateSlot): string {
712
+ const each = [
713
+ ...new Set(
714
+ slot.variants.map((v) => v.when.map((c) => `${c.param} is ${c.value}`).join(" and ")),
715
+ ),
716
+ ];
717
+ if (each.length <= 3) return `it's used when ${each.join("; or when ")}`;
718
+ const selectors = [...new Set(slot.variants.flatMap((v) => v.when.map((c) => c.param)))];
719
+ return `it's used only under other values of ${orList(selectors)}`;
720
+ }
721
+
722
+ /** What Galaxy says about the datasets a proposal names, for the format check. */
723
+ export interface InputFormats {
724
+ /**
725
+ * Each input's datatype extensions by `src:id`: one for a dataset, the
726
+ * element datatypes for a collection. Null when Galaxy couldn't say.
727
+ */
728
+ extensions: Map<string, string[] | null>;
729
+ /**
730
+ * Whether Galaxy takes a dataset of `ext` where `accepted` are asked for, as
731
+ * a subtype of one or by converting it. Undefined when that can't be told
732
+ * (a datatype the server didn't describe); null when the datatype hierarchy
733
+ * couldn't be read at all.
734
+ */
735
+ accepts: ((ext: string, accepted: string[]) => boolean | undefined) | null;
736
+ }
737
+
738
+ export const inputKey = (i: { src: string; id: string }) => `${i.src}:${i.id}`;
739
+
740
+ function orList(xs: string[]): string {
741
+ return xs.length < 2 ? xs.join("") : `${xs.slice(0, -1).join(", ")} or ${xs.at(-1)}`;
742
+ }
743
+
744
+ /**
745
+ * Each input whose datatype its slot can't take (#618). Only a provable
746
+ * mismatch is refused: a slot that names no formats, a dataset Galaxy didn't
747
+ * describe, or a datatype pairing the hierarchy can't settle is let through
748
+ * and listed in `unchecked` instead.
749
+ */
750
+ export function checkInputFormats(
751
+ proposal: Proposal,
752
+ shape: TemplateShape,
753
+ formats: InputFormats,
754
+ ): { problems: string[]; unchecked: string[] } {
755
+ const problems: string[] = [];
756
+ const unchecked: string[] = [];
757
+ for (const input of proposal.inputs) {
758
+ const slot = slotFor(shape, input.slot);
759
+ if (!slot) continue;
760
+ const variants = variantsFor(proposal, shape, slot, input.slot);
761
+ if (variants.length === 0 || variants.some((v) => v.accepts.length === 0)) continue;
762
+ const accepted = [...new Set(variants.flatMap((v) => v.accepts))];
763
+ if (accepted.includes("data")) continue;
764
+ const exts = formats.extensions.get(inputKey(input));
765
+ const what = input.src === "hdca" ? `collection ${input.id}` : `dataset ${input.id}`;
766
+ if (!exts || exts.length === 0) {
767
+ unchecked.push(`${input.slot}: Galaxy didn't say what format ${what} is`);
768
+ continue;
769
+ }
770
+ if (!formats.accepts) {
771
+ unchecked.push(`${input.slot}: Galaxy's datatype hierarchy couldn't be read`);
772
+ continue;
773
+ }
774
+ const verdicts = exts.map((ext) => ({ ext, ok: formats.accepts!(ext, accepted) }));
775
+ const wrong = verdicts.filter((v) => v.ok === false).map((v) => v.ext);
776
+ if (wrong.length > 0) {
777
+ problems.push(
778
+ `input slot "${input.slot}" takes ${orList(accepted)} (or a subtype), but ${what} ` +
779
+ `${input.src === "hdca" ? "holds" : "is"} ${orList(wrong)}. Give it one in a format ` +
780
+ `the slot takes; if the data is right but mislabeled, change its datatype in Galaxy first`,
781
+ );
782
+ } else if (verdicts.some((v) => v.ok === undefined)) {
783
+ const unsure = verdicts.filter((v) => v.ok === undefined).map((v) => v.ext);
784
+ unchecked.push(
785
+ `${input.slot}: can't tell whether ${orList(unsure)} fits ${orList(accepted)}`,
786
+ );
787
+ }
788
+ }
789
+ return { problems, unchecked };
790
+ }
791
+
792
+ /**
793
+ * The slots a proposal maps over: a collection on a tool's one-dataset slot,
794
+ * which Galaxy runs once per element.
795
+ */
796
+ export function mappedOverSlots(proposal: Proposal, shape: TemplateShape): string[] {
797
+ if (proposal.target.kind === "workflow") return [];
798
+ return proposal.inputs
799
+ .filter((i) => {
800
+ const slot = slotFor(shape, i.slot);
801
+ if (i.src !== "hdca" || !slot) return false;
802
+ // When the branch can't be pinned down, say it may run per element.
803
+ return variantsFor(proposal, shape, slot, i.slot).some((v) => !v.multiple && !v.collection);
804
+ })
805
+ .map((i) => i.slot);
806
+ }
807
+
808
+ /**
809
+ * Every way the proposal disagrees with the template, or an empty list.
810
+ * Checked: each input names a dataset slot the template has, with a src that
811
+ * slot takes, at most once unless it takes several; every required slot is
812
+ * filled; each override names a parameter the template has and isn't a
813
+ * dataset slot or a duplicate; a pinned version is the version fetched.
814
+ */
815
+ export function validateProposal(proposal: Proposal, shape: TemplateShape): string[] {
816
+ const problems: string[] = [];
817
+ const pinned = proposal.target.version !== UNPINNED;
818
+ if (pinned && proposal.target.version !== shape.version) {
819
+ problems.push(
820
+ `version ${proposal.target.version} was asked for but Galaxy has ${shape.version || "no version"}`,
821
+ );
822
+ }
823
+
824
+ // Counted by the name as given (two repeat instances are two slots) and,
825
+ // separately, by the template slot they fill, for the required check.
826
+ const filled = new Map<string, number>();
827
+ const covered = new Set<string>();
828
+ // Inputs Galaxy would drop, reported last: a selector value no branch names
829
+ // already explains why nothing applies.
830
+ const unused = new Map<string, string>();
831
+ const srcs = new Map<string, Set<string>>();
832
+ for (const input of proposal.inputs) {
833
+ const slot = slotFor(shape, input.slot);
834
+ if (!slot) {
835
+ problems.push(
836
+ `input slot "${input.slot}" is not a dataset input; slots: ${describeSlots(shape)}`,
837
+ );
838
+ continue;
839
+ }
840
+ // Branches can disagree; only what every applicable one refuses is refused.
841
+ const variants = variantsFor(proposal, shape, slot, input.slot);
842
+ if (variants.length === 0) {
843
+ unused.set(
844
+ input.slot,
845
+ `input slot "${input.slot}" isn't used with the branch these choices pick ` +
846
+ `(overrides, or the selectors' defaults); ${slotBranches(slot)}`,
847
+ );
848
+ continue;
849
+ }
850
+ if (variants.every((v) => v.collection) && input.src !== "hdca") {
851
+ problems.push(
852
+ `input slot "${input.slot}" takes a collection (src hdca), not ${input.src}; ` +
853
+ `give a collection id with src hdca`,
854
+ );
855
+ } else if (
856
+ !variants.some((v) => v.collection || v.multiple) &&
857
+ input.src === "hdca" &&
858
+ proposal.target.kind === "workflow"
859
+ ) {
860
+ // A tool maps over a collection on a one-dataset slot (one job per
861
+ // element) and a `multiple` slot takes it whole; a workflow input does
862
+ // neither without a batch request the proposal can't express.
863
+ problems.push(collectionOnDatasetSlot(input.slot, shape));
864
+ }
865
+ filled.set(input.slot, (filled.get(input.slot) ?? 0) + 1);
866
+ srcs.set(input.slot, (srcs.get(input.slot) ?? new Set()).add(input.src));
867
+ covered.add(slot.name);
868
+ }
869
+ for (const [name, given] of srcs) {
870
+ // Galaxy refuses a collection beside anything else on one input.
871
+ if (given.size > 1 && given.has("hdca")) {
872
+ problems.push(
873
+ `input slot "${name}" is given both datasets and collections; Galaxy takes only ` +
874
+ `collections once one is given, so give all collections or all datasets`,
875
+ );
876
+ }
877
+ }
878
+ for (const [name, n] of filled) {
879
+ const slot = slotFor(shape, name);
880
+ const variants = slot ? variantsFor(proposal, shape, slot, name) : [];
881
+ if (n > 1 && variants.length > 0 && !variants.some((v) => v.multiple)) {
882
+ problems.push(`input slot "${name}" takes one dataset but is given ${n}`);
883
+ }
884
+ }
885
+ for (const slot of shape.slots.values()) {
886
+ if (slot.required && !covered.has(slot.name)) {
887
+ problems.push(
888
+ `required input "${slot.name}"${slot.label && slot.label !== slot.name ? ` (${slot.label})` : ""} is not given`,
889
+ );
890
+ }
891
+ }
892
+
893
+ const seen = new Set<string>();
894
+ for (const o of proposal.overrides) {
895
+ if (seen.has(o.param)) problems.push(`parameter "${o.param}" is overridden twice`);
896
+ seen.add(o.param);
897
+ if (slotFor(shape, o.param)) {
898
+ problems.push(`"${o.param}" is a dataset input; give it in inputs, not overrides`);
899
+ continue;
900
+ }
901
+ if (o.param.includes("_#") || !shape.params.has(foldRepeats(o.param, shape.repeats))) {
902
+ problems.push(`"${o.param}" is not a parameter of this ${proposalNoun(proposal)}`);
903
+ continue;
904
+ }
905
+ // A selector set to something no branch names would leave which inputs
906
+ // and formats apply unknown, so it is refused rather than guessed at.
907
+ const selector = foldRepeats(o.param, shape.repeats);
908
+ const b = shape.booleans.get(selector);
909
+ if (b && typeof o.value === "string" && o.value === b.t && !galaxyTrue(o.value)) {
910
+ problems.push(
911
+ `"${o.param}" is a boolean; give true or false (Galaxy reads "${o.value}" as false)`,
912
+ );
913
+ continue;
914
+ }
915
+ const values = shape.choices.get(selector);
916
+ const value = selectorText(shape, selector, o.value);
917
+ if (values && (value === undefined || !values.has(value))) {
918
+ problems.push(`"${o.param}" chooses a branch; it takes ${orList([...values])}`);
919
+ unused.clear();
920
+ }
921
+ }
922
+ problems.push(...unused.values());
923
+ return problems;
924
+ }
925
+
926
+ function proposalNoun(p: Proposal): string {
927
+ return p.target.kind === "udt" ? "user-defined tool" : p.target.kind;
928
+ }
929
+
930
+ // ─────────────────────────────────────────────────────────────────────────────
931
+ // Building and checking the Spec
932
+ // ─────────────────────────────────────────────────────────────────────────────
933
+
934
+ /** What approval fixed beyond the proposal itself. */
935
+ export interface FrozenParts {
936
+ version: string;
937
+ templateRef: Spec["template_ref"];
938
+ definitionDigest?: string;
939
+ required: (slot: string) => boolean;
940
+ assertions: Spec["assertions"];
941
+ }
942
+
943
+ /** The Spec a proposal amounts to, given what approval froze. */
944
+ export function buildSpec(proposal: Proposal, frozen: FrozenParts): Spec {
945
+ const field = targetIdField(proposal.target.kind);
946
+ const target: Spec["target"] = {
947
+ kind: proposal.target.kind,
948
+ [field]: targetId(proposal.target),
949
+ version: frozen.version,
950
+ };
951
+ if (frozen.definitionDigest !== undefined) target.definition_digest = frozen.definitionDigest;
952
+ return {
953
+ target,
954
+ server_url: normalizeServerUrl(proposal.serverUrl),
955
+ history_id: proposal.historyId,
956
+ inputs: proposal.inputs.map((i) => ({ ...i, required: frozen.required(i.slot) })),
957
+ overrides: proposal.overrides.map((o) => ({ ...o })),
958
+ template_ref: { ...frozen.templateRef },
959
+ predicate: structuredClone(proposal.predicate),
960
+ assertions: frozen.assertions.map((a) => structuredClone(a)),
961
+ };
962
+ }
963
+
964
+ /**
965
+ * Whether a proposal, read as it is now, still amounts to the approved Spec
966
+ * and is still bound to the same step. A pinned version must be the frozen
967
+ * one; "unpinned" means "whatever approval resolved", so it matches. A label
968
+ * isn't part of what runs, so changing it changes nothing.
969
+ */
970
+ export function proposalStillMatches(proposal: Proposal, approved: Attempt): boolean {
971
+ const approval = approved.approval;
972
+ if (!approval) return false;
973
+ if (proposal.stepAnchor !== approved.binding.step_anchor) return false;
974
+ const spec = approval.spec_snapshot;
975
+ if (proposal.target.version !== UNPINNED && proposal.target.version !== spec.target.version) {
976
+ return false;
977
+ }
978
+ const required = new Map(spec.inputs.map((i) => [i.slot, i.required]));
979
+ const frozenAssertions = new Map(spec.assertions.map((a) => [a.id, a]));
980
+ const assertions = proposal.assertions.map(
981
+ (id) => frozenAssertions.get(id) ?? { id, definition_digest: "", definition: null },
982
+ );
983
+ let rebuilt: Spec;
984
+ try {
985
+ rebuilt = buildSpec(proposal, {
986
+ version: spec.target.version,
987
+ templateRef: spec.template_ref,
988
+ definitionDigest: spec.target.definition_digest,
989
+ required: (slot) => required.get(slot) ?? false,
990
+ assertions,
991
+ });
992
+ return specRevision(rebuilt) === approval.spec_revision;
993
+ } catch {
994
+ return false;
995
+ }
996
+ }
997
+
998
+ // ─────────────────────────────────────────────────────────────────────────────
999
+ // Registry operations
1000
+ // ─────────────────────────────────────────────────────────────────────────────
1001
+
1002
+ /** Attempts carrying an approval for this proposal, oldest first. */
1003
+ export function attemptsForProposal(registry: Registry, proposalId: string): Attempt[] {
1004
+ return Object.values(registry.attempts)
1005
+ .filter((a) => a.approval?.proposal_id === proposalId)
1006
+ .sort((a, b) => (a.attempt_id < b.attempt_id ? -1 : 1));
1007
+ }
1008
+
1009
+ export function liveApproval(registry: Registry, proposalId: string): Attempt | undefined {
1010
+ return attemptsForProposal(registry, proposalId).find((a) => a.approval?.status === "live");
1011
+ }
1012
+
1013
+ export type ApproveResult =
1014
+ | {
1015
+ ok: true;
1016
+ attemptId: AttemptId;
1017
+ specRevision: string;
1018
+ spec: Spec;
1019
+ /** The same Spec was already approved and live; nothing was written. */
1020
+ unchanged: boolean;
1021
+ /** Live approvals this one replaced. */
1022
+ superseded: AttemptId[];
1023
+ }
1024
+ | { ok: false; problems: string[] };
1025
+
1026
+ export interface ApproveRequest {
1027
+ proposal: Proposal;
1028
+ /** Fetched from Galaxy just now, for this target. */
1029
+ snapshot: TemplateSnapshot;
1030
+ /** Current definitions of the assertions the proposal names, by id. */
1031
+ assertionDefinitions: Map<string, unknown>;
1032
+ /** ISO timestamp. */
1033
+ now: string;
1034
+ }
1035
+
1036
+ /**
1037
+ * Freeze and record an approval. The caller is responsible for having got
1038
+ * here from the user and nowhere else -- this function is the recording, not
1039
+ * the consent. Validates again against the freshly fetched template, so a
1040
+ * hand-written block or one validated against an older template is held to
1041
+ * what Galaxy says now.
1042
+ */
1043
+ export function approveProposal(store: RegistryStore, req: ApproveRequest): ApproveResult {
1044
+ const { snapshot, now } = req;
1045
+ const shape = templateShape(req.proposal.target.kind, snapshot);
1046
+ const proposal = req.proposal;
1047
+ const problems = validateProposal(proposal, shape);
1048
+ if (normalizeServerUrl(proposal.serverUrl) !== store.serverUrl) {
1049
+ problems.push(
1050
+ `the proposal is for ${proposal.serverUrl}, but this session is connected to ${store.serverUrl || "no Galaxy server"}`,
1051
+ );
1052
+ }
1053
+ if (!snapshot.version || snapshot.version === UNPINNED) {
1054
+ problems.push("Galaxy did not say which version it resolved");
1055
+ }
1056
+ const assertions: Spec["assertions"] = [];
1057
+ for (const id of proposal.assertions) {
1058
+ if (!req.assertionDefinitions.has(id)) {
1059
+ problems.push(`assertion "${id}" has no definition to freeze`);
1060
+ continue;
1061
+ }
1062
+ const definition = req.assertionDefinitions.get(id);
1063
+ assertions.push({ id, definition_digest: sha256Hex(canonicalJson(definition)), definition });
1064
+ }
1065
+ if (problems.length > 0) return { ok: false, problems };
1066
+
1067
+ const definition = proposal.target.kind === "udt" ? udtDefinition(snapshot.body) : null;
1068
+ const digest = store.putTemplate(snapshot.body);
1069
+ let spec: Spec;
1070
+ try {
1071
+ spec = parseSpec(
1072
+ buildSpec(proposal, {
1073
+ version: snapshot.version,
1074
+ templateRef: { digest, fetched_at: now, version: snapshot.version },
1075
+ definitionDigest: definition ? sha256Hex(canonicalJson(definition)) : undefined,
1076
+ required: (slot) => slotFor(shape, slot)?.required ?? false,
1077
+ assertions,
1078
+ }),
1079
+ );
1080
+ } catch (err) {
1081
+ return { ok: false, problems: [(err as Error).message] };
1082
+ }
1083
+ const revision = specRevision(spec);
1084
+
1085
+ const current = store.snapshot();
1086
+ const live = attemptsForProposal(current, proposal.proposalId).filter(
1087
+ (a) => a.approval?.status === "live",
1088
+ );
1089
+ // The same proposal against the same template is the same approval, even
1090
+ // though a fresh `fetched_at` makes a fresh Spec hash: compare against each
1091
+ // live approval with its own frozen template reference put back.
1092
+ const same = live.find((a) => {
1093
+ const old = a.approval?.spec_snapshot;
1094
+ if (!old || a.binding.step_anchor !== proposal.stepAnchor) return false;
1095
+ if (old.template_ref.digest !== spec.template_ref.digest) return false;
1096
+ return specRevision({ ...spec, template_ref: old.template_ref }) === a.approval?.spec_revision;
1097
+ });
1098
+ if (same) {
1099
+ return {
1100
+ ok: true,
1101
+ attemptId: same.attempt_id,
1102
+ specRevision: same.approval?.spec_revision ?? revision,
1103
+ spec: same.approval?.spec_snapshot ?? spec,
1104
+ unchanged: true,
1105
+ superseded: [],
1106
+ };
1107
+ }
1108
+
1109
+ const attemptId = store.newAttemptId();
1110
+ store.update((draft) => {
1111
+ // A re-approval starts over (v3 §5): the old live approval is revoked and
1112
+ // the new attempt inherits nothing from it.
1113
+ for (const old of live) {
1114
+ const a = draft.attempts[old.attempt_id];
1115
+ if (a?.approval) a.approval.status = "revoked";
1116
+ }
1117
+ draft.attempts[attemptId] = {
1118
+ attempt_id: attemptId,
1119
+ kind: proposal.target.kind,
1120
+ binding: { step_anchor: proposal.stepAnchor, bound_at: now },
1121
+ server_url: store.serverUrl,
1122
+ history_id: proposal.historyId,
1123
+ approval: {
1124
+ proposal_id: proposal.proposalId,
1125
+ spec_revision: revision,
1126
+ spec_snapshot: spec,
1127
+ status: "live",
1128
+ by: "user",
1129
+ at: now,
1130
+ },
1131
+ handoff_eligible: false,
1132
+ };
1133
+ });
1134
+ return {
1135
+ ok: true,
1136
+ attemptId,
1137
+ specRevision: revision,
1138
+ spec,
1139
+ unchanged: false,
1140
+ superseded: live.map((a) => a.attempt_id),
1141
+ };
1142
+ }
1143
+
1144
+ /** Revoke every live approval of a proposal. Returns the attempts revoked. */
1145
+ export function revokeProposal(store: RegistryStore, proposalId: string): AttemptId[] {
1146
+ // A restored approval can't submit anything, but a reconcile check made
1147
+ // against it can still make its run eligible, so the user can withdraw it.
1148
+ const ids = Object.values(store.snapshot().attempts)
1149
+ .filter(
1150
+ (a) =>
1151
+ a.approval?.proposal_id === proposalId &&
1152
+ (a.approval.status === "live" || a.approval.status === "restored"),
1153
+ )
1154
+ .map((a) => a.attempt_id);
1155
+ if (ids.length === 0) return [];
1156
+ store.update((draft) => {
1157
+ for (const id of ids) {
1158
+ const approval = draft.attempts[id]?.approval;
1159
+ if (approval && approval.status !== "revoked") approval.status = "revoked";
1160
+ }
1161
+ });
1162
+ return ids;
1163
+ }
1164
+
1165
+ function liveOf(registry: Registry, pick: (a: Attempt) => boolean): AttemptId[] {
1166
+ return Object.values(registry.attempts)
1167
+ .filter((a) => a.approval?.status === "live" && pick(a))
1168
+ .map((a) => a.attempt_id);
1169
+ }
1170
+
1171
+ /** Revoke these attempts' live approvals. Returns the ids it was asked to revoke. */
1172
+ export function revokeAttempts(store: RegistryStore, ids: AttemptId[]): AttemptId[] {
1173
+ if (ids.length === 0) return [];
1174
+ store.update((draft) => {
1175
+ for (const id of ids) {
1176
+ const a = draft.attempts[id];
1177
+ if (a?.approval?.status === "live") a.approval.status = "revoked";
1178
+ }
1179
+ });
1180
+ return ids;
1181
+ }
1182
+
1183
+ export type DriftReason = "edited" | "removed" | "duplicated" | "unreadable" | "step_removed";
1184
+
1185
+ export interface Drift {
1186
+ attemptId: AttemptId;
1187
+ proposalId: string;
1188
+ reason: DriftReason;
1189
+ }
1190
+
1191
+ /** What the notebook says about one proposal id. */
1192
+ export interface ProposalSighting {
1193
+ proposalId: string;
1194
+ proposal: Proposal | null;
1195
+ }
1196
+
1197
+ /**
1198
+ * Live approvals the notebook no longer backs: the proposal was edited so it
1199
+ * no longer amounts to the approved Spec, removed, duplicated (two blocks
1200
+ * claiming one id -- there is no telling which was approved), or made
1201
+ * unreadable. Pure; `revokeDrifted` applies it.
1202
+ */
1203
+ export function findDrift(
1204
+ registry: Registry,
1205
+ sightings: ProposalSighting[],
1206
+ /** Whether the approved step still exists in the notebook; omitted, it isn't checked. */
1207
+ stepExists?: (anchor: string) => boolean,
1208
+ ): Drift[] {
1209
+ const byId = new Map<string, ProposalSighting[]>();
1210
+ for (const s of sightings) byId.set(s.proposalId, [...(byId.get(s.proposalId) ?? []), s]);
1211
+ const drift: Drift[] = [];
1212
+ for (const a of Object.values(registry.attempts)) {
1213
+ const approval = a.approval;
1214
+ if (!approval || approval.status !== "live") continue;
1215
+ const seen = byId.get(approval.proposal_id) ?? [];
1216
+ const base = { attemptId: a.attempt_id, proposalId: approval.proposal_id };
1217
+ if (seen.length === 0) drift.push({ ...base, reason: "removed" });
1218
+ else if (seen.length > 1) drift.push({ ...base, reason: "duplicated" });
1219
+ else if (!seen[0].proposal) drift.push({ ...base, reason: "unreadable" });
1220
+ else if (!proposalStillMatches(seen[0].proposal, a)) drift.push({ ...base, reason: "edited" });
1221
+ else if (stepExists && !stepExists(a.binding.step_anchor)) {
1222
+ drift.push({ ...base, reason: "step_removed" });
1223
+ }
1224
+ }
1225
+ return drift;
1226
+ }
1227
+
1228
+ export function revokeDrifted(
1229
+ store: RegistryStore,
1230
+ sightings: ProposalSighting[],
1231
+ stepExists?: (anchor: string) => boolean,
1232
+ ): Drift[] {
1233
+ const drift = findDrift(store.snapshot(), sightings, stepExists);
1234
+ revokeAttempts(
1235
+ store,
1236
+ drift.map((d) => d.attemptId),
1237
+ );
1238
+ return drift;
1239
+ }
1240
+
1241
+ export type PendingState = "unapproved" | "revoked" | "restored" | "invalid";
1242
+
1243
+ /**
1244
+ * Proposals in the notebook without a live approval, in notebook order. A
1245
+ * proposal whose last approval was revoked or came back restored from an
1246
+ * import is pending again: neither counts.
1247
+ */
1248
+ export function pendingProposals(
1249
+ registry: Registry,
1250
+ sightings: ProposalSighting[],
1251
+ ): Array<{ sighting: ProposalSighting; state: PendingState }> {
1252
+ const out: Array<{ sighting: ProposalSighting; state: PendingState }> = [];
1253
+ for (const s of sightings) {
1254
+ if (!s.proposal) {
1255
+ out.push({ sighting: s, state: "invalid" });
1256
+ continue;
1257
+ }
1258
+ const attempts = attemptsForProposal(registry, s.proposalId);
1259
+ if (attempts.some((a) => a.approval?.status === "live")) continue;
1260
+ const last = attempts[attempts.length - 1]?.approval?.status;
1261
+ out.push({
1262
+ sighting: s,
1263
+ state: last === "revoked" ? "revoked" : last === "restored" ? "restored" : "unapproved",
1264
+ });
1265
+ }
1266
+ return out;
1267
+ }
1268
+
1269
+ // ─────────────────────────────────────────────────────────────────────────────
1270
+ // Rendering
1271
+ // ─────────────────────────────────────────────────────────────────────────────
1272
+
1273
+ function cell(v: string): string {
1274
+ return v.replace(/\|/g, "\\|").replace(/[\r\n]+/g, " ");
1275
+ }
1276
+
1277
+ /** A value as JSON, so `4` and `"4"` don't look alike; shortened only when asked. */
1278
+ function showValue(v: unknown, compact: boolean): string {
1279
+ const s = canonicalJson(v);
1280
+ return compact && s.length > 80 ? `${s.slice(0, 77)}...` : s;
1281
+ }
1282
+
1283
+ export function describePredicate(p: Predicate): string {
1284
+ switch (p.kind) {
1285
+ case "manual":
1286
+ return "manual (you check the result)";
1287
+ case "exists_with_ext":
1288
+ return `at least ${p.min_count ?? 1} ${p.ext} output(s)`;
1289
+ case "count_eq":
1290
+ return `exactly ${p.count}${p.ext ? ` ${p.ext}` : ""} output(s)`;
1291
+ case "assertions_pass":
1292
+ return `assertions pass: ${p.ids.join(", ")}`;
1293
+ }
1294
+ }
1295
+
1296
+ /**
1297
+ * The short markdown table the model repeats in chat and `/pending` and
1298
+ * `/approve` show. Rendered from the proposal by the harness, so what the
1299
+ * user reads is what the block says, not the model's paraphrase of it.
1300
+ */
1301
+ export function renderProposalTable(
1302
+ proposal: Proposal,
1303
+ /**
1304
+ * `compact` shortens long values, for listings. Anything shown to someone
1305
+ * about to approve must leave it off: the table is what they consent to.
1306
+ */
1307
+ opts: {
1308
+ resolvedVersion?: string;
1309
+ specRevision?: string;
1310
+ compact?: boolean;
1311
+ plain?: boolean;
1312
+ /** The template, so a map-over can say it runs once per element. */
1313
+ shape?: TemplateShape;
1314
+ } = {},
1315
+ ): string {
1316
+ const t = proposal.target;
1317
+ const version =
1318
+ opts.resolvedVersion && t.version === UNPINNED
1319
+ ? `${opts.resolvedVersion} (resolved)`
1320
+ : t.version;
1321
+ const rows: Array<[string, string]> = [
1322
+ ["Proposal", proposal.proposalId],
1323
+ ["Step", proposal.stepAnchor],
1324
+ [proposalNoun(proposal), `${targetId(t) ?? "?"} @ ${version}`],
1325
+ ["History", proposal.historyId],
1326
+ ];
1327
+ if (proposal.label) rows.splice(1, 0, ["Label", proposal.label]);
1328
+ const mapped = new Set(opts.shape ? mappedOverSlots(proposal, opts.shape) : []);
1329
+ for (const i of proposal.inputs) {
1330
+ const each = mapped.has(i.slot) ? " -- runs once per element, one job each" : "";
1331
+ rows.push([`input ${i.slot}`, `${i.src} ${i.id}${each}`]);
1332
+ }
1333
+ for (const o of proposal.overrides) {
1334
+ rows.push([
1335
+ `param ${o.param}`,
1336
+ `${showValue(o.value, opts.compact === true)} -- ${o.rationale}`,
1337
+ ]);
1338
+ }
1339
+ rows.push(["Done when", describePredicate(proposal.predicate)]);
1340
+ if (opts.specRevision) rows.push(["spec_revision", opts.specRevision.slice(0, 12)]);
1341
+ if (opts.plain) {
1342
+ // For notices and dialogs, which show text as-is: aligned columns read
1343
+ // better there than markdown pipes.
1344
+ const width = Math.max(...rows.map(([k]) => k.length));
1345
+ return rows.map(([k, v]) => `${k.padEnd(width)} ${v.replace(/[\r\n]+/g, " ")}`).join("\n");
1346
+ }
1347
+ return ["| | |", "|---|---|", ...rows.map(([k, v]) => `| ${cell(k)} | ${cell(v)} |`)].join("\n");
1348
+ }