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.
Files changed (185) hide show
  1. {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
  2. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/AGENTS.md +24 -20
  3. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/PKG-INFO +1 -1
  4. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/pyproject.toml +1 -1
  5. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/__init__.py +1 -1
  6. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/cli.py +5 -0
  7. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/server.py +61 -4
  8. paratext_cli-0.3.0/src/paratext/review/sessions.py +125 -0
  9. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/static/app.js +44 -135
  10. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/static/index.html +25 -0
  11. paratext_cli-0.3.0/src/paratext/workshop.py +105 -0
  12. paratext_cli-0.3.0/tests/test_sessions.py +92 -0
  13. paratext_cli-0.3.0/tests/test_workshop.py +99 -0
  14. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/uv.lock +1 -1
  15. paratext_cli-0.2.4/backlog/config.yml +0 -16
  16. paratext_cli-0.2.4/backlog/milestones/m-0 - framework-split-and-first-release.md +0 -8
  17. paratext_cli-0.2.4/backlog/milestones/m-1 - review-ui.md +0 -8
  18. paratext_cli-0.2.4/backlog/milestones/m-2 - export-and-publishing.md +0 -8
  19. paratext_cli-0.2.4/backlog/milestones/m-3 - oat-first-ui-rework.md +0 -8
  20. paratext_cli-0.2.4/backlog/milestones/m-4 - packaging-and-pypi-release.md +0 -8
  21. paratext_cli-0.2.4/backlog/milestones/m-5 - marc-fidelity.md +0 -8
  22. paratext_cli-0.2.4/backlog/milestones/m-6 - test-coverage-and-onboarding.md +0 -8
  23. paratext_cli-0.2.4/backlog/milestones/m-7 - export-v2.md +0 -8
  24. paratext_cli-0.2.4/backlog/milestones/m-8 - review-ui-polish.md +0 -8
  25. paratext_cli-0.2.4/backlog/tasks/task-1 - Split-the-framework-out-of-paratext-nls-and-release-it.md +0 -27
  26. paratext_cli-0.2.4/backlog/tasks/task-10 - Remove-the-WattTime-provider.md +0 -26
  27. paratext_cli-0.2.4/backlog/tasks/task-11 - Make-paratext-new-self-installing.md +0 -26
  28. paratext_cli-0.2.4/backlog/tasks/task-12 - Add-paratext-guide-so-agents-can-self-orient.md +0 -27
  29. paratext_cli-0.2.4/backlog/tasks/task-13 - Guard-schema-prompt-and-view-drift.md +0 -27
  30. paratext_cli-0.2.4/backlog/tasks/task-14 - Build-the-human-corrected-gold-eval-set-in-review.md +0 -27
  31. paratext_cli-0.2.4/backlog/tasks/task-15 - Add-MARC-and-Dublin-Core-export-formats.md +0 -27
  32. paratext_cli-0.2.4/backlog/tasks/task-16 - Ship-schema.json-with-each-exported-dataset.md +0 -26
  33. paratext_cli-0.2.4/backlog/tasks/task-17 - Add-the-project-inspector-and-fix-sqlite-thread-safety.md +0 -27
  34. paratext_cli-0.2.4/backlog/tasks/task-18 - Point-the-default-card-detector-at-the-published-NLS-repo.md +0 -26
  35. paratext_cli-0.2.4/backlog/tasks/task-19 - Add-the-scope-aware-export-modal.md +0 -28
  36. paratext_cli-0.2.4/backlog/tasks/task-2 - Add-the-inbuilt-review-web-UI.md +0 -27
  37. paratext_cli-0.2.4/backlog/tasks/task-20 - Move-the-review-UI-onto-Oat.md +0 -27
  38. paratext_cli-0.2.4/backlog/tasks/task-21 - Render-entries-as-tables-read-only-and-editable.md +0 -26
  39. paratext_cli-0.2.4/backlog/tasks/task-22 - Publish-gold-to-Hugging-Face-with-Sign-in-with-HF.md +0 -27
  40. paratext_cli-0.2.4/backlog/tasks/task-23 - Warn-on-incomplete-provenance-without-blocking-publication.md +0 -26
  41. paratext_cli-0.2.4/backlog/tasks/task-24 - Add-the-paratext-logo-and-favicon.md +0 -26
  42. paratext_cli-0.2.4/backlog/tasks/task-25 - Remove-duplicated-helpers-across-the-codebase.md +0 -27
  43. paratext_cli-0.2.4/backlog/tasks/task-26 - Split-Store-and-dataset-loading-out-of-the-review-server.md +0 -27
  44. paratext_cli-0.2.4/backlog/tasks/task-27 - Lean-the-README-and-split-the-reference-into-docs.md +0 -27
  45. paratext_cli-0.2.4/backlog/tasks/task-28 - Add-NOTICE-and-make-the-repository-public.md +0 -27
  46. paratext_cli-0.2.4/backlog/tasks/task-29 - Rename-the-distribution-to-paratext-cli-and-publish-to-PyPI.md +0 -27
  47. paratext_cli-0.2.4/backlog/tasks/task-3 - Make-the-review-server-configurable.md +0 -26
  48. paratext_cli-0.2.4/backlog/tasks/task-30 - Make-project-discovery-work-from-any-environment.md +0 -27
  49. paratext_cli-0.2.4/backlog/tasks/task-31 - Give-clear-errors-for-provider-and-source-misconfiguration.md +0 -27
  50. paratext_cli-0.2.4/backlog/tasks/task-32 - Add-a-configurable-output-budget-and-provider-extra-body-passthrough.md +0 -27
  51. paratext_cli-0.2.4/backlog/tasks/task-33 - Bring-MARC-output-up-to-ISBD-punctuation-and-leader-conventions.md +0 -28
  52. paratext_cli-0.2.4/backlog/tasks/task-34 - Replace-the-blind-margin-crop-with-a-content-aware-fallback.md +0 -27
  53. paratext_cli-0.2.4/backlog/tasks/task-35 - Add-the-opt-in-AI-assistance-note-to-MARC-and-DC-export.md +0 -28
  54. paratext_cli-0.2.4/backlog/tasks/task-36 - Emit-only-one-main-entry-per-MARC-record.md +0 -28
  55. paratext_cli-0.2.4/backlog/tasks/task-37 - Extend-CLI-test-coverage-to-argparse-wiring-round-resolution-and-config-layering.md +0 -30
  56. paratext_cli-0.2.4/backlog/tasks/task-38 - Walk-a-real-run-from-a-fresh-scaffold.md +0 -28
  57. paratext_cli-0.2.4/backlog/tasks/task-39 - Add-the-paratext-dependency-when-scaffolding-into-an-existing-project.md +0 -27
  58. paratext_cli-0.2.4/backlog/tasks/task-4 - Genericise-the-defaults-away-from-local-inference-servers.md +0 -26
  59. paratext_cli-0.2.4/backlog/tasks/task-40 - Fix-cross-batch-accumulation-overwriting-metadata.jsonl.md +0 -26
  60. paratext_cli-0.2.4/backlog/tasks/task-41 - Remove-the-silent-hf-default-from-format.md +0 -27
  61. paratext_cli-0.2.4/backlog/tasks/task-42 - Add-paratext-export-db.md +0 -26
  62. paratext_cli-0.2.4/backlog/tasks/task-43 - Decide-whether-a-gold-export-ships-as-a-held-out-test-split.md +0 -26
  63. paratext_cli-0.2.4/backlog/tasks/task-44 - HF-export-v2-multi-image-projects-and-round-versioning-as-Hub-revisions.md +0 -26
  64. paratext_cli-0.2.4/backlog/tasks/task-45 - Add-paratext-bench.md +0 -26
  65. paratext_cli-0.2.4/backlog/tasks/task-46 - Review-UI-polish-flagged-stat-key-score-badge-and-verdict-toggles.md +0 -27
  66. paratext_cli-0.2.4/backlog/tasks/task-47 - Eval-editor-reach-View-hidden-fields-and-diff-corrected-against-model-output.md +0 -27
  67. paratext_cli-0.2.4/backlog/tasks/task-48 - Add-richer-scaffolder-field-types-for-enums-and-lists.md +0 -26
  68. paratext_cli-0.2.4/backlog/tasks/task-49 - Extend-the-carbon-providers.md +0 -25
  69. paratext_cli-0.2.4/backlog/tasks/task-5 - Add-source-adapters-and-slim-the-project-layout.md +0 -27
  70. paratext_cli-0.2.4/backlog/tasks/task-50 - Build-the-wordmark-in-a-mono-typeface.md +0 -24
  71. paratext_cli-0.2.4/backlog/tasks/task-51 - Decide-whether-to-pursue-the-PyPI-paratext-name.md +0 -26
  72. paratext_cli-0.2.4/backlog/tasks/task-6 - Rename-init-to-new-and-document-every-CLI-flag.md +0 -26
  73. paratext_cli-0.2.4/backlog/tasks/task-7 - Key-runs-and-packages-on-the-prompt-hash.md +0 -27
  74. paratext_cli-0.2.4/backlog/tasks/task-8 - Add-paratext-export-for-publishing-a-reviewed-round.md +0 -27
  75. paratext_cli-0.2.4/backlog/tasks/task-9 - Add-carbon-aware-scheduling.md +0 -27
  76. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.github/workflows/ci.yml +0 -0
  77. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.github/workflows/publish.yml +0 -0
  78. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.gitignore +0 -0
  79. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/intro.md +0 -0
  80. {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
  81. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-10 remove-the-watttime-provider.md +0 -0
  82. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-11 make-paratext-new-self-installing.md +0 -0
  83. {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
  84. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-13 guard-schema-prompt-and-view-drift.md +0 -0
  85. {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
  86. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-15 add-marc-and-dublin-core-export-formats.md +0 -0
  87. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-16 ship-schema-json-with-each-exported-dataset.md +0 -0
  88. {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
  89. {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
  90. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-19 add-the-scope-aware-export-modal.md +0 -0
  91. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-2 add-the-inbuilt-review-web-ui.md +0 -0
  92. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-20 move-the-review-ui-onto-oat.md +0 -0
  93. {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
  94. {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
  95. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-23 warn-on-incomplete-provenance-without-blocking-publication.md +0 -0
  96. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-24 add-the-paratext-logo-and-favicon.md +0 -0
  97. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-25 remove-duplicated-helpers-across-the-codebase.md +0 -0
  98. {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
  99. {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
  100. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-28 add-notice-and-make-the-repository-public.md +0 -0
  101. {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
  102. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-3 make-the-review-server-configurable.md +0 -0
  103. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-30 make-project-discovery-work-from-any-environment.md +0 -0
  104. {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
  105. {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
  106. {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
  107. {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
  108. {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
  109. {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
  110. {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
  111. {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
  112. {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
  113. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-40 fix-cross-batch-accumulation-overwriting-metadata-jsonl.md +0 -0
  114. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-41 remove-the-silent-hf-default-from-format.md +0 -0
  115. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-42 add-paratext-export-db.md +0 -0
  116. {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
  117. {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
  118. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-45 add-paratext-bench.md +0 -0
  119. {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
  120. {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
  121. {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
  122. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-49 extend-the-carbon-providers.md +0 -0
  123. {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
  124. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-50 build-the-wordmark-in-a-mono-typeface.md +0 -0
  125. {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
  126. {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
  127. {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
  128. {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
  129. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/.helical/tasks/PTX-9 add-carbon-aware-scheduling.md +0 -0
  130. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/LICENSE +0 -0
  131. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/NOTICE +0 -0
  132. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/README.md +0 -0
  133. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/assets/logo.png +0 -0
  134. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/assets/logo.svg +0 -0
  135. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/docs/configuration.md +0 -0
  136. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/docs/export.md +0 -0
  137. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/docs/green-scheduling.md +0 -0
  138. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/docs/hf-export-spec.md +0 -0
  139. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/docs/publishing.md +0 -0
  140. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/docs/scanned-cards.md +0 -0
  141. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/paratext.example.toml +0 -0
  142. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/AGENTS.md +0 -0
  143. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/carbon.py +0 -0
  144. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/cards.py +0 -0
  145. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/catalogue.py +0 -0
  146. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/config.py +0 -0
  147. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/datasets.py +0 -0
  148. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/extract.py +0 -0
  149. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/hf_export.py +0 -0
  150. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/inspect.py +0 -0
  151. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/io.py +0 -0
  152. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/packaging.py +0 -0
  153. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/paratext.example.toml +0 -0
  154. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/projects/__init__.py +0 -0
  155. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/projects/card_template/__init__.py +0 -0
  156. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/projects/card_template/prompt.md +0 -0
  157. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/projects/card_template/schema.py +0 -0
  158. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/records.py +0 -0
  159. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/__init__.py +0 -0
  160. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/hf_oauth.py +0 -0
  161. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/static/favicon.svg +0 -0
  162. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/static/oat.min.css +0 -0
  163. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/review/static/oat.min.js +0 -0
  164. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/runner.py +0 -0
  165. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/scaffold.py +0 -0
  166. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/sources.py +0 -0
  167. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/src/paratext/store.py +0 -0
  168. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_carbon.py +0 -0
  169. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_catalogue.py +0 -0
  170. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_cli.py +0 -0
  171. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_config.py +0 -0
  172. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_extract.py +0 -0
  173. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_guide.py +0 -0
  174. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_hf_export.py +0 -0
  175. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_io.py +0 -0
  176. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_packaging.py +0 -0
  177. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_project_audit.py +0 -0
  178. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_records.py +0 -0
  179. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_review.py +0 -0
  180. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_rounds.py +0 -0
  181. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_runner.py +0 -0
  182. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_scaffold.py +0 -0
  183. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_show_through.py +0 -0
  184. {paratext_cli-0.2.4 → paratext_cli-0.3.0}/tests/test_sources.py +0 -0
  185. {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-02'
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
- ## Backlog tasks are public
270
+ ## Tasks in this repo are public
271
271
 
272
- This repository is public, and `backlog/` is committed with it. Task titles,
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` backlog or the roadmap at `/opt/backlog` instead.
277
+ `paratext-nls` board or the `admin` project instead.
278
278
 
279
- <!-- BACKLOG.MD GUIDELINES START -->
280
- <!-- backlog.md-instructions-version: 1.50.1 -->
281
- <CRITICAL_INSTRUCTION>
279
+ <!-- HELICAL START -->
280
+ ## Tasks
282
281
 
283
- ## Backlog.md Workflow
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
- This project uses Backlog.md for task and project management.
286
-
287
- **For every user request in this project, run `backlog instructions overview` before answering or taking action.**
288
-
289
- Use the overview to decide whether to search, read, create, or update Backlog tasks.
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
- Before task lifecycle actions, read the matching detailed guide:
292
- - `backlog instructions task-creation` before creating or splitting tasks
293
- - `backlog instructions task-execution` before planning, changing status or assignee, adding a plan or implementation notes, or implementing task work
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
- Use `backlog <command> --help` before running unfamiliar commands. Help shows options, fields, and examples.
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
- Do not edit Backlog task, draft, document, decision, or milestone markdown files directly. Use the `backlog` CLI so metadata, relationships, and history stay consistent.
302
+ Labels come from a fixed vocabulary; `helical new --help` lists it.
299
303
 
300
- </CRITICAL_INSTRUCTION>
301
- <!-- BACKLOG.MD GUIDELINES END -->
304
+ Boards: <https://helical.ai.nls.uk> · read-only <https://projects.ai.nls.uk>
305
+ <!-- HELICAL END -->
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: paratext-cli
3
- Version: 0.2.4
3
+ Version: 0.3.0
4
4
  Summary: Modular, project-based metadata extraction from digitised library/archive collections with a multimodal model
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -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.2.4"
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
@@ -1,3 +1,3 @@
1
1
  """paratext — metadata extraction from digitised collections with a multimodal model."""
2
2
 
3
- __version__ = "0.2.4"
3
+ __version__ = "0.3.0"
@@ -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: Path
71
- store: Store
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.data_dir = data_dir
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.store = Store(default_db_path(data_dir, Path(db_path).resolve() if db_path else None))
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
- <a href="#/projects">Project configuration</a> — the fields, prompt and
290
- source options each installed project runs with.
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
- <a href="#/projects">Project configuration</a> — the fields, prompt and
343
- source options each installed project runs with.
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