@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.
- package/CHANGELOG.md +123 -0
- package/LICENSE +21 -0
- package/README.md +760 -5
- package/bin/legacy-mcp-config.js +363 -0
- package/bin/orbit.js +810 -0
- package/bin/pi-extension-path.d.ts +4 -0
- package/bin/pi-extension-path.js +44 -0
- package/bin/provider-auth.d.ts +22 -0
- package/bin/provider-auth.js +77 -0
- package/bin/thinking-pref.js +38 -0
- package/bin/update-check.d.ts +15 -0
- package/bin/update-check.js +137 -0
- package/extensions/cli-update/index.ts +32 -0
- package/extensions/orbit/activity-hooks.ts +141 -0
- package/extensions/orbit/activity.ts +116 -0
- package/extensions/orbit/agent-dir.ts +11 -0
- package/extensions/orbit/auto-resume.ts +352 -0
- package/extensions/orbit/config.ts +3 -0
- package/extensions/orbit/confusables.ts +1 -0
- package/extensions/orbit/context.ts +957 -0
- package/extensions/orbit/dashboard-commands.ts +191 -0
- package/extensions/orbit/dashboard-store.ts +473 -0
- package/extensions/orbit/dashboard-tools.ts +1185 -0
- package/extensions/orbit/empty-completion.ts +99 -0
- package/extensions/orbit/evidence-gate.ts +1276 -0
- package/extensions/orbit/evidence-override-command.ts +294 -0
- package/extensions/orbit/exec-guard/bash-risk.ts +1408 -0
- package/extensions/orbit/exec-guard/gate.ts +181 -0
- package/extensions/orbit/exec-guard/guardian-config.ts +54 -0
- package/extensions/orbit/exec-guard/index.ts +1 -0
- package/extensions/orbit/exec-guard/model-tier.ts +36 -0
- package/extensions/orbit/exec-guard/path-jail.ts +54 -0
- package/extensions/orbit/exec-guard/policy.ts +342 -0
- package/extensions/orbit/exec-guard/runtime-state.ts +15 -0
- package/extensions/orbit/exec-guard/sensitive-read.ts +525 -0
- package/extensions/orbit/exec-guard/types.ts +42 -0
- package/extensions/orbit/execution-commands.ts +134 -0
- package/extensions/orbit/feedback-command.ts +81 -0
- package/extensions/orbit/feedback.ts +96 -0
- package/extensions/orbit/galaxy-api.test.ts +122 -0
- package/extensions/orbit/galaxy-api.ts +606 -0
- package/extensions/orbit/galaxy-artifact-link-guidance.ts +5 -0
- package/extensions/orbit/galaxy-destructive-gate.ts +113 -0
- package/extensions/orbit/galaxy-enrich.ts +1108 -0
- package/extensions/orbit/galaxy-failure.ts +101 -0
- package/extensions/orbit/galaxy-fixture.ts +126 -0
- package/extensions/orbit/galaxy-history-guard.ts +233 -0
- package/extensions/orbit/galaxy-job-block.ts +347 -0
- package/extensions/orbit/galaxy-job-ops.ts +288 -0
- package/extensions/orbit/galaxy-live-source.ts +929 -0
- package/extensions/orbit/galaxy-markdown-adapter.ts +248 -0
- package/extensions/orbit/galaxy-op-policy.ts +332 -0
- package/extensions/orbit/galaxy-ops-tools.ts +326 -0
- package/extensions/orbit/galaxy-output-store.ts +80 -0
- package/extensions/orbit/galaxy-output.ts +346 -0
- package/extensions/orbit/galaxy-page-binding.ts +222 -0
- package/extensions/orbit/galaxy-page-markdown-guidance.ts +15 -0
- package/extensions/orbit/galaxy-page-ops.ts +159 -0
- package/extensions/orbit/galaxy-page-sync.test.ts +417 -0
- package/extensions/orbit/galaxy-page-sync.ts +318 -0
- package/extensions/orbit/galaxy-pages-api.ts +203 -0
- package/extensions/orbit/galaxy-pages-sync.ts +409 -0
- package/extensions/orbit/galaxy-poll-guard.ts +189 -0
- package/extensions/orbit/galaxy-poller.ts +706 -0
- package/extensions/orbit/galaxy-provenance.ts +623 -0
- package/extensions/orbit/galaxy-reconcile.ts +956 -0
- package/extensions/orbit/galaxy-recovery.ts +176 -0
- package/extensions/orbit/galaxy-repeat-guard.ts +55 -0
- package/extensions/orbit/galaxy-submission-capture.ts +707 -0
- package/extensions/orbit/galaxy-submission.ts +502 -0
- package/extensions/orbit/galaxy-tool-source.ts +78 -0
- package/extensions/orbit/galaxy-tool-wrap.ts +234 -0
- package/extensions/orbit/galaxy-udt-block.ts +127 -0
- package/extensions/orbit/galaxy-upload-tus.ts +334 -0
- package/extensions/orbit/galaxy-upload.ts +300 -0
- package/extensions/orbit/galaxy-version-capture.ts +65 -0
- package/extensions/orbit/git.ts +173 -0
- package/extensions/orbit/harness-block-fields.ts +597 -0
- package/extensions/orbit/index.ts +593 -0
- package/extensions/orbit/init-gate.ts +284 -0
- package/extensions/orbit/instructions-command.ts +136 -0
- package/extensions/orbit/invocation-failure-hint.ts +57 -0
- package/extensions/orbit/iwc-plan-check.ts +94 -0
- package/extensions/orbit/lesson-command.ts +303 -0
- package/extensions/orbit/lesson-hint.ts +245 -0
- package/extensions/orbit/lesson-nudge.ts +106 -0
- package/extensions/orbit/lessons/compose.ts +177 -0
- package/extensions/orbit/lessons/counters.ts +120 -0
- package/extensions/orbit/lessons/enabled.ts +116 -0
- package/extensions/orbit/lessons/matcher.ts +346 -0
- package/extensions/orbit/lessons/paths.ts +286 -0
- package/extensions/orbit/lessons/pi-event-contract.ts +105 -0
- package/extensions/orbit/lessons/propose-replay.ts +103 -0
- package/extensions/orbit/lessons/propose-tool.ts +222 -0
- package/extensions/orbit/lessons/propose.ts +502 -0
- package/extensions/orbit/lessons/replay.ts +89 -0
- package/extensions/orbit/lessons/reproduction-index.ts +83 -0
- package/extensions/orbit/lessons/search-tool.ts +99 -0
- package/extensions/orbit/lessons/search.ts +136 -0
- package/extensions/orbit/lessons/store.ts +415 -0
- package/extensions/orbit/lessons/text.ts +36 -0
- package/extensions/orbit/lessons/types.ts +69 -0
- package/extensions/orbit/lessons/user-dir.ts +24 -0
- package/extensions/orbit/lessons/user-lesson.ts +88 -0
- package/extensions/orbit/lessons/wrapper.ts +61 -0
- package/extensions/orbit/lessons-command.ts +147 -0
- package/extensions/orbit/local-exec.ts +39 -0
- package/extensions/orbit/mcp-output.ts +179 -0
- package/extensions/orbit/mcp-servers.test.ts +24 -0
- package/extensions/orbit/mcp-servers.ts +25 -0
- package/extensions/orbit/model-script.ts +351 -0
- package/extensions/orbit/notebook-anchors.ts +342 -0
- package/extensions/orbit/notebook-writer.ts +863 -0
- package/extensions/orbit/observation-allowlists.ts +1109 -0
- package/extensions/orbit/observation-replay.ts +148 -0
- package/extensions/orbit/observation-triggers.ts +563 -0
- package/extensions/orbit/observation-ui.ts +175 -0
- package/extensions/orbit/observations-command.ts +421 -0
- package/extensions/orbit/observations-config.ts +200 -0
- package/extensions/orbit/observations.ts +1381 -0
- package/extensions/orbit/profiles.ts +270 -0
- package/extensions/orbit/progress-updates.ts +95 -0
- package/extensions/orbit/proposal-commands.ts +882 -0
- package/extensions/orbit/provenance-fs-file.ts +59 -0
- package/extensions/orbit/provenance-fs.ts +32 -0
- package/extensions/orbit/provenance-memory-fs.ts +59 -0
- package/extensions/orbit/record-store-file.ts +177 -0
- package/extensions/orbit/record-store-page.ts +77 -0
- package/extensions/orbit/record-store-seam.ts +47 -0
- package/extensions/orbit/record-store.ts +149 -0
- package/extensions/orbit/registry-carrier.ts +107 -0
- package/extensions/orbit/registry-eligibility.ts +95 -0
- package/extensions/orbit/registry-evaluation.ts +403 -0
- package/extensions/orbit/registry-evaluator.ts +519 -0
- package/extensions/orbit/registry-import.ts +91 -0
- package/extensions/orbit/registry-lock.ts +221 -0
- package/extensions/orbit/registry-memory-fs.ts +42 -0
- package/extensions/orbit/registry-page-carrier.ts +103 -0
- package/extensions/orbit/registry-proposal.ts +1348 -0
- package/extensions/orbit/registry-runtime.ts +234 -0
- package/extensions/orbit/registry-schema.ts +735 -0
- package/extensions/orbit/registry-submitter.ts +59 -0
- package/extensions/orbit/registry-templates.ts +346 -0
- package/extensions/orbit/registry.ts +476 -0
- package/extensions/orbit/sandbox/index.ts +120 -0
- package/extensions/orbit/sandbox/sandbox-bash.ts +93 -0
- package/extensions/orbit/sandbox/sandbox-config.ts +87 -0
- package/extensions/orbit/sandbox/sandbox-init-message.ts +28 -0
- package/extensions/orbit/secret-redaction.ts +247 -0
- package/extensions/orbit/session-index/cwd.ts +18 -0
- package/extensions/orbit/session-index/db.ts +56 -0
- package/extensions/orbit/session-index/indexer.ts +154 -0
- package/extensions/orbit/session-index/is-enabled.ts +17 -0
- package/extensions/orbit/session-index/parse.ts +254 -0
- package/extensions/orbit/session-index/query.ts +209 -0
- package/extensions/orbit/session-index/schema.ts +72 -0
- package/extensions/orbit/session-index/tools.ts +152 -0
- package/extensions/orbit/session-lifecycle.ts +321 -0
- package/extensions/orbit/skill-triggers.ts +159 -0
- package/extensions/orbit/skills-command.ts +48 -0
- package/extensions/orbit/skills-discovery.ts +435 -0
- package/extensions/orbit/skills.ts +84 -0
- package/extensions/orbit/sra-import-gate.ts +77 -0
- package/extensions/orbit/state.ts +265 -0
- package/extensions/orbit/submission-replay.ts +147 -0
- package/extensions/orbit/sync-command.ts +157 -0
- package/extensions/orbit/teams/critic-parser.ts +93 -0
- package/extensions/orbit/teams/dispatcher.ts +210 -0
- package/extensions/orbit/teams/is-enabled.ts +22 -0
- package/extensions/orbit/teams/tool.ts +187 -0
- package/extensions/orbit/teams/types.ts +57 -0
- package/extensions/orbit/teams/validate.ts +42 -0
- package/extensions/orbit/tester-id-command.ts +133 -0
- package/extensions/orbit/tools-sync.ts +279 -0
- package/extensions/orbit/tools.ts +1300 -0
- package/extensions/orbit/types.ts +41 -0
- package/extensions/orbit/ui-bridge.ts +55 -0
- package/extensions/orbit/ulid.ts +60 -0
- package/extensions/orbit/user-instructions.ts +388 -0
- package/extensions/orbit/vendor/skills/_catalog.json +68 -0
- package/extensions/orbit/vendor/skills/_manifest.json +1106 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/SKILL.md +112 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_provenance.json +218 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_required_tools.json +33 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/add.json +50 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-extract.json +51 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-next-step.json +42 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-validate.json +140 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/list.json +32 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/galaxy-tool-job-failure-reference.md +137 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-tool-summary.schema.json +175 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/SKILL.md +82 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_provenance.json +133 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_required_tools.json +16 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/cli/draft-validate.json +140 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/galaxy-workflow-draft-format.md +135 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/SKILL.md +84 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_provenance.json +166 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_verify.json +29 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/galaxy-workflow-testability-design.md +136 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-test-data-conventions.md +308 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/summary-galaxy-workflow.schema.json +687 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/tests-format.schema.json +7328 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/SKILL.md +127 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_provenance.json +198 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_required_tools.json +16 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/cli/convert.json +123 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/galaxy-data-flow-draft-contract.md +129 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-test-data-conventions.md +308 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/SKILL.md +73 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_provenance.json +160 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.md +43 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.yml +1250 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-tool-job-failure-reference.md +137 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/nextflow-operators-to-galaxy-collection-recipes.md +98 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/SKILL.md +174 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/_provenance.json +130 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/_required_tools.json +18 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-revisions.json +48 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-search.json +95 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-versions.json +39 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/notes/component-tool-shed-search.md +230 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/schemas/galaxy-tool-discovery.schema.json +231 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/SKILL.md +93 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_provenance.json +201 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_required_tools.json +28 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/planemo.md +44 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/validate-tests.json +40 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/galaxy-workflow-testability-design.md +136 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-test-data-conventions.md +308 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/tests-format.schema.json +7328 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/SKILL.md +81 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_provenance.json +93 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/schemas/summary-galaxy-workflow.schema.json +687 -0
- package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/SKILL.md +81 -0
- package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/_assembly.json +23 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/SKILL.md +79 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/_provenance.json +125 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/_required_tools.json +28 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/planemo.md +44 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/validate-tests.json +40 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/schemas/tests-format.schema.json +7328 -0
- package/extensions/orbit/vendor/skills/skills/LICENSE +21 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/README.md +39 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/SKILL.md +892 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/api-patterns.md +927 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/apply-rules.md +1482 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/test-patterns.md +996 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/tools.md +609 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-data-libraries/SKILL.md +170 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/README.md +162 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/SKILL.md +99 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/README.md +119 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/tool-checking.md +419 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/workflow-testing.md +617 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/galaxy-integration.md +614 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/SKILL.md +144 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/average_col3.ipynb +68 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/extract_sample_metadata.ipynb +190 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/variant_annotation.ipynb +816 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/vcp_variant_map.ipynb +443 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/README.md +165 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/galaxy_tool_checker.py +521 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/SKILL.md +128 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/gotchas.md +135 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/history-access.md +132 -0
- package/extensions/orbit/vendor/skills/skills/reproduciblify/SKILL.md +146 -0
- package/extensions/orbit/vendor/skills/skills/reproduciblify/references/directives.yml +496 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/SKILL.md +245 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/01-minimal-echo.yml +27 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/02-single-data-in-out.yml +27 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/03-multiple-data-js.yml +28 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/04-select-integer-boolean.yml +41 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/05-resource-requirements.yml +25 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/06-biocontainer-tool.yml +38 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/07-inline-python-script.yml +56 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/08-configfile-script.yml +74 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/references/common-mistakes.md +76 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/references/schema-reference.md +155 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/references/templating.md +196 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/scripts/validate.py +109 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/README.md +71 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/SKILL.md +190 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/histology-staining.md +145 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/tissue-microarray-analysis.md +176 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/references/directives.md +415 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/SKILL.md +98 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_provenance.json +93 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_required_tools.json +28 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/convert.json +123 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/foundry.md +25 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/validate.json +142 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/schemas/summary-galaxy-workflow.schema.json +687 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/SKILL.md +74 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_provenance.json +71 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_required_tools.json +16 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/cli/validate.json +142 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-testability-design.md +136 -0
- package/extensions/orbit/vendor-skills.ts +283 -0
- package/extensions/orbit/workspace-state-dir.ts +46 -0
- package/extensions/orbit-handoff/index.ts +73 -0
- package/extensions/orbit-handoff/orbit-launcher.ts +142 -0
- package/extensions/whats-new/index.ts +98 -0
- package/lessons/LICENSE +17 -0
- package/lessons/README.md +117 -0
- package/lessons/SCHEMA.md +187 -0
- package/lessons/build-snapshot.mjs +311 -0
- package/lessons/data/downloaded-file-is-not-what-its-extension-says.md +61 -0
- package/lessons/galaxy-api/403-history-is-a-hard-stop.md +57 -0
- package/lessons/galaxy-api/collection-into-single-dataset-input.md +61 -0
- package/lessons/galaxy-api/connectedvalue-in-command-line.md +62 -0
- package/lessons/galaxy-api/hid-is-not-an-id.md +59 -0
- package/lessons/galaxy-api/invoke-workflow-inputs-not-params.md +60 -0
- package/lessons/galaxy-api/repeat-param-pipe-keys.md +63 -0
- package/lessons/galaxy-api/run-tool-returns-on-submit.md +61 -0
- package/lessons/galaxy-tools/reference-index-not-on-server.md +62 -0
- package/lessons/log.md +42 -0
- package/lessons/reproduction/input-population-mismatch.md +59 -0
- package/lessons/reproduction/methods-text-vs-executed-parameters.md +62 -0
- package/lessons/snapshot.json +1004 -0
- package/lessons/stats/de-contrast-direction-and-sample-labels.md +63 -0
- package/lessons/stats/na-coerced-to-zero-in-filters.md +59 -0
- package/lessons/validate.mjs +1219 -0
- package/package.json +103 -5
- package/shared/agent-prompt.d.ts +8 -0
- package/shared/agent-prompt.js +15 -0
- package/shared/approval-prompt.d.ts +4 -0
- package/shared/approval-prompt.js +61 -0
- package/shared/brain-env.d.ts +14 -0
- package/shared/brain-env.js +108 -0
- package/shared/brain-exit.d.ts +20 -0
- package/shared/brain-exit.js +149 -0
- package/shared/capability-profile.d.ts +16 -0
- package/shared/capability-profile.js +75 -0
- package/shared/config-source.d.ts +13 -0
- package/shared/config-source.js +17 -0
- package/shared/confusables.d.ts +3 -0
- package/shared/confusables.js +93 -0
- package/shared/custom-provider.d.ts +45 -0
- package/shared/custom-provider.js +169 -0
- package/shared/dashboard-contract.d.ts +123 -0
- package/shared/dashboard-contract.js +560 -0
- package/shared/dashboard-layout-store.d.ts +44 -0
- package/shared/dashboard-layout-store.js +281 -0
- package/shared/feedback-contract.d.ts +61 -0
- package/shared/feedback-contract.js +146 -0
- package/shared/galaxy-artifact-links.d.ts +30 -0
- package/shared/galaxy-artifact-links.js +166 -0
- package/shared/galaxy-credentials.d.ts +26 -0
- package/shared/galaxy-credentials.js +71 -0
- package/shared/galaxy-destructive.d.ts +21 -0
- package/shared/galaxy-destructive.js +269 -0
- package/shared/galaxy-guidance.d.ts +2 -0
- package/shared/galaxy-guidance.js +53 -0
- package/shared/galaxy-live-contract.d.ts +92 -0
- package/shared/galaxy-live-contract.js +180 -0
- package/shared/galaxy-page-cells.d.ts +2 -0
- package/shared/galaxy-page-cells.js +22 -0
- package/shared/galaxy-tool-family.d.ts +5 -0
- package/shared/galaxy-tool-family.js +27 -0
- package/shared/galaxy-url.d.ts +1 -0
- package/shared/galaxy-url.js +48 -0
- package/shared/gtn.d.ts +31 -0
- package/shared/gtn.js +253 -0
- package/shared/handled-prompt.d.ts +3 -0
- package/shared/handled-prompt.js +26 -0
- package/shared/known-provider.d.ts +11 -0
- package/shared/known-provider.js +89 -0
- package/shared/lesson-hint-marker.d.ts +7 -0
- package/shared/lesson-hint-marker.js +32 -0
- package/shared/lesson-rules.d.ts +150 -0
- package/shared/lesson-rules.js +203 -0
- package/shared/malformed-object-ids.d.ts +1 -0
- package/shared/malformed-object-ids.js +37 -0
- package/shared/markdown-sections.d.ts +10 -0
- package/shared/markdown-sections.js +118 -0
- package/shared/notebook-fences.d.ts +12 -0
- package/shared/notebook-fences.js +53 -0
- package/shared/observation-contract.d.ts +91 -0
- package/shared/observation-contract.js +544 -0
- package/shared/orbit-config.d.ts +226 -0
- package/shared/orbit-config.js +189 -0
- package/shared/orbit-env.d.ts +13 -0
- package/shared/orbit-env.js +124 -0
- package/shared/orbit-shell-contract.d.ts +74 -0
- package/shared/orbit-shell-contract.js +30 -0
- package/shared/page-content-hash.d.ts +1 -0
- package/shared/page-content-hash.js +13 -0
- package/shared/pi-default-model.d.ts +7 -0
- package/shared/pi-default-model.js +41 -0
- package/shared/prompt-blocks.d.ts +87 -0
- package/shared/prompt-blocks.js +898 -0
- package/shared/provider-auth-caps.d.ts +22 -0
- package/shared/provider-auth-caps.js +57 -0
- package/shared/random.d.ts +2 -0
- package/shared/random.js +15 -0
- package/shared/record-excerpt.d.ts +4 -0
- package/shared/record-excerpt.js +16 -0
- package/shared/redirect-guard.d.ts +36 -0
- package/shared/redirect-guard.js +235 -0
- package/shared/skills-pin.d.ts +8 -0
- package/shared/skills-pin.js +9 -0
- package/shared/skills-router.d.ts +17 -0
- package/shared/skills-router.js +63 -0
- package/shared/sra-import.d.ts +34 -0
- package/shared/sra-import.js +206 -0
- package/shared/state-dir.d.ts +21 -0
- package/shared/state-dir.js +240 -0
- package/shared/team-dispatch-contract.d.ts +28 -0
- package/shared/team-dispatch-contract.js +1 -0
- package/shared/truncate-middle.d.ts +11 -0
- package/shared/truncate-middle.js +68 -0
- package/shared/version-compare.d.ts +10 -0
- package/shared/version-compare.js +74 -0
- package/shared/whats-new.d.ts +31 -0
- package/shared/whats-new.js +83 -0
- package/shared/wsl.d.ts +10 -0
- 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
|
+
}
|