@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,1108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enrichment: fill in what each recorded run actually was, from Galaxy.
|
|
3
|
+
*
|
|
4
|
+
* A block written at submission has an id, a label and -- for a tool run -- the
|
|
5
|
+
* tool version from Galaxy's answer. What the run used (effective parameters,
|
|
6
|
+
* input datasets) and made (outputs, with extension and dbkey) only exists in
|
|
7
|
+
* the job's own details, which are not there yet when the submission returns
|
|
8
|
+
* and are routinely unavailable when asked for later: the job is not
|
|
9
|
+
* scheduled, the server answers 5xx, the transport drops. So enrichment is a
|
|
10
|
+
* state on the block, not a one-shot: `pending`, then `complete`, or
|
|
11
|
+
* `unavailable` once a 404 or a run of failures says the answer is not coming.
|
|
12
|
+
* Never silently dropped.
|
|
13
|
+
*
|
|
14
|
+
* Per job, never per workflow. An invocation is walked to its step jobs --
|
|
15
|
+
* mapped-over steps contribute one job each, subworkflows are followed -- and
|
|
16
|
+
* every job gets its own record. Fields Galaxy did not give are `unknown`.
|
|
17
|
+
*
|
|
18
|
+
* Two facts about Galaxy's answers that decide where values come from:
|
|
19
|
+
* `GET /api/jobs/{id}?full=true` drops `tool_version` (pydantic ignores the
|
|
20
|
+
* extra key), so a tool run's version comes from the submission response
|
|
21
|
+
* seeded into the block; and its `inputs`/`outputs` are dicts keyed by the
|
|
22
|
+
* tool's slot names, holding only ids, so names, extensions and dbkeys come
|
|
23
|
+
* from one dataset lookup each.
|
|
24
|
+
*
|
|
25
|
+
* The block carries a compact summary; the full record goes to the attempt's
|
|
26
|
+
* provenance file (galaxy-provenance.ts), which refuses a block that does not
|
|
27
|
+
* own the attempt it names.
|
|
28
|
+
*
|
|
29
|
+
* Drift lives here too, because enrichment is the moment an attempt's tool
|
|
30
|
+
* versions are all known: when a completed attempt is already bound to the
|
|
31
|
+
* same step, the versions that moved are written on the new block.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import * as path from "path";
|
|
35
|
+
import { appendActivityEvent } from "./activity";
|
|
36
|
+
import {
|
|
37
|
+
GalaxyApiError,
|
|
38
|
+
INVOCATION_SCHEDULING_DONE,
|
|
39
|
+
galaxyGetDataset,
|
|
40
|
+
galaxyGetInvocation,
|
|
41
|
+
galaxyGetJobDetails,
|
|
42
|
+
getGalaxyConfig,
|
|
43
|
+
isGalaxyEncodedId,
|
|
44
|
+
sameGalaxyServer,
|
|
45
|
+
type GalaxyJobDetailsResponse,
|
|
46
|
+
type InvocationDetail,
|
|
47
|
+
} from "./galaxy-api";
|
|
48
|
+
import {
|
|
49
|
+
findJobBlocks,
|
|
50
|
+
isTerminalJobState,
|
|
51
|
+
locateJobBlock,
|
|
52
|
+
upsertJobBlock,
|
|
53
|
+
type JobYaml,
|
|
54
|
+
} from "./galaxy-job-block";
|
|
55
|
+
import {
|
|
56
|
+
ProvenanceRefusal,
|
|
57
|
+
UNKNOWN,
|
|
58
|
+
ensureAttemptRecord,
|
|
59
|
+
provenanceRelativePath,
|
|
60
|
+
readAttemptRecord,
|
|
61
|
+
noteEnrichmentRetry,
|
|
62
|
+
readAttemptRecordSync,
|
|
63
|
+
writeEnrichment,
|
|
64
|
+
type AttemptRecord,
|
|
65
|
+
type Maybe,
|
|
66
|
+
type ProvenanceDataset,
|
|
67
|
+
type ProvenanceJob,
|
|
68
|
+
} from "./galaxy-provenance";
|
|
69
|
+
import {
|
|
70
|
+
flattenErrorText,
|
|
71
|
+
type BlockDriftNote,
|
|
72
|
+
type BlockJobSummary,
|
|
73
|
+
type HarnessBlockFields,
|
|
74
|
+
} from "./harness-block-fields";
|
|
75
|
+
import {
|
|
76
|
+
NotebookChangedError,
|
|
77
|
+
findInvocationBlocks,
|
|
78
|
+
locateInvocationBlock,
|
|
79
|
+
readNotebook,
|
|
80
|
+
statNotebook,
|
|
81
|
+
upsertInvocationBlock,
|
|
82
|
+
withNotebookLock,
|
|
83
|
+
writeNotebook,
|
|
84
|
+
type InvocationYaml,
|
|
85
|
+
} from "./notebook-writer";
|
|
86
|
+
import { getNotebookPath } from "./state";
|
|
87
|
+
import { isUlid } from "./ulid";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Attempts before a block goes `unavailable`. With the backoff below that is
|
|
91
|
+
* roughly eight minutes of a job's details refusing to appear after it ended,
|
|
92
|
+
* which is well past a scheduler or metadata lag and short enough that a
|
|
93
|
+
* genuinely broken server stops costing a round trip per tick.
|
|
94
|
+
*/
|
|
95
|
+
export const MAX_ENRICHMENT_ATTEMPTS = 5;
|
|
96
|
+
const BACKOFF_BASE_MS = 30_000;
|
|
97
|
+
const BACKOFF_MAX_MS = 10 * 60_000;
|
|
98
|
+
/** How deep subworkflows are followed. Galaxy nests rarely past two. */
|
|
99
|
+
const MAX_SUBWORKFLOW_DEPTH = 4;
|
|
100
|
+
/** Concurrent Galaxy requests while enriching one block. */
|
|
101
|
+
const FETCH_CONCURRENCY = 4;
|
|
102
|
+
|
|
103
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
// Parsers (pure)
|
|
105
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
function str(value: unknown): string | undefined {
|
|
108
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A toolshed tool id carries its version as its last segment
|
|
113
|
+
* (`toolshed.g2.bx.psu.edu/repos/iuc/fastp/fastp/0.24.0+galaxy0`). The
|
|
114
|
+
* lineage is the id without it: what stays the same when the version moves,
|
|
115
|
+
* and so what drift compares on. A built-in id (`cat1`) is its own lineage.
|
|
116
|
+
*/
|
|
117
|
+
const TOOLSHED_GUID = /^(.+\/repos\/[^/]+\/[^/]+\/[^/]+)\/([^/]+)$/;
|
|
118
|
+
|
|
119
|
+
export function toolLineage(toolId: string): string {
|
|
120
|
+
return TOOLSHED_GUID.exec(toolId)?.[1] ?? toolId;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The version a toolshed guid names, or undefined for a built-in id. */
|
|
124
|
+
export function versionFromToolId(toolId: string): string | undefined {
|
|
125
|
+
return TOOLSHED_GUID.exec(toolId)?.[2];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A dataset reference out of a job's `inputs`/`outputs` map. */
|
|
129
|
+
export interface DatasetRef {
|
|
130
|
+
name: string;
|
|
131
|
+
id: string;
|
|
132
|
+
src: string | undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Read `inputs`/`outputs` from full job details. Galaxy keys them by slot
|
|
137
|
+
* name as a dict; a list of `{name, id, src}` is tolerated too, since the
|
|
138
|
+
* shape has moved before. Entries whose id is not a Galaxy id are dropped,
|
|
139
|
+
* because every id here becomes a URL path segment.
|
|
140
|
+
*/
|
|
141
|
+
export function parseDatasetRefs(raw: unknown): DatasetRef[] | null {
|
|
142
|
+
if (!raw || typeof raw !== "object") return null;
|
|
143
|
+
const entries: [string, unknown][] = Array.isArray(raw)
|
|
144
|
+
? raw.map((e, i) => [str((e as Record<string, unknown>)?.name) ?? String(i), e])
|
|
145
|
+
: Object.entries(raw as Record<string, unknown>);
|
|
146
|
+
const refs: DatasetRef[] = [];
|
|
147
|
+
for (const [name, value] of entries) {
|
|
148
|
+
if (!value || typeof value !== "object") continue;
|
|
149
|
+
const v = value as Record<string, unknown>;
|
|
150
|
+
if (!isGalaxyEncodedId(v.id)) continue;
|
|
151
|
+
refs.push({ name, id: v.id, src: str(v.src) });
|
|
152
|
+
}
|
|
153
|
+
return refs;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** One job an invocation ran, as the step listing names it. */
|
|
157
|
+
export interface InvocationStepJob {
|
|
158
|
+
jobId: string;
|
|
159
|
+
toolId?: string;
|
|
160
|
+
toolVersion?: string;
|
|
161
|
+
state?: string;
|
|
162
|
+
orderIndex?: number;
|
|
163
|
+
stepLabel?: string;
|
|
164
|
+
subworkflowInvocationId?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Every job an invocation ran: `steps[].jobs[]`, one entry per job (a
|
|
169
|
+
* mapped-over step has many), and the jobs of each subworkflow it invoked.
|
|
170
|
+
* Requires the step details view -- without it every `jobs` list is empty.
|
|
171
|
+
*/
|
|
172
|
+
export async function walkInvocationJobs(
|
|
173
|
+
invocation: InvocationDetail,
|
|
174
|
+
fetchInvocation: (id: string) => Promise<InvocationDetail>,
|
|
175
|
+
depth = 0,
|
|
176
|
+
seen: Set<string> = new Set([invocation.id]),
|
|
177
|
+
): Promise<InvocationStepJob[]> {
|
|
178
|
+
const out: InvocationStepJob[] = [];
|
|
179
|
+
for (const step of invocation.steps ?? []) {
|
|
180
|
+
const s = step as unknown as Record<string, unknown>;
|
|
181
|
+
const orderIndex = typeof s.order_index === "number" ? s.order_index : undefined;
|
|
182
|
+
const stepLabel = str(s.workflow_step_label);
|
|
183
|
+
const jobs = Array.isArray(s.jobs) ? s.jobs : [];
|
|
184
|
+
for (const job of jobs) {
|
|
185
|
+
const j = job as Record<string, unknown>;
|
|
186
|
+
if (!isGalaxyEncodedId(j.id)) continue;
|
|
187
|
+
out.push({
|
|
188
|
+
jobId: j.id,
|
|
189
|
+
toolId: str(j.tool_id),
|
|
190
|
+
toolVersion: str(j.tool_version),
|
|
191
|
+
state: str(j.state),
|
|
192
|
+
orderIndex,
|
|
193
|
+
stepLabel,
|
|
194
|
+
...(depth > 0 ? { subworkflowInvocationId: invocation.id } : {}),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const sub = s.subworkflow_invocation_id;
|
|
198
|
+
if (isGalaxyEncodedId(sub) && !seen.has(sub)) {
|
|
199
|
+
// Stopping quietly would leave the deeper jobs out of the list, and a
|
|
200
|
+
// caller filing jobs by this list would then treat them as standalone.
|
|
201
|
+
if (depth >= MAX_SUBWORKFLOW_DEPTH) {
|
|
202
|
+
throw new Error(`subworkflow ${sub} is nested deeper than ${MAX_SUBWORKFLOW_DEPTH} levels`);
|
|
203
|
+
}
|
|
204
|
+
seen.add(sub);
|
|
205
|
+
const child = await fetchInvocation(sub);
|
|
206
|
+
// Galaxy answering for some other invocation must not put that one's
|
|
207
|
+
// jobs under this run.
|
|
208
|
+
if (!child || child.id !== sub) {
|
|
209
|
+
throw new Error(`Galaxy answered for a different invocation than ${sub}`);
|
|
210
|
+
}
|
|
211
|
+
out.push(...(await walkInvocationJobs(child, fetchInvocation, depth + 1, seen)));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// A job reached twice (Galaxy repeating it across views) is one job.
|
|
215
|
+
const unique = new Map<string, InvocationStepJob>();
|
|
216
|
+
for (const job of out) if (!unique.has(job.jobId)) unique.set(job.jobId, job);
|
|
217
|
+
return [...unique.values()];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function datasetFacts(ref: DatasetRef, dataset: Record<string, unknown> | null): ProvenanceDataset {
|
|
221
|
+
const d = dataset ?? {};
|
|
222
|
+
return {
|
|
223
|
+
name: ref.name,
|
|
224
|
+
id: ref.id,
|
|
225
|
+
src: ref.src ?? UNKNOWN,
|
|
226
|
+
dataset_name: str(d.name) ?? UNKNOWN,
|
|
227
|
+
ext: str(d.extension) ?? str(d.file_ext) ?? UNKNOWN,
|
|
228
|
+
dbkey: str(d.genome_build) ?? str(d.metadata_dbkey) ?? UNKNOWN,
|
|
229
|
+
create_time: str(d.create_time) ?? UNKNOWN,
|
|
230
|
+
state: str(d.state) ?? UNKNOWN,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Where a job's version came from, given what we were handed. */
|
|
235
|
+
export function resolveToolVersion(
|
|
236
|
+
toolId: string | undefined,
|
|
237
|
+
known: string | undefined,
|
|
238
|
+
knownSource: ProvenanceJob["tool_version_source"],
|
|
239
|
+
): Pick<ProvenanceJob, "tool_version" | "tool_version_source"> {
|
|
240
|
+
if (known) return { tool_version: known, tool_version_source: knownSource };
|
|
241
|
+
const fromId = toolId ? versionFromToolId(toolId) : undefined;
|
|
242
|
+
if (fromId) return { tool_version: fromId, tool_version_source: "tool_id" };
|
|
243
|
+
return { tool_version: UNKNOWN, tool_version_source: UNKNOWN };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Build one job's provenance record from its full details and the dataset
|
|
248
|
+
* lookups. Pure, so every field's source is testable: what Galaxy said, or
|
|
249
|
+
* `unknown`, never a guess.
|
|
250
|
+
*/
|
|
251
|
+
export function buildJobRecord(
|
|
252
|
+
details: GalaxyJobDetailsResponse,
|
|
253
|
+
version: Pick<ProvenanceJob, "tool_version" | "tool_version_source">,
|
|
254
|
+
datasets: ReadonlyMap<string, Record<string, unknown> | null>,
|
|
255
|
+
step?: ProvenanceJob["step"],
|
|
256
|
+
): ProvenanceJob {
|
|
257
|
+
const inputs = parseDatasetRefs(details.inputs);
|
|
258
|
+
const outputs = parseDatasetRefs(details.outputs);
|
|
259
|
+
const collections = parseDatasetRefs(details.output_collections);
|
|
260
|
+
return {
|
|
261
|
+
job_id: details.id,
|
|
262
|
+
tool_id: str(details.tool_id) ?? UNKNOWN,
|
|
263
|
+
...version,
|
|
264
|
+
state: str(details.state) ?? UNKNOWN,
|
|
265
|
+
exit_code:
|
|
266
|
+
typeof details.exit_code === "number" || details.exit_code === null
|
|
267
|
+
? details.exit_code
|
|
268
|
+
: UNKNOWN,
|
|
269
|
+
create_time: str(details.create_time) ?? UNKNOWN,
|
|
270
|
+
update_time: str(details.update_time) ?? UNKNOWN,
|
|
271
|
+
command_version: str(details.command_version) ?? UNKNOWN,
|
|
272
|
+
params:
|
|
273
|
+
details.params && typeof details.params === "object" && !Array.isArray(details.params)
|
|
274
|
+
? details.params
|
|
275
|
+
: UNKNOWN,
|
|
276
|
+
inputs: inputs ? inputs.map((r) => datasetFacts(r, datasets.get(r.id) ?? null)) : UNKNOWN,
|
|
277
|
+
outputs: outputs ? outputs.map((r) => datasetFacts(r, datasets.get(r.id) ?? null)) : UNKNOWN,
|
|
278
|
+
output_collections: collections
|
|
279
|
+
? collections.map((r) => ({ name: r.name, id: r.id, src: r.src ?? UNKNOWN }))
|
|
280
|
+
: UNKNOWN,
|
|
281
|
+
...(step ? { step } : {}),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** A record for a job whose details Galaxy will not give us. */
|
|
286
|
+
export function unavailableJobRecord(
|
|
287
|
+
jobId: string,
|
|
288
|
+
stepJob: InvocationStepJob | undefined,
|
|
289
|
+
reason: string,
|
|
290
|
+
): ProvenanceJob {
|
|
291
|
+
return {
|
|
292
|
+
job_id: jobId,
|
|
293
|
+
tool_id: stepJob?.toolId ?? UNKNOWN,
|
|
294
|
+
...resolveToolVersion(stepJob?.toolId, stepJob?.toolVersion, "invocation_step"),
|
|
295
|
+
state: stepJob?.state ?? UNKNOWN,
|
|
296
|
+
exit_code: UNKNOWN,
|
|
297
|
+
create_time: UNKNOWN,
|
|
298
|
+
update_time: UNKNOWN,
|
|
299
|
+
command_version: UNKNOWN,
|
|
300
|
+
params: UNKNOWN,
|
|
301
|
+
inputs: UNKNOWN,
|
|
302
|
+
outputs: UNKNOWN,
|
|
303
|
+
output_collections: UNKNOWN,
|
|
304
|
+
unavailable: flattenErrorText(reason),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function known<T>(value: Maybe<T>): T | undefined {
|
|
309
|
+
return value === UNKNOWN ? undefined : (value as T);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** The compact per-job line the block carries. */
|
|
313
|
+
export function summarizeJobs(records: readonly ProvenanceJob[]): BlockJobSummary[] {
|
|
314
|
+
return records.map((r) => {
|
|
315
|
+
const outputs = r.outputs === UNKNOWN ? [] : r.outputs;
|
|
316
|
+
return {
|
|
317
|
+
jobId: r.job_id,
|
|
318
|
+
...(known(r.tool_id) ? { toolId: r.tool_id } : {}),
|
|
319
|
+
...(known(r.tool_version) ? { toolVersion: r.tool_version } : {}),
|
|
320
|
+
...(known(r.state) ? { state: r.state } : {}),
|
|
321
|
+
...(outputs.length > 0
|
|
322
|
+
? {
|
|
323
|
+
outputs: outputs.map((o) => ({
|
|
324
|
+
id: o.id,
|
|
325
|
+
...(known(o.ext) ? { ext: o.ext } : {}),
|
|
326
|
+
...(known(o.dbkey) && o.dbkey !== "?" ? { dbkey: o.dbkey } : {}),
|
|
327
|
+
})),
|
|
328
|
+
}
|
|
329
|
+
: {}),
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
335
|
+
// Drift (pure)
|
|
336
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
interface AttemptVersions {
|
|
339
|
+
attemptId: string;
|
|
340
|
+
submittedAt: string;
|
|
341
|
+
completed: boolean;
|
|
342
|
+
/**
|
|
343
|
+
* Version per tool lineage, `unknown` where the tool is known but its
|
|
344
|
+
* version is not. Null when the attempt has no record we trust at all --
|
|
345
|
+
* its baseline is unknown for every tool.
|
|
346
|
+
*/
|
|
347
|
+
versions: Map<string, string> | null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Every attempt bound to `anchor`, with the tool versions its blocks name. */
|
|
351
|
+
function attemptsOnAnchor(content: string, anchor: string): AttemptVersions[] {
|
|
352
|
+
const byAttempt = new Map<string, AttemptVersions>();
|
|
353
|
+
const blocks: (InvocationYaml | JobYaml)[] = [
|
|
354
|
+
...findInvocationBlocks(content),
|
|
355
|
+
...findJobBlocks(content),
|
|
356
|
+
];
|
|
357
|
+
for (const block of blocks) {
|
|
358
|
+
if (block.notebookAnchor !== anchor || !block.attemptId) continue;
|
|
359
|
+
const entry = byAttempt.get(block.attemptId) ?? {
|
|
360
|
+
attemptId: block.attemptId,
|
|
361
|
+
submittedAt: block.submittedAt,
|
|
362
|
+
completed: false,
|
|
363
|
+
versions: new Map<string, string>(),
|
|
364
|
+
};
|
|
365
|
+
if (block.status === "completed") entry.completed = true;
|
|
366
|
+
if (block.submittedAt < entry.submittedAt) entry.submittedAt = block.submittedAt;
|
|
367
|
+
for (const job of block.jobs ?? []) {
|
|
368
|
+
if (!job.toolId || !entry.versions) continue;
|
|
369
|
+
const lineage = toolLineage(job.toolId);
|
|
370
|
+
if (!entry.versions.has(lineage)) entry.versions.set(lineage, job.toolVersion ?? UNKNOWN);
|
|
371
|
+
}
|
|
372
|
+
byAttempt.set(block.attemptId, entry);
|
|
373
|
+
}
|
|
374
|
+
return [...byAttempt.values()];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Versions that moved between the latest earlier completed attempt on this
|
|
379
|
+
* step and this one. Versions only: the parameter diff is the registry's.
|
|
380
|
+
*
|
|
381
|
+
* An unknown version on either side is written as drift with `unknown` in
|
|
382
|
+
* place of the version, not left out: "nothing moved" and "we can't tell"
|
|
383
|
+
* are different claims, and a re-run whose baseline nobody recorded is
|
|
384
|
+
* exactly the case a reader has to be told about. A tool the earlier attempt
|
|
385
|
+
* did not run at all is not drift.
|
|
386
|
+
*/
|
|
387
|
+
export function computeDrift(
|
|
388
|
+
content: string,
|
|
389
|
+
anchor: string,
|
|
390
|
+
attemptId: string,
|
|
391
|
+
jobs: readonly BlockJobSummary[],
|
|
392
|
+
versionsOf?: (attemptId: string) => Map<string, string> | null,
|
|
393
|
+
): { against: string; drift: BlockDriftNote[] } | null {
|
|
394
|
+
if (!anchor || anchor === "unattributed") return null;
|
|
395
|
+
const attempts = attemptsOnAnchor(content, anchor);
|
|
396
|
+
if (versionsOf) {
|
|
397
|
+
// The blocks' summaries are notebook text; the baseline comes from the
|
|
398
|
+
// earlier attempt's protected record, or there is no baseline.
|
|
399
|
+
for (const a of attempts) a.versions = versionsOf(a.attemptId);
|
|
400
|
+
}
|
|
401
|
+
const self = attempts.find((a) => a.attemptId === attemptId);
|
|
402
|
+
const prior = attempts
|
|
403
|
+
.filter((a) => a.attemptId !== attemptId && a.completed)
|
|
404
|
+
.filter((a) => !self?.submittedAt || !a.submittedAt || a.submittedAt <= self.submittedAt)
|
|
405
|
+
.sort((a, b) => b.submittedAt.localeCompare(a.submittedAt))[0];
|
|
406
|
+
if (!prior) return null;
|
|
407
|
+
const drift: BlockDriftNote[] = [];
|
|
408
|
+
const seen = new Set<string>();
|
|
409
|
+
for (const job of jobs) {
|
|
410
|
+
if (!job.toolId) continue;
|
|
411
|
+
const lineage = toolLineage(job.toolId);
|
|
412
|
+
if (seen.has(lineage)) continue;
|
|
413
|
+
seen.add(lineage);
|
|
414
|
+
const to = job.toolVersion ?? UNKNOWN;
|
|
415
|
+
const from = prior.versions ? prior.versions.get(lineage) : UNKNOWN;
|
|
416
|
+
if (from === undefined) continue;
|
|
417
|
+
if (from === UNKNOWN || to === UNKNOWN || from !== to) {
|
|
418
|
+
drift.push({ toolId: lineage, from, to });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return { against: prior.attemptId, drift };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Tool versions an attempt's protected record holds, by lineage. Null for a
|
|
426
|
+
* record made from notebook text, whose versions are only as good as that text.
|
|
427
|
+
*/
|
|
428
|
+
function protectedVersions(dir: string, attemptId: string): Map<string, string> | null {
|
|
429
|
+
const record = readAttemptRecordSync(dir, attemptId);
|
|
430
|
+
if (!record || record.origin === "notebook") return null;
|
|
431
|
+
const versions = new Map<string, string>();
|
|
432
|
+
for (const job of Object.values(record.jobs)) {
|
|
433
|
+
if (job.tool_id === UNKNOWN) continue;
|
|
434
|
+
const lineage = toolLineage(job.tool_id);
|
|
435
|
+
if (!versions.has(lineage)) versions.set(lineage, job.tool_version);
|
|
436
|
+
}
|
|
437
|
+
return versions;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
441
|
+
// The pass
|
|
442
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
export interface EnrichDeps {
|
|
445
|
+
getJob: (jobId: string) => Promise<GalaxyJobDetailsResponse>;
|
|
446
|
+
getInvocation: (invocationId: string) => Promise<InvocationDetail>;
|
|
447
|
+
getDataset: (datasetId: string) => Promise<Record<string, unknown>>;
|
|
448
|
+
now: () => number;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const defaultDeps: EnrichDeps = {
|
|
452
|
+
getJob: (jobId) => galaxyGetJobDetails(jobId, undefined, { full: true }),
|
|
453
|
+
getInvocation: (id) => galaxyGetInvocation(id),
|
|
454
|
+
getDataset: (id) => galaxyGetDataset(id),
|
|
455
|
+
now: () => Date.now(),
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
/** In-memory backoff, keyed `<kind>:<id>`. The attempt count itself is on the block. */
|
|
459
|
+
const nextAttemptAt = new Map<string, number>();
|
|
460
|
+
/** Drift rows already written this session, so each attempt reports once. */
|
|
461
|
+
const driftReported = new Set<string>();
|
|
462
|
+
/**
|
|
463
|
+
* Failures counted this session, per block. The durable count is in the
|
|
464
|
+
* attempt record (mirrored on the block), but when the record cannot be read
|
|
465
|
+
* and the block cannot be written either, neither count moves -- without
|
|
466
|
+
* this, such a block would be retried on every backoff forever.
|
|
467
|
+
*/
|
|
468
|
+
const sessionFailures = new Map<string, number>();
|
|
469
|
+
/** Blocks given up on this session whose unavailable state could not be written down. */
|
|
470
|
+
const exhausted = new Set<string>();
|
|
471
|
+
|
|
472
|
+
export function resetEnrichmentState(): void {
|
|
473
|
+
nextAttemptAt.clear();
|
|
474
|
+
driftReported.clear();
|
|
475
|
+
sessionFailures.clear();
|
|
476
|
+
exhausted.clear();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function backoffMs(attempts: number): number {
|
|
480
|
+
return Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** Math.max(0, attempts - 1));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
type Candidate =
|
|
484
|
+
| { kind: "invocation"; id: string; block: InvocationYaml }
|
|
485
|
+
| { kind: "job"; id: string; block: JobYaml };
|
|
486
|
+
|
|
487
|
+
function isTerminalBlock(block: InvocationYaml | JobYaml): boolean {
|
|
488
|
+
return block.status !== "in_progress";
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Blocks waiting on enrichment that this server can answer for. */
|
|
492
|
+
export function enrichmentCandidates(content: string, serverUrl: string): Candidate[] {
|
|
493
|
+
const out: Candidate[] = [];
|
|
494
|
+
for (const block of findInvocationBlocks(content)) {
|
|
495
|
+
if (block.enrichment !== "pending" || !isTerminalBlock(block)) continue;
|
|
496
|
+
if (!sameGalaxyServer(block.galaxyServerUrl, serverUrl)) continue;
|
|
497
|
+
out.push({ kind: "invocation", id: block.invocationId, block });
|
|
498
|
+
}
|
|
499
|
+
for (const block of findJobBlocks(content)) {
|
|
500
|
+
if (block.enrichment !== "pending" || !isTerminalBlock(block)) continue;
|
|
501
|
+
if (!sameGalaxyServer(block.galaxyServerUrl, serverUrl)) continue;
|
|
502
|
+
out.push({ kind: "job", id: block.jobId, block });
|
|
503
|
+
}
|
|
504
|
+
return out;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** What one attempt at one block came to. */
|
|
508
|
+
type Outcome =
|
|
509
|
+
| { state: "complete"; jobs: ProvenanceJob[]; historyId?: string }
|
|
510
|
+
| {
|
|
511
|
+
state: "unavailable";
|
|
512
|
+
jobs: ProvenanceJob[];
|
|
513
|
+
reason: string;
|
|
514
|
+
error: string;
|
|
515
|
+
historyId?: string;
|
|
516
|
+
}
|
|
517
|
+
| { state: "retry"; error: string }
|
|
518
|
+
/** Galaxy says the run has not ended. Not a failed attempt; ask again later. */
|
|
519
|
+
| { state: "not_ready" };
|
|
520
|
+
|
|
521
|
+
const SCHEDULING_DONE = INVOCATION_SCHEDULING_DONE;
|
|
522
|
+
|
|
523
|
+
function isNotFound(err: unknown): boolean {
|
|
524
|
+
return err instanceof GalaxyApiError && (err.status === 404 || err.status === 400);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function errorText(err: unknown): string {
|
|
528
|
+
return err instanceof Error ? err.message : String(err);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function mapLimited<T, R>(items: readonly T[], fn: (item: T) => Promise<R>): Promise<R[]> {
|
|
532
|
+
const results: R[] = new Array(items.length);
|
|
533
|
+
let next = 0;
|
|
534
|
+
const workers = Array.from({ length: Math.min(FETCH_CONCURRENCY, items.length) }, async () => {
|
|
535
|
+
while (next < items.length) {
|
|
536
|
+
const index = next++;
|
|
537
|
+
results[index] = await fn(items[index]);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
await Promise.all(workers);
|
|
541
|
+
return results;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
class DatasetLookups {
|
|
545
|
+
private readonly cache = new Map<string, Record<string, unknown> | null>();
|
|
546
|
+
constructor(private readonly deps: EnrichDeps) {}
|
|
547
|
+
|
|
548
|
+
/** Look up every hda the details name. A 404 is a fact (purged); anything else throws. */
|
|
549
|
+
async load(
|
|
550
|
+
details: GalaxyJobDetailsResponse[],
|
|
551
|
+
): Promise<Map<string, Record<string, unknown> | null>> {
|
|
552
|
+
const wanted = new Set<string>();
|
|
553
|
+
for (const d of details) {
|
|
554
|
+
for (const ref of [
|
|
555
|
+
...(parseDatasetRefs(d.inputs) ?? []),
|
|
556
|
+
...(parseDatasetRefs(d.outputs) ?? []),
|
|
557
|
+
]) {
|
|
558
|
+
if ((ref.src ?? "hda") === "hda" && !this.cache.has(ref.id)) wanted.add(ref.id);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
await mapLimited([...wanted], async (id) => {
|
|
562
|
+
try {
|
|
563
|
+
this.cache.set(id, await this.deps.getDataset(id));
|
|
564
|
+
} catch (err) {
|
|
565
|
+
if (!isNotFound(err)) throw err;
|
|
566
|
+
this.cache.set(id, null);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
return this.cache;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Fetch full details and check Galaxy answered for the id we asked about. */
|
|
574
|
+
async function fetchDetails(deps: EnrichDeps, jobId: string): Promise<GalaxyJobDetailsResponse> {
|
|
575
|
+
const details = await deps.getJob(jobId);
|
|
576
|
+
if (!details || typeof details !== "object" || details.id !== jobId) {
|
|
577
|
+
throw new GalaxyApiError(404, `Galaxy answered for a different job than ${jobId}`, "");
|
|
578
|
+
}
|
|
579
|
+
return details;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* The history Galaxy says a set of jobs ran in: the one value they agree on,
|
|
584
|
+
* or a refusal when they disagree. Undefined when none of them said.
|
|
585
|
+
*/
|
|
586
|
+
function historyOf(details: GalaxyJobDetailsResponse[]): string | undefined {
|
|
587
|
+
const seen = new Set(details.map((d) => str(d.history_id)).filter((h): h is string => !!h));
|
|
588
|
+
if (seen.size > 1)
|
|
589
|
+
throw new ProvenanceRefusal(`these jobs ran in ${seen.size} different histories`);
|
|
590
|
+
return [...seen][0];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** Where a tool run's version comes from, given how its attempt was recorded. */
|
|
594
|
+
function seededVersion(
|
|
595
|
+
record: AttemptRecord,
|
|
596
|
+
block: JobYaml,
|
|
597
|
+
): { version?: string; source: ProvenanceJob["tool_version_source"] } {
|
|
598
|
+
if (record.origin === "notebook") {
|
|
599
|
+
// A record made late has no protected seed; the block's own summary is the
|
|
600
|
+
// only place a submission-time version survives, and it is labelled as such.
|
|
601
|
+
return {
|
|
602
|
+
version: block.jobs?.find((j) => j.jobId === block.jobId)?.toolVersion,
|
|
603
|
+
source: "notebook",
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
version: record.seeds?.[block.jobId]?.tool_version,
|
|
608
|
+
source: record.origin === "reconcile" ? "job_listing" : "submission",
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function enrichJob(
|
|
613
|
+
deps: EnrichDeps,
|
|
614
|
+
block: JobYaml,
|
|
615
|
+
record: AttemptRecord,
|
|
616
|
+
): Promise<Outcome> {
|
|
617
|
+
let details: GalaxyJobDetailsResponse;
|
|
618
|
+
try {
|
|
619
|
+
details = await fetchDetails(deps, block.jobId);
|
|
620
|
+
} catch (err) {
|
|
621
|
+
if (isNotFound(err)) {
|
|
622
|
+
return {
|
|
623
|
+
state: "unavailable",
|
|
624
|
+
jobs: [unavailableJobRecord(block.jobId, undefined, errorText(err))],
|
|
625
|
+
reason: "not_found",
|
|
626
|
+
error: errorText(err),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
return { state: "retry", error: errorText(err) };
|
|
630
|
+
}
|
|
631
|
+
// The block's status is notebook text; whether the job has ended is
|
|
632
|
+
// Galaxy's to say. Details taken from a running job would be recorded as
|
|
633
|
+
// complete and never revisited.
|
|
634
|
+
if (!isTerminalJobState(details.state)) return { state: "not_ready" };
|
|
635
|
+
let datasets: Map<string, Record<string, unknown> | null>;
|
|
636
|
+
try {
|
|
637
|
+
datasets = await new DatasetLookups(deps).load([details]);
|
|
638
|
+
} catch (err) {
|
|
639
|
+
return { state: "retry", error: errorText(err) };
|
|
640
|
+
}
|
|
641
|
+
const seed = seededVersion(record, block);
|
|
642
|
+
const version = resolveToolVersion(details.tool_id || block.toolId, seed.version, seed.source);
|
|
643
|
+
return {
|
|
644
|
+
state: "complete",
|
|
645
|
+
jobs: [buildJobRecord(details, version, datasets)],
|
|
646
|
+
historyId: historyOf([details]),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function enrichInvocation(deps: EnrichDeps, block: InvocationYaml): Promise<Outcome> {
|
|
651
|
+
let stepJobs: InvocationStepJob[];
|
|
652
|
+
let invocationState: string | undefined;
|
|
653
|
+
try {
|
|
654
|
+
const invocation = await deps.getInvocation(block.invocationId);
|
|
655
|
+
if (!invocation || invocation.id !== block.invocationId) {
|
|
656
|
+
throw new GalaxyApiError(404, `Galaxy answered for a different invocation`, "");
|
|
657
|
+
}
|
|
658
|
+
invocationState = invocation.state;
|
|
659
|
+
stepJobs = await walkInvocationJobs(invocation, deps.getInvocation);
|
|
660
|
+
} catch (err) {
|
|
661
|
+
if (isNotFound(err)) {
|
|
662
|
+
return { state: "unavailable", jobs: [], reason: "not_found", error: errorText(err) };
|
|
663
|
+
}
|
|
664
|
+
return { state: "retry", error: errorText(err) };
|
|
665
|
+
}
|
|
666
|
+
// Still scheduling, or a step still running: the job list is not final yet.
|
|
667
|
+
if (
|
|
668
|
+
!SCHEDULING_DONE.has(invocationState ?? "") ||
|
|
669
|
+
stepJobs.some((j) => !isTerminalJobState(j.state))
|
|
670
|
+
) {
|
|
671
|
+
return { state: "not_ready" };
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const fetched = new Map<string, GalaxyJobDetailsResponse>();
|
|
675
|
+
const missing = new Map<string, string>();
|
|
676
|
+
try {
|
|
677
|
+
await mapLimited(stepJobs, async (job) => {
|
|
678
|
+
try {
|
|
679
|
+
fetched.set(job.jobId, await fetchDetails(deps, job.jobId));
|
|
680
|
+
} catch (err) {
|
|
681
|
+
if (!isNotFound(err)) throw err;
|
|
682
|
+
missing.set(job.jobId, errorText(err));
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
} catch (err) {
|
|
686
|
+
return { state: "retry", error: errorText(err) };
|
|
687
|
+
}
|
|
688
|
+
if ([...fetched.values()].some((d) => !isTerminalJobState(d.state)))
|
|
689
|
+
return { state: "not_ready" };
|
|
690
|
+
let datasets: Map<string, Record<string, unknown> | null>;
|
|
691
|
+
try {
|
|
692
|
+
datasets = await new DatasetLookups(deps).load([...fetched.values()]);
|
|
693
|
+
} catch (err) {
|
|
694
|
+
return { state: "retry", error: errorText(err) };
|
|
695
|
+
}
|
|
696
|
+
let historyId: string | undefined;
|
|
697
|
+
try {
|
|
698
|
+
historyId = historyOf([...fetched.values()]);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
return { state: "unavailable", jobs: [], reason: "attribution", error: errorText(err) };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const records = stepJobs.map((job) => {
|
|
704
|
+
const step = {
|
|
705
|
+
order_index: job.orderIndex ?? UNKNOWN,
|
|
706
|
+
label: job.stepLabel ?? UNKNOWN,
|
|
707
|
+
...(job.subworkflowInvocationId
|
|
708
|
+
? { subworkflow_invocation_id: job.subworkflowInvocationId }
|
|
709
|
+
: {}),
|
|
710
|
+
} as ProvenanceJob["step"];
|
|
711
|
+
const details = fetched.get(job.jobId);
|
|
712
|
+
if (!details) {
|
|
713
|
+
const record = unavailableJobRecord(job.jobId, job, missing.get(job.jobId) ?? "not found");
|
|
714
|
+
return { ...record, step };
|
|
715
|
+
}
|
|
716
|
+
const version = resolveToolVersion(
|
|
717
|
+
details.tool_id || job.toolId,
|
|
718
|
+
job.toolVersion,
|
|
719
|
+
"invocation_step",
|
|
720
|
+
);
|
|
721
|
+
return buildJobRecord(details, version, datasets, step);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
if (missing.size > 0) {
|
|
725
|
+
return {
|
|
726
|
+
state: "unavailable",
|
|
727
|
+
jobs: records,
|
|
728
|
+
reason: "not_found",
|
|
729
|
+
error: `${missing.size} of ${stepJobs.length} job(s) not found on Galaxy`,
|
|
730
|
+
historyId,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
return { state: "complete", jobs: records, historyId };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* The attempt record this block writes into, created now if the submission
|
|
738
|
+
* predates the record (a block from before this module existed).
|
|
739
|
+
*
|
|
740
|
+
* When creating, a tool run's owned ids are every job block carrying the same
|
|
741
|
+
* attempt id -- a mapped run's siblings belong to one attempt, and creating it
|
|
742
|
+
* from the first block alone would lock the others out.
|
|
743
|
+
*/
|
|
744
|
+
async function attemptRecordFor(
|
|
745
|
+
analysisDir: string,
|
|
746
|
+
candidate: Candidate,
|
|
747
|
+
content: string,
|
|
748
|
+
): Promise<AttemptRecord> {
|
|
749
|
+
const attemptId = candidate.block.attemptId!;
|
|
750
|
+
const existing = await readAttemptRecord(analysisDir, attemptId);
|
|
751
|
+
if (existing) return existing;
|
|
752
|
+
const block = candidate.block;
|
|
753
|
+
return ensureAttemptRecord(analysisDir, {
|
|
754
|
+
attemptId,
|
|
755
|
+
kind: candidate.kind === "invocation" ? "invocation" : "jobs",
|
|
756
|
+
galaxyServerUrl: block.galaxyServerUrl,
|
|
757
|
+
// Nothing the block says about who submitted it or where it ran goes into
|
|
758
|
+
// a protected record: the history is filled in from Galaxy's own answer.
|
|
759
|
+
submittedBy: "unknown",
|
|
760
|
+
ids:
|
|
761
|
+
candidate.kind === "invocation"
|
|
762
|
+
? { invocation_id: candidate.id }
|
|
763
|
+
: {
|
|
764
|
+
job_ids: findJobBlocks(content)
|
|
765
|
+
.filter((j) => j.attemptId === attemptId)
|
|
766
|
+
.map((j) => j.jobId),
|
|
767
|
+
},
|
|
768
|
+
origin: "notebook",
|
|
769
|
+
createdAt: block.submittedAt || undefined,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function record(dir: string, kind: string, payload: Record<string, unknown>): void {
|
|
774
|
+
appendActivityEvent(dir, {
|
|
775
|
+
timestamp: new Date().toISOString(),
|
|
776
|
+
kind,
|
|
777
|
+
source: "galaxy-enrich",
|
|
778
|
+
payload,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Write the outcome onto the block, in one locked compare-and-swap.
|
|
784
|
+
*
|
|
785
|
+
* Re-reads the block it is about to replace and gives up if that block no
|
|
786
|
+
* longer names the same attempt: an edit that re-pointed it while we were
|
|
787
|
+
* talking to Galaxy has made this result about something else.
|
|
788
|
+
*/
|
|
789
|
+
async function persistBlock(
|
|
790
|
+
nbPath: string,
|
|
791
|
+
candidate: Candidate,
|
|
792
|
+
fields: HarnessBlockFields,
|
|
793
|
+
withDrift: boolean,
|
|
794
|
+
): Promise<{ written: boolean; drift?: { against: string; drift: BlockDriftNote[] } }> {
|
|
795
|
+
return withNotebookLock(nbPath, async () => {
|
|
796
|
+
for (let attempt = 0; ; attempt++) {
|
|
797
|
+
const stamp = await statNotebook(nbPath);
|
|
798
|
+
const content = await readNotebook(nbPath);
|
|
799
|
+
const located =
|
|
800
|
+
candidate.kind === "invocation"
|
|
801
|
+
? locateInvocationBlock(content, candidate.id)
|
|
802
|
+
: locateJobBlock(content, candidate.id);
|
|
803
|
+
const current = located.record;
|
|
804
|
+
if (!current || current.attemptId !== candidate.block.attemptId) return { written: false };
|
|
805
|
+
const dir = path.dirname(nbPath);
|
|
806
|
+
const drift =
|
|
807
|
+
withDrift && fields.jobs
|
|
808
|
+
? computeDrift(content, current.notebookAnchor, current.attemptId!, fields.jobs, (id) =>
|
|
809
|
+
protectedVersions(dir, id),
|
|
810
|
+
)
|
|
811
|
+
: null;
|
|
812
|
+
const harness: HarnessBlockFields = {
|
|
813
|
+
...fields,
|
|
814
|
+
...(drift && drift.drift.length > 0 ? { drift: drift.drift } : {}),
|
|
815
|
+
};
|
|
816
|
+
const next =
|
|
817
|
+
candidate.kind === "invocation"
|
|
818
|
+
? upsertInvocationBlock(content, current as InvocationYaml, harness)
|
|
819
|
+
: upsertJobBlock(content, current as JobYaml, harness);
|
|
820
|
+
if (next === content) return { written: false };
|
|
821
|
+
try {
|
|
822
|
+
await writeNotebook(nbPath, next, stamp ?? undefined);
|
|
823
|
+
return { written: true, ...(drift ? { drift } : {}) };
|
|
824
|
+
} catch (err) {
|
|
825
|
+
if (!(err instanceof NotebookChangedError) || attempt >= 2) throw err;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
export interface EnrichmentPassResult {
|
|
832
|
+
completed: string[];
|
|
833
|
+
retried: string[];
|
|
834
|
+
unavailable: string[];
|
|
835
|
+
skipped: number;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
let inFlightPass: Promise<EnrichmentPassResult> | null = null;
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Enrich every pending block that is due. One pass at a time; a second caller
|
|
842
|
+
* gets the pass already running. `force` ignores the backoff, for `/reconcile`
|
|
843
|
+
* -- a person asking now is a reason not to wait out the timer.
|
|
844
|
+
*/
|
|
845
|
+
export function runEnrichmentPass(
|
|
846
|
+
options: { force?: boolean; deps?: Partial<EnrichDeps> } = {},
|
|
847
|
+
): Promise<EnrichmentPassResult> {
|
|
848
|
+
if (inFlightPass) return inFlightPass;
|
|
849
|
+
inFlightPass = enrichOnce(options).finally(() => {
|
|
850
|
+
inFlightPass = null;
|
|
851
|
+
});
|
|
852
|
+
return inFlightPass;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async function enrichOnce(options: {
|
|
856
|
+
force?: boolean;
|
|
857
|
+
deps?: Partial<EnrichDeps>;
|
|
858
|
+
}): Promise<EnrichmentPassResult> {
|
|
859
|
+
const result: EnrichmentPassResult = { completed: [], retried: [], unavailable: [], skipped: 0 };
|
|
860
|
+
const nbPath = getNotebookPath();
|
|
861
|
+
const server = getGalaxyConfig()?.url;
|
|
862
|
+
if (!nbPath || !server) return result;
|
|
863
|
+
const deps: EnrichDeps = { ...defaultDeps, ...options.deps };
|
|
864
|
+
const dir = path.dirname(nbPath);
|
|
865
|
+
|
|
866
|
+
let content: string;
|
|
867
|
+
try {
|
|
868
|
+
content = await readNotebook(nbPath);
|
|
869
|
+
} catch {
|
|
870
|
+
return result;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
for (const candidate of enrichmentCandidates(content, server)) {
|
|
874
|
+
const key = `${candidate.kind}:${candidate.id}`;
|
|
875
|
+
if (exhausted.has(key)) {
|
|
876
|
+
result.skipped++;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (!options.force && (nextAttemptAt.get(key) ?? 0) > deps.now()) {
|
|
880
|
+
result.skipped++;
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
const block = candidate.block;
|
|
884
|
+
// Provisional until the record is open: the record's count is the one that
|
|
885
|
+
// decides, and the block's is only display.
|
|
886
|
+
let attempts = (block.enrichmentAttempts ?? 0) + 1;
|
|
887
|
+
let counted: AttemptRecord | null = null;
|
|
888
|
+
const rowBase = {
|
|
889
|
+
block_kind: candidate.kind,
|
|
890
|
+
id: candidate.id,
|
|
891
|
+
attempt_id: block.attemptId ?? null,
|
|
892
|
+
step_anchor: block.notebookAnchor || null,
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
const giveUp = async (
|
|
896
|
+
reason: string,
|
|
897
|
+
error: string,
|
|
898
|
+
jobs: ProvenanceJob[] = [],
|
|
899
|
+
historyId?: string,
|
|
900
|
+
) => {
|
|
901
|
+
let provenance: string | null = null;
|
|
902
|
+
if (jobs.length > 0 && block.attemptId && isUlid(block.attemptId)) {
|
|
903
|
+
try {
|
|
904
|
+
await writeEnrichment(dir, block.attemptId, {
|
|
905
|
+
blockKind: candidate.kind,
|
|
906
|
+
blockId: candidate.id,
|
|
907
|
+
state: "unavailable",
|
|
908
|
+
attempts,
|
|
909
|
+
error,
|
|
910
|
+
jobs,
|
|
911
|
+
serverUrl: server,
|
|
912
|
+
historyId,
|
|
913
|
+
});
|
|
914
|
+
provenance = provenanceRelativePath(block.attemptId);
|
|
915
|
+
} catch {
|
|
916
|
+
// The block still says why; the partial record is a bonus.
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const persisted = await persistBlock(
|
|
920
|
+
nbPath,
|
|
921
|
+
candidate,
|
|
922
|
+
{
|
|
923
|
+
enrichment: "unavailable",
|
|
924
|
+
enrichmentAttempts: attempts,
|
|
925
|
+
enrichmentError: error,
|
|
926
|
+
// The job ids it did learn still go on the block: reconcile reads
|
|
927
|
+
// them to know those jobs belong to this run.
|
|
928
|
+
...(jobs.length > 0 ? { jobs: summarizeJobs(jobs) } : {}),
|
|
929
|
+
},
|
|
930
|
+
false,
|
|
931
|
+
).catch(() => ({ written: false }));
|
|
932
|
+
// A block that cannot take the verdict is still not retried again this
|
|
933
|
+
// session; the row says the notebook does not show it yet.
|
|
934
|
+
if (!persisted.written) exhausted.add(key);
|
|
935
|
+
nextAttemptAt.delete(key);
|
|
936
|
+
result.unavailable.push(key);
|
|
937
|
+
record(dir, "enrichment.unavailable", {
|
|
938
|
+
...rowBase,
|
|
939
|
+
reason,
|
|
940
|
+
attempts,
|
|
941
|
+
error: flattenErrorText(error),
|
|
942
|
+
...(provenance ? { provenance } : {}),
|
|
943
|
+
...(persisted.written ? {} : { block_written: false }),
|
|
944
|
+
});
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
// The attempt id names the provenance file, so a block without a readable
|
|
948
|
+
// one has nowhere to put its record. That is a fact about the block, not a
|
|
949
|
+
// transient, so it is not retried.
|
|
950
|
+
if (!block.attemptId || !isUlid(block.attemptId)) {
|
|
951
|
+
await giveUp("no_attempt_id", "the block carries no readable attempt id");
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (!isGalaxyEncodedId(candidate.id)) {
|
|
955
|
+
await giveUp("not_found", `"${candidate.id}" is not a Galaxy id`);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* A failure worth asking again about: counted on the block, logged, and
|
|
961
|
+
* backed off -- and turned into `unavailable` once the attempts run out,
|
|
962
|
+
* so a fault that never clears is never silently pending forever.
|
|
963
|
+
*/
|
|
964
|
+
const retryLater = async (error: string) => {
|
|
965
|
+
attempts = Math.max(attempts, (sessionFailures.get(key) ?? 0) + 1);
|
|
966
|
+
sessionFailures.set(key, attempts);
|
|
967
|
+
if (attempts >= MAX_ENRICHMENT_ATTEMPTS) {
|
|
968
|
+
await giveUp("attempts", error);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (counted) {
|
|
972
|
+
try {
|
|
973
|
+
await noteEnrichmentRetry(
|
|
974
|
+
dir,
|
|
975
|
+
counted.attempt_id,
|
|
976
|
+
{ blockKind: candidate.kind, blockId: candidate.id, serverUrl: server },
|
|
977
|
+
attempts,
|
|
978
|
+
flattenErrorText(error),
|
|
979
|
+
);
|
|
980
|
+
} catch (err) {
|
|
981
|
+
if (err instanceof ProvenanceRefusal) {
|
|
982
|
+
await giveUp("attribution", err.message);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
// Disk trouble: the block write below still counts it for display.
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
// The block mirrors the count; whether that write lands does not decide
|
|
989
|
+
// whether this attempt counted or when the next one is due.
|
|
990
|
+
await persistBlock(
|
|
991
|
+
nbPath,
|
|
992
|
+
candidate,
|
|
993
|
+
{ enrichmentAttempts: attempts, enrichmentError: error },
|
|
994
|
+
false,
|
|
995
|
+
).catch(() => ({ written: false }));
|
|
996
|
+
const wait = backoffMs(attempts);
|
|
997
|
+
nextAttemptAt.set(key, deps.now() + wait);
|
|
998
|
+
result.retried.push(key);
|
|
999
|
+
record(dir, "enrichment.retry", {
|
|
1000
|
+
...rowBase,
|
|
1001
|
+
attempt: attempts,
|
|
1002
|
+
next_in_ms: wait,
|
|
1003
|
+
error: flattenErrorText(error),
|
|
1004
|
+
});
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
let attemptRecord: AttemptRecord;
|
|
1008
|
+
try {
|
|
1009
|
+
attemptRecord = await attemptRecordFor(dir, candidate, content);
|
|
1010
|
+
counted = attemptRecord;
|
|
1011
|
+
attempts = (attemptRecord.enrichment.attempts_by_block?.[key] ?? 0) + 1;
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
if (err instanceof ProvenanceRefusal) {
|
|
1014
|
+
await giveUp("attribution", err.message);
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
await retryLater(`could not open the provenance record: ${errorText(err)}`);
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const outcome =
|
|
1022
|
+
candidate.kind === "invocation"
|
|
1023
|
+
? await enrichInvocation(deps, candidate.block)
|
|
1024
|
+
: await enrichJob(deps, candidate.block, attemptRecord);
|
|
1025
|
+
|
|
1026
|
+
if (outcome.state === "not_ready") {
|
|
1027
|
+
// Galaxy says it is still going. Not an attempt; look again shortly.
|
|
1028
|
+
nextAttemptAt.set(key, deps.now() + backoffMs(1));
|
|
1029
|
+
result.skipped++;
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (outcome.state === "retry") {
|
|
1034
|
+
await retryLater(outcome.error);
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (outcome.state === "unavailable") {
|
|
1039
|
+
await giveUp(outcome.reason, outcome.error, outcome.jobs, outcome.historyId);
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
try {
|
|
1044
|
+
await writeEnrichment(dir, block.attemptId, {
|
|
1045
|
+
blockKind: candidate.kind,
|
|
1046
|
+
blockId: candidate.id,
|
|
1047
|
+
state: "complete",
|
|
1048
|
+
attempts,
|
|
1049
|
+
jobs: outcome.jobs,
|
|
1050
|
+
serverUrl: server,
|
|
1051
|
+
historyId: outcome.historyId,
|
|
1052
|
+
});
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
if (err instanceof ProvenanceRefusal) {
|
|
1055
|
+
await giveUp("attribution", err.message);
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
await retryLater(`could not write the provenance record: ${errorText(err)}`);
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const jobs = summarizeJobs(outcome.jobs);
|
|
1063
|
+
let persisted: Awaited<ReturnType<typeof persistBlock>>;
|
|
1064
|
+
try {
|
|
1065
|
+
persisted = await persistBlock(
|
|
1066
|
+
nbPath,
|
|
1067
|
+
candidate,
|
|
1068
|
+
{ enrichment: "complete", enrichmentAttempts: attempts, enrichmentError: "", jobs },
|
|
1069
|
+
true,
|
|
1070
|
+
);
|
|
1071
|
+
} catch {
|
|
1072
|
+
// The provenance file landed; the block catches up on the next pass.
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
if (!persisted.written) continue;
|
|
1076
|
+
nextAttemptAt.delete(key);
|
|
1077
|
+
result.completed.push(key);
|
|
1078
|
+
record(dir, "enrichment.complete", {
|
|
1079
|
+
...rowBase,
|
|
1080
|
+
attempts,
|
|
1081
|
+
job_count: outcome.jobs.length,
|
|
1082
|
+
tool_versions_unknown: outcome.jobs.filter((j) => j.tool_version === UNKNOWN).length,
|
|
1083
|
+
provenance: provenanceRelativePath(block.attemptId),
|
|
1084
|
+
});
|
|
1085
|
+
const drift = persisted.drift;
|
|
1086
|
+
if (drift && drift.drift.length > 0 && !driftReported.has(block.attemptId)) {
|
|
1087
|
+
driftReported.add(block.attemptId);
|
|
1088
|
+
const wire = (d: BlockDriftNote) => ({ tool_id: d.toolId, from: d.from, to: d.to });
|
|
1089
|
+
const moved = drift.drift.filter((d) => d.from !== UNKNOWN && d.to !== UNKNOWN);
|
|
1090
|
+
const unknown = drift.drift.filter((d) => d.from === UNKNOWN || d.to === UNKNOWN);
|
|
1091
|
+
if (moved.length > 0) {
|
|
1092
|
+
record(dir, "drift.detected", {
|
|
1093
|
+
...rowBase,
|
|
1094
|
+
against_attempt_id: drift.against,
|
|
1095
|
+
drift: moved.map(wire),
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
if (unknown.length > 0) {
|
|
1099
|
+
record(dir, "drift.unknown", {
|
|
1100
|
+
...rowBase,
|
|
1101
|
+
against_attempt_id: drift.against,
|
|
1102
|
+
drift: unknown.map(wire),
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return result;
|
|
1108
|
+
}
|