paratext-cli 0.2.4__tar.gz → 0.3.0__tar.gz
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.
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-4 genericise-the-defaults-away-from-local-inference-servers.md +1 -2
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/AGENTS.md +24 -20
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/PKG-INFO +1 -1
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/pyproject.toml +1 -1
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/__init__.py +1 -1
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/cli.py +5 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/server.py +61 -4
- paratext_cli-0.3.0/src/paratext/review/sessions.py +125 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/static/app.js +44 -135
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/static/index.html +25 -0
- paratext_cli-0.3.0/src/paratext/workshop.py +105 -0
- paratext_cli-0.3.0/tests/test_sessions.py +92 -0
- paratext_cli-0.3.0/tests/test_workshop.py +99 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/uv.lock +1 -1
- paratext_cli-0.2.4/backlog/config.yml +0 -16
- paratext_cli-0.2.4/backlog/milestones/m-0 - framework-split-and-first-release.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-1 - review-ui.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-2 - export-and-publishing.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-3 - oat-first-ui-rework.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-4 - packaging-and-pypi-release.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-5 - marc-fidelity.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-6 - test-coverage-and-onboarding.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-7 - export-v2.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-8 - review-ui-polish.md +0 -8
- paratext_cli-0.2.4/backlog/tasks/task-1 - Split-the-framework-out-of-paratext-nls-and-release-it.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-10 - Remove-the-WattTime-provider.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-11 - Make-paratext-new-self-installing.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-12 - Add-paratext-guide-so-agents-can-self-orient.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-13 - Guard-schema-prompt-and-view-drift.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-14 - Build-the-human-corrected-gold-eval-set-in-review.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-15 - Add-MARC-and-Dublin-Core-export-formats.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-16 - Ship-schema.json-with-each-exported-dataset.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-17 - Add-the-project-inspector-and-fix-sqlite-thread-safety.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-18 - Point-the-default-card-detector-at-the-published-NLS-repo.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-19 - Add-the-scope-aware-export-modal.md +0 -28
- paratext_cli-0.2.4/backlog/tasks/task-2 - Add-the-inbuilt-review-web-UI.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-20 - Move-the-review-UI-onto-Oat.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-21 - Render-entries-as-tables-read-only-and-editable.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-22 - Publish-gold-to-Hugging-Face-with-Sign-in-with-HF.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-23 - Warn-on-incomplete-provenance-without-blocking-publication.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-24 - Add-the-paratext-logo-and-favicon.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-25 - Remove-duplicated-helpers-across-the-codebase.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-26 - Split-Store-and-dataset-loading-out-of-the-review-server.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-27 - Lean-the-README-and-split-the-reference-into-docs.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-28 - Add-NOTICE-and-make-the-repository-public.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-29 - Rename-the-distribution-to-paratext-cli-and-publish-to-PyPI.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-3 - Make-the-review-server-configurable.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-30 - Make-project-discovery-work-from-any-environment.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-31 - Give-clear-errors-for-provider-and-source-misconfiguration.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-32 - Add-a-configurable-output-budget-and-provider-extra-body-passthrough.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-33 - Bring-MARC-output-up-to-ISBD-punctuation-and-leader-conventions.md +0 -28
- paratext_cli-0.2.4/backlog/tasks/task-34 - Replace-the-blind-margin-crop-with-a-content-aware-fallback.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-35 - Add-the-opt-in-AI-assistance-note-to-MARC-and-DC-export.md +0 -28
- paratext_cli-0.2.4/backlog/tasks/task-36 - Emit-only-one-main-entry-per-MARC-record.md +0 -28
- paratext_cli-0.2.4/backlog/tasks/task-37 - Extend-CLI-test-coverage-to-argparse-wiring-round-resolution-and-config-layering.md +0 -30
- paratext_cli-0.2.4/backlog/tasks/task-38 - Walk-a-real-run-from-a-fresh-scaffold.md +0 -28
- paratext_cli-0.2.4/backlog/tasks/task-39 - Add-the-paratext-dependency-when-scaffolding-into-an-existing-project.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-4 - Genericise-the-defaults-away-from-local-inference-servers.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-40 - Fix-cross-batch-accumulation-overwriting-metadata.jsonl.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-41 - Remove-the-silent-hf-default-from-format.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-42 - Add-paratext-export-db.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-43 - Decide-whether-a-gold-export-ships-as-a-held-out-test-split.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-44 - HF-export-v2-multi-image-projects-and-round-versioning-as-Hub-revisions.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-45 - Add-paratext-bench.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-46 - Review-UI-polish-flagged-stat-key-score-badge-and-verdict-toggles.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-47 - Eval-editor-reach-View-hidden-fields-and-diff-corrected-against-model-output.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-48 - Add-richer-scaffolder-field-types-for-enums-and-lists.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-49 - Extend-the-carbon-providers.md +0 -25
- paratext_cli-0.2.4/backlog/tasks/task-5 - Add-source-adapters-and-slim-the-project-layout.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-50 - Build-the-wordmark-in-a-mono-typeface.md +0 -24
- paratext_cli-0.2.4/backlog/tasks/task-51 - Decide-whether-to-pursue-the-PyPI-paratext-name.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-6 - Rename-init-to-new-and-document-every-CLI-flag.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-7 - Key-runs-and-packages-on-the-prompt-hash.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-8 - Add-paratext-export-for-publishing-a-reviewed-round.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-9 - Add-carbon-aware-scheduling.md +0 -27
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.github/workflows/ci.yml +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.github/workflows/publish.yml +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.gitignore +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/intro.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-1 split-the-framework-out-of-paratext-nls-and-release-it.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-10 remove-the-watttime-provider.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-11 make-paratext-new-self-installing.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-12 add-paratext-guide-so-agents-can-self-orient.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-13 guard-schema-prompt-and-view-drift.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-14 build-the-human-corrected-gold-eval-set-in-review.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-15 add-marc-and-dublin-core-export-formats.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-16 ship-schema-json-with-each-exported-dataset.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-17 add-the-project-inspector-and-fix-sqlite-thread-safety.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-18 point-the-default-card-detector-at-the-published-nls-repo.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-19 add-the-scope-aware-export-modal.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-2 add-the-inbuilt-review-web-ui.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-20 move-the-review-ui-onto-oat.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-21 render-entries-as-tables-read-only-and-editable.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-22 publish-gold-to-hugging-face-with-sign-in-with-hf.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-23 warn-on-incomplete-provenance-without-blocking-publication.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-24 add-the-paratext-logo-and-favicon.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-25 remove-duplicated-helpers-across-the-codebase.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-26 split-store-and-dataset-loading-out-of-the-review-server.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-27 lean-the-readme-and-split-the-reference-into-docs.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-28 add-notice-and-make-the-repository-public.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-29 rename-the-distribution-to-paratext-cli-and-publish-to-pypi.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-3 make-the-review-server-configurable.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-30 make-project-discovery-work-from-any-environment.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-31 give-clear-errors-for-provider-and-source-misconfiguration.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-32 add-a-configurable-output-budget-and-provider-extra-body-pas.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-33 bring-marc-output-up-to-isbd-punctuation-and-leader-conventi.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-34 replace-the-blind-margin-crop-with-a-content-aware-fallback.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-35 add-the-opt-in-ai-assistance-note-to-marc-and-dc-export.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-36 emit-only-one-main-entry-per-marc-record.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-37 extend-cli-test-coverage-to-argparse-wiring-round-resolution.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-38 walk-a-real-run-from-a-fresh-scaffold.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-39 add-the-paratext-dependency-when-scaffolding-into-an-existin.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-40 fix-cross-batch-accumulation-overwriting-metadata-jsonl.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-41 remove-the-silent-hf-default-from-format.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-42 add-paratext-export-db.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-43 decide-whether-a-gold-export-ships-as-a-held-out-test-split.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-44 hf-export-v2-multi-image-projects-and-round-versioning-as-hu.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-45 add-paratext-bench.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-46 review-ui-polish-flagged-stat-key-score-badge-and-verdict-to.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-47 eval-editor-reach-view-hidden-fields-and-diff-corrected-agai.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-48 add-richer-scaffolder-field-types-for-enums-and-lists.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-49 extend-the-carbon-providers.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-5 add-source-adapters-and-slim-the-project-layout.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-50 build-the-wordmark-in-a-mono-typeface.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-51 decide-whether-to-pursue-the-pypi-paratext-name.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-6 rename-init-to-new-and-document-every-cli-flag.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-7 key-runs-and-packages-on-the-prompt-hash.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-8 add-paratext-export-for-publishing-a-reviewed-round.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-9 add-carbon-aware-scheduling.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/LICENSE +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/NOTICE +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/README.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/assets/logo.png +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/assets/logo.svg +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/docs/configuration.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/docs/export.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/docs/green-scheduling.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/docs/hf-export-spec.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/docs/publishing.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/docs/scanned-cards.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/paratext.example.toml +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/AGENTS.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/carbon.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/cards.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/catalogue.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/config.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/datasets.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/extract.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/hf_export.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/inspect.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/io.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/packaging.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/paratext.example.toml +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/projects/__init__.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/projects/card_template/__init__.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/projects/card_template/prompt.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/projects/card_template/schema.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/records.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/__init__.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/hf_oauth.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/static/favicon.svg +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/static/oat.min.css +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/static/oat.min.js +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/runner.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/scaffold.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/sources.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/store.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_carbon.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_catalogue.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_cli.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_config.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_extract.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_guide.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_hf_export.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_io.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_packaging.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_project_audit.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_records.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_review.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_rounds.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_runner.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_scaffold.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_show_through.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_sources.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_verso.py +0 -0
|
@@ -4,11 +4,10 @@ title: Genericise the defaults away from local inference servers
|
|
|
4
4
|
status: done
|
|
5
5
|
horizon: now
|
|
6
6
|
flow: clear
|
|
7
|
-
priority: high
|
|
8
7
|
labels:
|
|
9
8
|
- docs
|
|
10
9
|
created: '2026-06-28'
|
|
11
|
-
updated: '2026-09-
|
|
10
|
+
updated: '2026-09-03'
|
|
12
11
|
---
|
|
13
12
|
|
|
14
13
|
Defaults naming Lemonade and LM Studio make the framework look like it only runs against one person's setup.
|
|
@@ -267,35 +267,39 @@ alongside `_api_export_catalogue`.
|
|
|
267
267
|
- **Bump `schema_version`** when a schema changes.
|
|
268
268
|
- **Commits:** short messages, no Co-Authored-By trailer.
|
|
269
269
|
|
|
270
|
-
##
|
|
270
|
+
## Tasks in this repo are public
|
|
271
271
|
|
|
272
|
-
This repository is public, and
|
|
272
|
+
This repository is public, and `.helical/` is committed with it. Task titles,
|
|
273
273
|
descriptions and notes are visible to anyone.
|
|
274
274
|
|
|
275
275
|
Keep repo tasks to engineering work. Anything naming NLS internals, staff,
|
|
276
276
|
suppliers, unannounced plans or licensing negotiations belongs in the
|
|
277
|
-
`paratext-nls`
|
|
277
|
+
`paratext-nls` board or the `admin` project instead.
|
|
278
278
|
|
|
279
|
-
<!--
|
|
280
|
-
|
|
281
|
-
<CRITICAL_INSTRUCTION>
|
|
279
|
+
<!-- HELICAL START -->
|
|
280
|
+
## Tasks
|
|
282
281
|
|
|
283
|
-
|
|
282
|
+
This project's tasks live in `.helical/tasks/` and are managed with the `helical`
|
|
283
|
+
command. Read them before starting, and record what you find.
|
|
284
284
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
285
|
+
```bash
|
|
286
|
+
helical ls -p <project> # open work here
|
|
287
|
+
helical show <ID> # one task in full
|
|
288
|
+
helical set <ID> --status doing # todo | doing | done
|
|
289
|
+
helical set <ID> --horizon next # now | next | future
|
|
290
|
+
helical set <ID> --flow waiting --blocker "why"
|
|
291
|
+
helical new "Title" -p <project> --horizon next
|
|
292
|
+
```
|
|
290
293
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
- `backlog instructions task-finalization` before checking acceptance criteria, writing final summaries, or moving tasks to terminal statuses
|
|
294
|
+
`--json` works on every command. A task carries both a **status** (how far along)
|
|
295
|
+
and a **horizon** (when it matters); the routemap across all projects is built from
|
|
296
|
+
horizons, so set one deliberately.
|
|
295
297
|
|
|
296
|
-
|
|
298
|
+
**Flow** says who can unstick a task: `waiting` sits with someone else and needs
|
|
299
|
+
chasing, `blocked` sits with us and needs a decision. Neither is accepted without
|
|
300
|
+
`--blocker` explaining it.
|
|
297
301
|
|
|
298
|
-
|
|
302
|
+
Labels come from a fixed vocabulary; `helical new --help` lists it.
|
|
299
303
|
|
|
300
|
-
|
|
301
|
-
<!--
|
|
304
|
+
Boards: <https://helical.ai.nls.uk> · read-only <https://projects.ai.nls.uk>
|
|
305
|
+
<!-- HELICAL END -->
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# the plain name on PyPI belongs to an unrelated project, so the distribution
|
|
4
4
|
# carries the `-cli` suffix.
|
|
5
5
|
name = "paratext-cli"
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.3.0"
|
|
7
7
|
description = "Modular, project-based metadata extraction from digitised library/archive collections with a multimodal model"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
# Core is pure-Python + numpy/pillow/pydantic. The [detector] extra needs
|
|
@@ -439,6 +439,7 @@ def _cmd_review(args: argparse.Namespace) -> int:
|
|
|
439
439
|
# A missing default root means "nothing run yet", not a mistake — serve
|
|
440
440
|
# anyway so Projects and the homepage guidance are reachable.
|
|
441
441
|
allow_empty=args.data_dir == REVIEW_ROOT,
|
|
442
|
+
workshop=args.workshop,
|
|
442
443
|
)
|
|
443
444
|
return 0
|
|
444
445
|
|
|
@@ -779,6 +780,10 @@ def _build_parser() -> tuple[
|
|
|
779
780
|
"nginx; use 0.0.0.0 to expose directly on the network)")
|
|
780
781
|
rv.add_argument("--db", type=Path, default=None,
|
|
781
782
|
help="Annotations SQLite path (default: <data_dir>/annotations.db)")
|
|
783
|
+
rv.add_argument("--workshop", metavar="DIR", default=None,
|
|
784
|
+
help="Workshop mode: give each browser its own workspace under DIR "
|
|
785
|
+
"(prompt, fields, rounds and verdicts), seeded with the rounds "
|
|
786
|
+
"served here")
|
|
782
787
|
rv.add_argument("--no-open", action="store_true", help="Don't open a browser")
|
|
783
788
|
rv.set_defaults(func=_cmd_review)
|
|
784
789
|
|
|
@@ -18,6 +18,7 @@ import io
|
|
|
18
18
|
import json
|
|
19
19
|
import logging
|
|
20
20
|
import webbrowser
|
|
21
|
+
from http.cookies import SimpleCookie
|
|
21
22
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
from urllib.parse import parse_qs, unquote, urlparse
|
|
@@ -32,6 +33,7 @@ from ..datasets import (
|
|
|
32
33
|
schema_history,
|
|
33
34
|
)
|
|
34
35
|
from ..store import Store, default_db_path
|
|
36
|
+
from .sessions import COOKIE_NAME, Sessions
|
|
35
37
|
|
|
36
38
|
logger = logging.getLogger(__name__)
|
|
37
39
|
|
|
@@ -67,18 +69,60 @@ _MIME = {
|
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
class Handler(BaseHTTPRequestHandler):
|
|
70
|
-
data_dir
|
|
71
|
-
|
|
72
|
+
# The single-tenant defaults. In workshop mode `data_dir` and `store` below
|
|
73
|
+
# resolve per request instead, so one container serves a room of people
|
|
74
|
+
# without them overwriting each other.
|
|
75
|
+
base_data_dir: Path
|
|
76
|
+
base_store: Store
|
|
77
|
+
sessions: Sessions | None = None
|
|
72
78
|
|
|
73
79
|
def log_message(self, *a): # quiet by default
|
|
74
80
|
pass
|
|
75
81
|
|
|
82
|
+
# -- per-request session --
|
|
83
|
+
def _session(self):
|
|
84
|
+
"""The caller's workshop session, creating one on first request. None
|
|
85
|
+
outside workshop mode, where the server is single-tenant."""
|
|
86
|
+
if self.sessions is None:
|
|
87
|
+
return None
|
|
88
|
+
if getattr(self, "_session_cache", None) is None:
|
|
89
|
+
cookie = SimpleCookie(self.headers.get("cookie") or "")
|
|
90
|
+
morsel = cookie.get(COOKIE_NAME)
|
|
91
|
+
session, created = self.sessions.get_or_create(morsel.value if morsel else None)
|
|
92
|
+
self._session_cache = session
|
|
93
|
+
self._set_cookie = created
|
|
94
|
+
return self._session_cache
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def data_dir(self) -> Path:
|
|
98
|
+
session = self._session()
|
|
99
|
+
return session.data_dir if session else type(self).base_data_dir
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def store(self) -> Store:
|
|
103
|
+
session = self._session()
|
|
104
|
+
if not session:
|
|
105
|
+
return type(self).base_store
|
|
106
|
+
if getattr(self, "_store_cache", None) is None:
|
|
107
|
+
self._store_cache = session.store()
|
|
108
|
+
return self._store_cache
|
|
109
|
+
|
|
110
|
+
def _session_headers(self) -> None:
|
|
111
|
+
"""Hand the browser its session on the response that created it."""
|
|
112
|
+
if getattr(self, "_set_cookie", False):
|
|
113
|
+
self.send_header(
|
|
114
|
+
"set-cookie",
|
|
115
|
+
f"{COOKIE_NAME}={self._session_cache.id}; Path=/; SameSite=Lax; Max-Age=86400",
|
|
116
|
+
)
|
|
117
|
+
self._set_cookie = False
|
|
118
|
+
|
|
76
119
|
# -- helpers --
|
|
77
120
|
def _json(self, obj, status=200):
|
|
78
121
|
body = json.dumps(obj).encode()
|
|
79
122
|
self.send_response(status)
|
|
80
123
|
self.send_header("content-type", "application/json")
|
|
81
124
|
self.send_header("content-length", str(len(body)))
|
|
125
|
+
self._session_headers()
|
|
82
126
|
self.end_headers()
|
|
83
127
|
self.wfile.write(body)
|
|
84
128
|
|
|
@@ -88,6 +132,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
88
132
|
self.send_header("content-length", str(len(body)))
|
|
89
133
|
for k, v in (headers or {}).items():
|
|
90
134
|
self.send_header(k, v)
|
|
135
|
+
self._session_headers()
|
|
91
136
|
self.end_headers()
|
|
92
137
|
self.wfile.write(body)
|
|
93
138
|
|
|
@@ -630,6 +675,7 @@ def serve(
|
|
|
630
675
|
host: str = "127.0.0.1",
|
|
631
676
|
db_path: Path | None = None,
|
|
632
677
|
allow_empty: bool = False,
|
|
678
|
+
workshop: Path | str | None = None,
|
|
633
679
|
) -> None:
|
|
634
680
|
"""Serve the review UI over ``data_dir``.
|
|
635
681
|
|
|
@@ -648,11 +694,20 @@ def serve(
|
|
|
648
694
|
f"or pass a dataset directory: paratext review <dir>"
|
|
649
695
|
)
|
|
650
696
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
651
|
-
Handler.
|
|
697
|
+
Handler.base_data_dir = data_dir
|
|
652
698
|
# Default the annotation store to <data_dir>/annotations.db; --db can point it
|
|
653
699
|
# elsewhere (e.g. an existing DB when swapping in for another review app).
|
|
654
|
-
Handler.
|
|
700
|
+
Handler.base_store = Store(
|
|
701
|
+
default_db_path(data_dir, Path(db_path).resolve() if db_path else None)
|
|
702
|
+
)
|
|
655
703
|
datasets = discover_datasets(data_dir)
|
|
704
|
+
# Workshop mode: every browser gets its own workspace, seeded with the rounds
|
|
705
|
+
# shipped here so an attendee's own runs sit beside the worked example.
|
|
706
|
+
Handler.sessions = (
|
|
707
|
+
Sessions(Path(workshop).resolve(), examples=[d["dir"] for d in datasets])
|
|
708
|
+
if workshop
|
|
709
|
+
else None
|
|
710
|
+
)
|
|
656
711
|
httpd = ThreadingHTTPServer((host, port), Handler)
|
|
657
712
|
# When bound to all interfaces there's no single canonical URL; show
|
|
658
713
|
# localhost for clicking and note the bind address.
|
|
@@ -663,6 +718,8 @@ def serve(
|
|
|
663
718
|
print(
|
|
664
719
|
f" data dir: {data_dir} ({len(datasets)} dataset(s): {names})"
|
|
665
720
|
)
|
|
721
|
+
if workshop:
|
|
722
|
+
print(f" workshop mode: per-session workspaces under {Handler.sessions.root}")
|
|
666
723
|
if open_browser:
|
|
667
724
|
try:
|
|
668
725
|
webbrowser.open(url)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Per-attendee workspaces for workshop mode.
|
|
2
|
+
|
|
3
|
+
A Space is one container shared by everyone looking at it, so without this a
|
|
4
|
+
room full of people editing one prompt overwrite each other. Each browser gets
|
|
5
|
+
a session: its own prompt and fields, its own rounds, its own verdicts.
|
|
6
|
+
|
|
7
|
+
Sessions are disposable. They live under a root the caller chooses (``/tmp`` in
|
|
8
|
+
a Space), and losing them costs a workshop exercise, not data.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import secrets
|
|
16
|
+
import shutil
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from ..store import Store
|
|
21
|
+
|
|
22
|
+
# A session id comes back from a cookie, i.e. from the client. It is used to
|
|
23
|
+
# build a path, so it is matched against this and rejected otherwise — never
|
|
24
|
+
# sanitised and used anyway.
|
|
25
|
+
SESSION_ID = re.compile(r"^[A-Za-z0-9_-]{16,64}$")
|
|
26
|
+
COOKIE_NAME = "pt_session"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def new_session_id() -> str:
|
|
30
|
+
return secrets.token_urlsafe(18)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def valid_session_id(sid: str | None) -> bool:
|
|
34
|
+
return bool(sid and SESSION_ID.match(sid))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Session:
|
|
39
|
+
id: str
|
|
40
|
+
dir: Path
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def data_dir(self) -> Path:
|
|
44
|
+
"""Where this attendee's rounds live — the review root for them."""
|
|
45
|
+
return self.dir / "review"
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def db_path(self) -> Path:
|
|
49
|
+
return self.dir / "annotations.db"
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def state_path(self) -> Path:
|
|
53
|
+
return self.dir / "state.json"
|
|
54
|
+
|
|
55
|
+
def store(self) -> Store:
|
|
56
|
+
return Store(self.db_path)
|
|
57
|
+
|
|
58
|
+
def read_state(self) -> dict:
|
|
59
|
+
try:
|
|
60
|
+
return json.loads(self.state_path.read_text())
|
|
61
|
+
except (OSError, json.JSONDecodeError):
|
|
62
|
+
return {}
|
|
63
|
+
|
|
64
|
+
def write_state(self, state: dict) -> None:
|
|
65
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
self.state_path.write_text(json.dumps(state, indent=1))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Sessions:
|
|
70
|
+
"""Creates and finds session workspaces under one root."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, root: Path, examples: list[Path] | None = None):
|
|
73
|
+
self.root = Path(root)
|
|
74
|
+
self.examples = list(examples or [])
|
|
75
|
+
|
|
76
|
+
def path_for(self, sid: str) -> Path:
|
|
77
|
+
return self.root / sid
|
|
78
|
+
|
|
79
|
+
def exists(self, sid: str) -> bool:
|
|
80
|
+
return valid_session_id(sid) and self.path_for(sid).is_dir()
|
|
81
|
+
|
|
82
|
+
def get(self, sid: str | None) -> Session | None:
|
|
83
|
+
if not valid_session_id(sid) or not self.path_for(sid).is_dir():
|
|
84
|
+
return None
|
|
85
|
+
return Session(id=sid, dir=self.path_for(sid))
|
|
86
|
+
|
|
87
|
+
def create(self, sid: str | None = None) -> Session:
|
|
88
|
+
sid = sid if valid_session_id(sid) else new_session_id()
|
|
89
|
+
session = Session(id=sid, dir=self.path_for(sid))
|
|
90
|
+
session.data_dir.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
self._seed_examples(session)
|
|
92
|
+
return session
|
|
93
|
+
|
|
94
|
+
def get_or_create(self, sid: str | None) -> tuple[Session, bool]:
|
|
95
|
+
"""Returns the session and whether it was just created (so the caller
|
|
96
|
+
knows to set the cookie)."""
|
|
97
|
+
found = self.get(sid)
|
|
98
|
+
if found:
|
|
99
|
+
return found, False
|
|
100
|
+
return self.create(), True
|
|
101
|
+
|
|
102
|
+
def _seed_examples(self, session: Session) -> None:
|
|
103
|
+
"""Symlink the shipped rounds in, so every attendee starts with the same
|
|
104
|
+
worked example beside their own work. Symlinked, not copied: the rounds
|
|
105
|
+
are read-only and their images are the bulk of the image."""
|
|
106
|
+
for src in self.examples:
|
|
107
|
+
dest = session.data_dir / src.name
|
|
108
|
+
if dest.exists() or not src.is_dir():
|
|
109
|
+
continue
|
|
110
|
+
try:
|
|
111
|
+
dest.symlink_to(src.resolve(), target_is_directory=True)
|
|
112
|
+
except OSError:
|
|
113
|
+
shutil.copytree(src, dest)
|
|
114
|
+
|
|
115
|
+
def reset(self, sid: str) -> bool:
|
|
116
|
+
"""Delete one session's workspace. Used by the facilitator reset."""
|
|
117
|
+
if not self.exists(sid):
|
|
118
|
+
return False
|
|
119
|
+
shutil.rmtree(self.path_for(sid))
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
def all_ids(self) -> list[str]:
|
|
123
|
+
if not self.root.is_dir():
|
|
124
|
+
return []
|
|
125
|
+
return sorted(p.name for p in self.root.iterdir() if p.is_dir())
|
|
@@ -286,8 +286,8 @@ function renderPicker() {
|
|
|
286
286
|
review round under <code>review/</code>, then reload — or point
|
|
287
287
|
<code>paratext review</code> at a directory that contains one.</p>
|
|
288
288
|
<p class="page-foot">
|
|
289
|
-
<
|
|
290
|
-
|
|
289
|
+
<code>paratext inspect</code> shows the fields, prompt and source options
|
|
290
|
+
each installed project runs with.
|
|
291
291
|
</p>`;
|
|
292
292
|
return;
|
|
293
293
|
}
|
|
@@ -339,8 +339,8 @@ function renderPicker() {
|
|
|
339
339
|
<h2>Choose a dataset to review</h2>
|
|
340
340
|
<div style="max-width:32rem;">${cards}</div>
|
|
341
341
|
<p class="page-foot">
|
|
342
|
-
<
|
|
343
|
-
|
|
342
|
+
<code>paratext inspect</code> shows the fields, prompt and source options
|
|
343
|
+
each installed project runs with.
|
|
344
344
|
</p>
|
|
345
345
|
`;
|
|
346
346
|
document.querySelectorAll("[data-dataset]").forEach((btn) => {
|
|
@@ -1140,17 +1140,45 @@ function renderFieldsPanel(rounds) {
|
|
|
1140
1140
|
</details>`;
|
|
1141
1141
|
}
|
|
1142
1142
|
|
|
1143
|
+
|
|
1144
|
+
// ── Prompt drift ──────────────────────────────────────────────────────
|
|
1145
|
+
// The one diagnostic the Projects page carried that nothing else does: does
|
|
1146
|
+
// the *installed* prompt still match what this round was run with? Silent when
|
|
1147
|
+
// no matching project is installed — reviewing a packaged round on a machine
|
|
1148
|
+
// that never ran it is normal.
|
|
1149
|
+
function renderDriftPanel(projects, schema) {
|
|
1150
|
+
const p = (projects ?? []).find((x) => x.name === schema);
|
|
1151
|
+
if (!p) return "";
|
|
1152
|
+
const lr = p.latest_round;
|
|
1153
|
+
const audit = (p.audit ?? []).length ? auditPill(p.audit) : "";
|
|
1154
|
+
if (!lr) return audit ? `<p class="mt-2">${audit}</p>` : "";
|
|
1155
|
+
const label = `round ${escapeHtml(String(lr.round))}`;
|
|
1156
|
+
if (lr.matches_installed) {
|
|
1157
|
+
return `<p class="text-light" style="font-size:.875rem;">
|
|
1158
|
+
Prompt matches the installed project ${audit}</p>`;
|
|
1159
|
+
}
|
|
1160
|
+
return `<div class="mt-2">
|
|
1161
|
+
<p class="bad"><strong>Prompt differs</strong> from ${label}
|
|
1162
|
+
(<code>${escapeHtml(lr.dataset)}</code>) — a new run will not reproduce it. ${audit}</p>
|
|
1163
|
+
<details class="mt-2"><summary>Diff — ${label} → installed</summary>
|
|
1164
|
+
<pre class="mt-2" style="font-size:.8125rem;">${renderDiff(lr.prompt, p.prompt)}</pre>
|
|
1165
|
+
</details>
|
|
1166
|
+
</div>`;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1143
1169
|
async function renderStats() {
|
|
1144
|
-
const [statsRes, tableRes, promptsRes, schemaRes] = await Promise.all([
|
|
1170
|
+
const [statsRes, tableRes, promptsRes, schemaRes, projectsRes] = await Promise.all([
|
|
1145
1171
|
fetch(api("api/stats")),
|
|
1146
1172
|
fetch(api("api/table")),
|
|
1147
1173
|
fetch(api("api/prompts")),
|
|
1148
1174
|
fetch(api("api/schema")),
|
|
1175
|
+
fetch("api/projects"),
|
|
1149
1176
|
]);
|
|
1150
1177
|
const s = await statsRes.json();
|
|
1151
1178
|
const rows = await tableRes.json();
|
|
1152
1179
|
const promptsData = await promptsRes.json();
|
|
1153
1180
|
const schemaData = await schemaRes.json();
|
|
1181
|
+
const projects = await projectsRes.json().catch(() => []);
|
|
1154
1182
|
document.getElementById("progress").textContent = "";
|
|
1155
1183
|
|
|
1156
1184
|
const badge = (v) => {
|
|
@@ -1194,11 +1222,19 @@ async function renderStats() {
|
|
|
1194
1222
|
<div class="field-row"><dt>Needs tweaks</dt><dd class="warn">${s.model.needs_tweaks}</dd></div>
|
|
1195
1223
|
<div class="field-row"><dt>Not accurate</dt><dd class="bad">${s.model.not_accurate}</dd></div>
|
|
1196
1224
|
${hasFlag ? `<div class="field-row"><dt>Flagged</dt><dd>${s.flagged_marc}</dd></div>` : ""}
|
|
1197
|
-
<div class="field-row"><dt>Eval gold set</dt><dd><strong>${s.eval_gold ?? s.model.good_enough}</strong>
|
|
1198
|
-
<small class="text-light">(${s.model.good_enough} good-enough + ${s.corrected ?? 0} human-corrected ·
|
|
1199
|
-
<a href="#/eval">build →</a>)</small></dd></div>
|
|
1200
1225
|
</dl>
|
|
1201
1226
|
|
|
1227
|
+
<div class="eval-cta">
|
|
1228
|
+
<a href="#/eval" class="button primary gold-cta">
|
|
1229
|
+
Build eval set
|
|
1230
|
+
<span class="gold-count">${s.eval_gold ?? s.model.good_enough}</span>
|
|
1231
|
+
</a>
|
|
1232
|
+
<p class="text-light eval-cta-note">${s.model.good_enough} good-enough +
|
|
1233
|
+
${s.corrected ?? 0} human-corrected — the records that ship as gold.</p>
|
|
1234
|
+
</div>
|
|
1235
|
+
|
|
1236
|
+
${renderDriftPanel(projects, s.schema)}
|
|
1237
|
+
|
|
1202
1238
|
${renderFieldsPanel(schemaData.rounds ?? [])}
|
|
1203
1239
|
|
|
1204
1240
|
${renderPromptsPanel(promptsData.prompts ?? [])}
|
|
@@ -1270,124 +1306,6 @@ async function renderStats() {
|
|
|
1270
1306
|
});
|
|
1271
1307
|
}
|
|
1272
1308
|
|
|
1273
|
-
// ── Projects (read-only inspector) ────────────────────────────────────
|
|
1274
|
-
// Describes the *installed* projects. Read-only: editing a project means
|
|
1275
|
-
// editing a Python package, and this server may be shared.
|
|
1276
|
-
// Styling is Oat's (.card / .badge / .table); only grid layout is local.
|
|
1277
|
-
function fieldTypeDetail(f) {
|
|
1278
|
-
if (f.options)
|
|
1279
|
-
return `<span class="text-light">{ ${f.options.map(escapeHtml).join(" | ")} }</span>`;
|
|
1280
|
-
if (f.item_fields)
|
|
1281
|
-
return `<span class="text-light">[ ${f.item_fields
|
|
1282
|
-
.map((i) => escapeHtml(i.key))
|
|
1283
|
-
.join(", ")} ]</span>`;
|
|
1284
|
-
return "";
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
// Audit is green almost always, so it reads as a pill. Only a failure earns
|
|
1288
|
-
// detail — and then it earns all of it.
|
|
1289
|
-
function auditPill(problems) {
|
|
1290
|
-
if (!problems.length) return `<span class="badge" data-variant="success">Audit ok</span>`;
|
|
1291
|
-
return `<span class="badge" data-variant="danger">Audit failed</span>
|
|
1292
|
-
<ul class="mt-2">${problems.map((a) => `<li class="bad">${escapeHtml(a)}</li>`).join("")}</ul>`;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
function projectCard(p) {
|
|
1296
|
-
if (p.error) {
|
|
1297
|
-
return `<article class="card proj mb-4">
|
|
1298
|
-
<h3>${escapeHtml(p.name)}</h3>
|
|
1299
|
-
<p class="bad">Failed to load: ${escapeHtml(p.error)}</p>
|
|
1300
|
-
</article>`;
|
|
1301
|
-
}
|
|
1302
|
-
const ep = p.entry_point || {};
|
|
1303
|
-
const src = p.source || {};
|
|
1304
|
-
const srcOpts = Object.entries(src)
|
|
1305
|
-
.filter(([k]) => k !== "kind" && k !== "exts")
|
|
1306
|
-
.map(([k, v]) => `${escapeHtml(k)}=${escapeHtml(String(v))}`)
|
|
1307
|
-
.join(", ");
|
|
1308
|
-
|
|
1309
|
-
const fields = (p.view.panels || [])
|
|
1310
|
-
.flatMap((panel) => panel.fields || [])
|
|
1311
|
-
.map(
|
|
1312
|
-
(f) => `<tr>
|
|
1313
|
-
<td><code>${escapeHtml(f.key)}</code></td>
|
|
1314
|
-
<td>${escapeHtml(f.type)} ${fieldTypeDetail(f)}</td>
|
|
1315
|
-
<td class="text-light">${f.collapsed ? "collapsed" : ""}</td>
|
|
1316
|
-
</tr>`,
|
|
1317
|
-
)
|
|
1318
|
-
.join("");
|
|
1319
|
-
|
|
1320
|
-
// One line per fact rather than a label/value grid — the labels were doing
|
|
1321
|
-
// less work than the space they took.
|
|
1322
|
-
const meta = [
|
|
1323
|
-
`<code>${escapeHtml(ep.value || "?")}</code>`,
|
|
1324
|
-
`${escapeHtml(ep.package || "")} ${escapeHtml(ep.version || "")}`.trim(),
|
|
1325
|
-
`${escapeHtml(src.kind || "custom")}${srcOpts ? ` (${srcOpts})` : ""}`,
|
|
1326
|
-
`images ${escapeHtml(String(p.images.max_size))}px q${escapeHtml(
|
|
1327
|
-
String(p.images.quality),
|
|
1328
|
-
)}`,
|
|
1329
|
-
`prompt <code>${escapeHtml(p.prompt_hash)}</code> · ${p.prompt.split("\n").length} lines`,
|
|
1330
|
-
]
|
|
1331
|
-
.filter(Boolean)
|
|
1332
|
-
.join(" · ");
|
|
1333
|
-
|
|
1334
|
-
const lr = p.latest_round;
|
|
1335
|
-
const roundLabel = (r) =>
|
|
1336
|
-
`round ${escapeHtml(String(r.round))}` +
|
|
1337
|
-
(r.prompt_hash ? ` <code>${escapeHtml(r.prompt_hash)}</code>` : "");
|
|
1338
|
-
let drift = "";
|
|
1339
|
-
if (lr && !lr.matches_installed) {
|
|
1340
|
-
drift = `<p class="bad mt-2"><strong>Prompt differs</strong> from ${roundLabel(lr)}
|
|
1341
|
-
(<code>${escapeHtml(lr.dataset)}</code>) — a new run will not reproduce it.</p>
|
|
1342
|
-
<details class="mt-2"><summary>Diff — ${roundLabel(lr)} → installed</summary>
|
|
1343
|
-
<pre class="mt-2" style="font-size:.8125rem;">${renderDiff(
|
|
1344
|
-
lr.prompt,
|
|
1345
|
-
p.prompt,
|
|
1346
|
-
)}</pre>
|
|
1347
|
-
</details>`;
|
|
1348
|
-
} else if (lr) {
|
|
1349
|
-
drift = `<p class="text-light mt-2">Prompt matches ${roundLabel(lr)}.</p>`;
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
return `<article class="card proj mb-4">
|
|
1353
|
-
<h3>${escapeHtml(p.name)}
|
|
1354
|
-
<span class="badge outline">${escapeHtml(p.schema_version)}</span>
|
|
1355
|
-
${auditPill(p.audit)}</h3>
|
|
1356
|
-
<p class="proj-meta text-light">${meta}</p>
|
|
1357
|
-
${drift}
|
|
1358
|
-
<div class="table"><table><thead><tr><th>Field</th><th>Type</th><th></th></tr></thead>
|
|
1359
|
-
<tbody>${fields}</tbody></table></div>
|
|
1360
|
-
<details class="mt-4"><summary>Prompt</summary>
|
|
1361
|
-
<pre class="prompt-text">${escapeHtml(p.prompt)}</pre></details>
|
|
1362
|
-
</article>`;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
async function renderProjects() {
|
|
1366
|
-
const el = document.getElementById("view");
|
|
1367
|
-
el.innerHTML = `<p class="text-light">Loading projects…</p>`;
|
|
1368
|
-
let projects;
|
|
1369
|
-
try {
|
|
1370
|
-
// Relative path, no dataset param: this endpoint is about what's installed,
|
|
1371
|
-
// and the prefix must survive being served behind /verify/.
|
|
1372
|
-
const res = await fetch("api/projects");
|
|
1373
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1374
|
-
projects = await res.json();
|
|
1375
|
-
} catch (e) {
|
|
1376
|
-
el.innerHTML = `<p class="bad">Could not load projects: ${escapeHtml(String(e))}</p>`;
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
if (!projects.length) {
|
|
1381
|
-
el.innerHTML = `<h2>Project configuration</h2>
|
|
1382
|
-
<p>No projects installed. Scaffold one with <code>paratext new</code>.</p>`;
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
el.innerHTML = `<h2>Project configuration</h2>
|
|
1387
|
-
${projects.map(projectCard).join("")}
|
|
1388
|
-
<p class="mt-4"><a href="#/select" class="button outline small">← Back to datasets</a></p>`;
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
1309
|
// ── Export modal ──────────────────────────────────────────────────────
|
|
1392
1310
|
// A native <dialog> (Oat-styled) over the /api/export/* endpoints. Format-first
|
|
1393
1311
|
// tabs; a records-scope row (verdict-coloured dots) that governs MARC/DC/JSONL;
|
|
@@ -1748,7 +1666,6 @@ async function openExportModal() {
|
|
|
1748
1666
|
function currentRoute() {
|
|
1749
1667
|
if (location.hash === "#/select") return "select";
|
|
1750
1668
|
if (location.hash === "#/stats") return "stats";
|
|
1751
|
-
if (location.hash === "#/projects") return "projects";
|
|
1752
1669
|
if (location.hash === "#/eval" || location.hash.startsWith("#/eval/")) return "eval";
|
|
1753
1670
|
return "review";
|
|
1754
1671
|
}
|
|
@@ -1762,14 +1679,6 @@ async function route() {
|
|
|
1762
1679
|
await loadDatasets();
|
|
1763
1680
|
}
|
|
1764
1681
|
|
|
1765
|
-
// Projects describes what's installed, not what's been run — so it must not
|
|
1766
|
-
// sit behind dataset selection (a fresh install has no datasets at all).
|
|
1767
|
-
if (currentRoute() === "projects") {
|
|
1768
|
-
renderHeader();
|
|
1769
|
-
await renderProjects();
|
|
1770
|
-
return;
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
1682
|
// Resolve the current dataset:
|
|
1774
1683
|
// - 0 datasets → handled by renderPicker (shows guidance).
|
|
1775
1684
|
// - 1 dataset → auto-select.
|
|
@@ -114,6 +114,31 @@
|
|
|
114
114
|
.proj-meta { margin: .25rem 0 .75rem; font-size: .8125rem; word-break: break-word; }
|
|
115
115
|
.prompt-text { white-space: pre-wrap; font-size: .8125rem; max-height: 30rem;
|
|
116
116
|
overflow-y: auto; }
|
|
117
|
+
/* Eval-set call to action. The gold set is the point of reviewing, so it
|
|
118
|
+
is a button, not a statistic. The hover ring is a conic gradient rotated
|
|
119
|
+
through a registered @property — a plain gradient can't be animated. */
|
|
120
|
+
.eval-cta { margin: 1.25rem 0 1.5rem; }
|
|
121
|
+
.eval-cta-note { font-size: var(--text-8); margin: .5rem 0 0; }
|
|
122
|
+
.gold-cta { position: relative; font-size: 1rem; padding: .6875rem 1.25rem;
|
|
123
|
+
display: inline-flex; align-items: center; gap: .625rem; }
|
|
124
|
+
.gold-cta .gold-count { font-variant-numeric: tabular-nums; font-weight: 700;
|
|
125
|
+
background: rgb(255 255 255 / .18); border-radius: var(--radius, .375rem);
|
|
126
|
+
padding: .0625rem .5rem; }
|
|
127
|
+
@property --gold-angle { syntax: "<angle>"; initial-value: 0deg; inherits: false; }
|
|
128
|
+
.gold-cta::before {
|
|
129
|
+
content: ""; position: absolute; inset: -2px; border-radius: inherit; z-index: -1;
|
|
130
|
+
background: conic-gradient(from var(--gold-angle),
|
|
131
|
+
transparent 0turn .45turn, #b8860b .62turn, #ffd76e .75turn,
|
|
132
|
+
#fff3c4 .8turn, #ffd76e .85turn, #b8860b .95turn, transparent 1turn);
|
|
133
|
+
opacity: 0; transition: opacity .18s ease;
|
|
134
|
+
}
|
|
135
|
+
.gold-cta:hover::before, .gold-cta:focus-visible::before {
|
|
136
|
+
opacity: 1; animation: gold-sweep 2.2s linear infinite;
|
|
137
|
+
}
|
|
138
|
+
@keyframes gold-sweep { to { --gold-angle: 1turn; } }
|
|
139
|
+
@media (prefers-reduced-motion: reduce) {
|
|
140
|
+
.gold-cta:hover::before, .gold-cta:focus-visible::before { animation: none; }
|
|
141
|
+
}
|
|
117
142
|
/* Export modal. Oat styles the dialog (pads > header / > div / > footer, the
|
|
118
143
|
body div scrolls) plus inputs, selects, switches and tables. Inline CSS is
|
|
119
144
|
UNLAYERED and beats Oat's @layer rules, so this stays narrow: the bespoke
|