calkit-python 0.41.19__tar.gz → 0.41.21__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 (356) hide show
  1. calkit_python-0.41.21/.gitmodules +3 -0
  2. {calkit_python-0.41.19 → calkit_python-0.41.21}/CONTRIBUTING.md +1 -1
  3. {calkit_python-0.41.19 → calkit_python-0.41.21}/PKG-INFO +9 -8
  4. {calkit_python-0.41.19 → calkit_python-0.41.21}/README.md +7 -6
  5. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/list.py +35 -1
  6. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/main/core.py +33 -3
  7. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/new.py +4 -8
  8. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/overleaf.py +12 -2
  9. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/detect.py +102 -25
  10. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/dvc/zip.py +17 -0
  11. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/git.py +29 -0
  12. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/models/core.py +11 -1
  13. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/main/test_core.py +90 -0
  14. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/test_list.py +41 -1
  15. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/test_new.py +14 -19
  16. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/test_overleaf.py +47 -0
  17. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/dvc/test_zip.py +25 -0
  18. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/models/test_core.py +22 -1
  19. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_detect.py +59 -2
  20. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/calkit-yaml.md +1 -1
  21. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/cli-reference.md +3 -3
  22. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/help.md +1 -1
  23. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/index.md +7 -6
  24. calkit_python-0.41.21/docs/questions.md +31 -0
  25. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/latex-codespaces.md +27 -4
  26. {calkit_python-0.41.19 → calkit_python-0.41.21}/mkdocs.yml +1 -0
  27. {calkit_python-0.41.19 → calkit_python-0.41.21}/pyproject.toml +1 -2
  28. {calkit_python-0.41.19 → calkit_python-0.41.21}/uv.lock +1064 -136
  29. {calkit_python-0.41.19 → calkit_python-0.41.21}/.claude-plugin/marketplace.json +0 -0
  30. {calkit_python-0.41.19 → calkit_python-0.41.21}/.gitattributes +0 -0
  31. {calkit_python-0.41.19 → calkit_python-0.41.21}/.gitignore +0 -0
  32. {calkit_python-0.41.19 → calkit_python-0.41.21}/.pre-commit-config.yaml +0 -0
  33. {calkit_python-0.41.19 → calkit_python-0.41.21}/.prettierignore +0 -0
  34. {calkit_python-0.41.19 → calkit_python-0.41.21}/.prettierrc.json +0 -0
  35. {calkit_python-0.41.19 → calkit_python-0.41.21}/.python-version +0 -0
  36. {calkit_python-0.41.19 → calkit_python-0.41.21}/.vscode/launch.json +0 -0
  37. {calkit_python-0.41.19 → calkit_python-0.41.21}/.vscode/tasks.json +0 -0
  38. {calkit_python-0.41.19 → calkit_python-0.41.21}/AGENTS.md +0 -0
  39. {calkit_python-0.41.19 → calkit_python-0.41.21}/CITATION.cff +0 -0
  40. {calkit_python-0.41.19 → calkit_python-0.41.21}/CODE_OF_CONDUCT.md +0 -0
  41. {calkit_python-0.41.19 → calkit_python-0.41.21}/LICENSE +0 -0
  42. {calkit_python-0.41.19 → calkit_python-0.41.21}/Makefile +0 -0
  43. {calkit_python-0.41.19 → calkit_python-0.41.21}/agent-plugin/.claude-plugin/plugin.json +0 -0
  44. {calkit_python-0.41.19 → calkit_python-0.41.21}/agent-plugin/skills/add-pipeline-stage/SKILL.md +0 -0
  45. {calkit_python-0.41.19 → calkit_python-0.41.21}/agent-plugin/skills/conventions/SKILL.md +0 -0
  46. {calkit_python-0.41.19 → calkit_python-0.41.21}/agent-plugin/skills/create-pipeline/SKILL.md +0 -0
  47. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/__init__.py +0 -0
  48. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/__main__.py +0 -0
  49. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/calc.py +0 -0
  50. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/check.py +0 -0
  51. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/__init__.py +0 -0
  52. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/check.py +0 -0
  53. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/cloud.py +0 -0
  54. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/config.py +0 -0
  55. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/core.py +0 -0
  56. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/delete.py +0 -0
  57. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/describe.py +0 -0
  58. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/dev.py +0 -0
  59. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/import_.py +0 -0
  60. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/latex.py +0 -0
  61. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/main/__init__.py +0 -0
  62. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/main/xr.py +0 -0
  63. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/notebooks.py +0 -0
  64. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/office.py +0 -0
  65. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/scheduler.py +0 -0
  66. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cli/update.py +0 -0
  67. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/cloud.py +0 -0
  68. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/conda.py +0 -0
  69. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/config.py +0 -0
  70. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/core.py +0 -0
  71. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/datasets.py +0 -0
  72. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/dependencies.py +0 -0
  73. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/docker.py +0 -0
  74. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/dvc/__init__.py +0 -0
  75. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/dvc/core.py +0 -0
  76. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/environments.py +0 -0
  77. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/fs.py +0 -0
  78. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/github.py +0 -0
  79. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/gui.py +0 -0
  80. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/install.py +0 -0
  81. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/invenio.py +0 -0
  82. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/julia.py +0 -0
  83. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/jupyter.py +0 -0
  84. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/jupyterlab/__init__.py +0 -0
  85. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/jupyterlab/routes.py +0 -0
  86. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/labextension/package.json +0 -0
  87. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/labextension/schemas/calkit/package.json.orig +0 -0
  88. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/labextension/schemas/calkit/plugin.json +0 -0
  89. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/labextension/static/502.9a2c5772a15466e923ef.js +0 -0
  90. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/labextension/static/695.2c41003a452d43d2b358.js +0 -0
  91. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/labextension/static/867.a42a046aa5108f54f8fb.js +0 -0
  92. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/labextension/static/909.e3f9cc3408834a7fdcc3.js +0 -0
  93. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/labextension/static/946.050af2abf7845cfbdbd2.js +0 -0
  94. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/labextension/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
  95. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/labextension/static/b2f1c3efe70cb539d121.png +0 -0
  96. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/labextension/static/remoteEntry.ac9035764d5b2adbb542.js +0 -0
  97. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/labextension/static/style.js +0 -0
  98. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/labextension/static/third-party-licenses.json +0 -0
  99. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/licenses.py +0 -0
  100. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/magics.py +0 -0
  101. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/matlab.py +0 -0
  102. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/models/__init__.py +0 -0
  103. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/models/io.py +0 -0
  104. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/models/iteration.py +0 -0
  105. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/models/pipeline.py +0 -0
  106. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/notebooks.py +0 -0
  107. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/office.py +0 -0
  108. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/ops.py +0 -0
  109. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/overleaf.py +0 -0
  110. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/pipeline.py +0 -0
  111. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/releases.py +0 -0
  112. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/server.py +0 -0
  113. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/templates/__init__.py +0 -0
  114. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/templates/core.py +0 -0
  115. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/templates/latex/__init__.py +0 -0
  116. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/templates/latex/article/paper.tex +0 -0
  117. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/templates/latex/core.py +0 -0
  118. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/templates/latex/jfm/jfm.bst +0 -0
  119. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/templates/latex/jfm/jfm.cls +0 -0
  120. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
  121. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/templates/latex/jfm/paper.tex +0 -0
  122. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/templates/latex/jfm/upmath.sty +0 -0
  123. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/__init__.py +0 -0
  124. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/__init__.py +0 -0
  125. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/main/__init__.py +0 -0
  126. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/main/test_lock.py +0 -0
  127. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/main/test_subprojects.py +0 -0
  128. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/main/test_xr.py +0 -0
  129. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/test_check.py +0 -0
  130. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/test_cloud.py +0 -0
  131. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/test_config.py +0 -0
  132. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/test_delete.py +0 -0
  133. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/test_import.py +0 -0
  134. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/test_latex.py +0 -0
  135. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/test_notebooks.py +0 -0
  136. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/test_scheduler.py +0 -0
  137. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/cli/test_update.py +0 -0
  138. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/dvc/__init__.py +0 -0
  139. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/dvc/test_core.py +0 -0
  140. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/jupyterlab/__init__.py +0 -0
  141. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/jupyterlab/conftest.py +0 -0
  142. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/jupyterlab/test_routes.py +0 -0
  143. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/models/__init__.py +0 -0
  144. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/models/test_iteration.py +0 -0
  145. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/models/test_pipeline.py +0 -0
  146. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_calc.py +0 -0
  147. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_check.py +0 -0
  148. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_cloud.py +0 -0
  149. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_conda.py +0 -0
  150. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_core.py +0 -0
  151. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_dependencies.py +0 -0
  152. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_docker.py +0 -0
  153. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_environments.py +0 -0
  154. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_fs.py +0 -0
  155. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_git.py +0 -0
  156. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_install.py +0 -0
  157. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_invenio.py +0 -0
  158. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_julia.py +0 -0
  159. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_jupyter.py +0 -0
  160. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_licenses.py +0 -0
  161. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_magics.py +0 -0
  162. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_matlab.py +0 -0
  163. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_notebooks.py +0 -0
  164. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_pipeline.py +0 -0
  165. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_releases.py +0 -0
  166. {calkit_python-0.41.19 → calkit_python-0.41.21}/calkit/tests/test_templates.py +0 -0
  167. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/CNAME +0 -0
  168. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/acknowledgements.md +0 -0
  169. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/ai-tools.md +0 -0
  170. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/apps.md +0 -0
  171. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/calculations.md +0 -0
  172. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/cloud-integration.md +0 -0
  173. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/datasets.md +0 -0
  174. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/dependencies.md +0 -0
  175. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/environments.md +0 -0
  176. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/examples.md +0 -0
  177. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/governance.md +0 -0
  178. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/hpc.md +0 -0
  179. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/c-to-the-k-white.svg +0 -0
  180. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/calkit-fragmentation-compendium.png +0 -0
  181. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/calkit-no-bg.png +0 -0
  182. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/caltech.png +0 -0
  183. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/connect-zenodo.png +0 -0
  184. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/jupyterlab/all-green.png +0 -0
  185. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/jupyterlab/collect-data-stale.png +0 -0
  186. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/jupyterlab/new-env.png +0 -0
  187. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/jupyterlab/new-notebook.png +0 -0
  188. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/jupyterlab/pipeline-badge.png +0 -0
  189. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/jupyterlab-params.png +0 -0
  190. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/pipeline.png +0 -0
  191. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/plos-osi-code-2024-03.png +0 -0
  192. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/schmidt-sciences.png +0 -0
  193. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/img/vscode-nb-params.png +0 -0
  194. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/installation.md +0 -0
  195. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/jupyterlab.md +0 -0
  196. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/local-server.md +0 -0
  197. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/notebooks.md +0 -0
  198. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/overleaf.md +0 -0
  199. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/pipeline/index.md +0 -0
  200. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/pipeline/manual-steps.md +0 -0
  201. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/pipeline/running-and-logging.md +0 -0
  202. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/quickstart.md +0 -0
  203. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/references.md +0 -0
  204. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/releases.md +0 -0
  205. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/reproducibility.md +0 -0
  206. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/subprojects.md +0 -0
  207. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/adding-latex-pub-docker.md +0 -0
  208. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/ai-agents.md +0 -0
  209. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/conda-envs.md +0 -0
  210. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/existing-project.md +0 -0
  211. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/first-project.md +0 -0
  212. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/github-actions.md +0 -0
  213. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/actions-repo-secrets.png +0 -0
  214. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
  215. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
  216. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
  217. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
  218. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
  219. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
  220. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
  221. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
  222. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
  223. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
  224. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/latex-codespaces/push.png +0 -0
  225. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
  226. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
  227. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/chart-more-rows.png +0 -0
  228. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/create-project.png +0 -0
  229. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
  230. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/excel-chart.png +0 -0
  231. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/excel-data.png +0 -0
  232. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
  233. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/needs-clone.png +0 -0
  234. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/new-stage.png +0 -0
  235. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
  236. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
  237. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/status-more-rows.png +0 -0
  238. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
  239. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/untracked-data.png +0 -0
  240. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/updated-publication.png +0 -0
  241. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
  242. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/office/workflow-page.png +0 -0
  243. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/openfoam/clone.png +0 -0
  244. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/openfoam/create-project.png +0 -0
  245. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
  246. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
  247. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
  248. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/openfoam/new-token.png +0 -0
  249. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/openfoam/reclone.png +0 -0
  250. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
  251. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/quick-actions.png +0 -0
  252. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/run-proc.png +0 -0
  253. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/vscode-slurm-notebook/create-calkit-env.png +0 -0
  254. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/vscode-slurm-notebook/create-inner-env.png +0 -0
  255. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/vscode-slurm-notebook/create-new-calkit-env.png +0 -0
  256. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/vscode-slurm-notebook/select-calkit-env.png +0 -0
  257. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/vscode-slurm-notebook/slurm-job-running.png +0 -0
  258. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/vscode-slurm-notebook/slurm-launch-options.png +0 -0
  259. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/img/vscode-slurm-notebook/starting-slurm-job.png +0 -0
  260. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/index.md +0 -0
  261. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/jupyterlab.md +0 -0
  262. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/matlab.md +0 -0
  263. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/notebook-pipeline.md +0 -0
  264. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/office.md +0 -0
  265. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/openfoam.md +0 -0
  266. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/procedures.md +0 -0
  267. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/tutorials/vscode-slurm-notebook.md +0 -0
  268. {calkit_python-0.41.19 → calkit_python-0.41.21}/docs/version-control.md +0 -0
  269. {calkit_python-0.41.19 → calkit_python-0.41.21}/flake.lock +0 -0
  270. {calkit_python-0.41.19 → calkit_python-0.41.21}/flake.nix +0 -0
  271. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/.yarnrc.yml +0 -0
  272. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/babel.config.js +0 -0
  273. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/install.json +0 -0
  274. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/jest.config.js +0 -0
  275. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/jupyter-config/server-config/calkit.json +0 -0
  276. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/package.json +0 -0
  277. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/schema/plugin.json +0 -0
  278. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/__tests__/useQueries.spec.ts +0 -0
  279. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/calkit-config.ts +0 -0
  280. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/cell-output-marker.ts +0 -0
  281. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/components/commit-dialog.tsx +0 -0
  282. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/components/environment-editor.tsx +0 -0
  283. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/components/notebook-registration.tsx +0 -0
  284. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/components/notebook-toolbar.tsx +0 -0
  285. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/components/pipeline-status-bar.tsx +0 -0
  286. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/components/project-info-editor.tsx +0 -0
  287. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/components/sidebar-settings.tsx +0 -0
  288. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/components/sidebar.tsx +0 -0
  289. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/components/stage-editor.tsx +0 -0
  290. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/feature-flags.ts +0 -0
  291. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/file-browser-menu.ts +0 -0
  292. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/hooks/__tests__/useQueries.test.tsx +0 -0
  293. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/hooks/useQueries.ts +0 -0
  294. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/icons.ts +0 -0
  295. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/index.ts +0 -0
  296. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/io-tracker.ts +0 -0
  297. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/pipeline-state.ts +0 -0
  298. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/queryClient.ts +0 -0
  299. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/request.ts +0 -0
  300. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/src/shims-mainmenu.d.ts +0 -0
  301. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/style/base.css +0 -0
  302. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/style/cell-output-marker.css +0 -0
  303. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/style/environment-editor.css +0 -0
  304. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/style/environment-selector.css +0 -0
  305. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/style/img/calkit-no-bg.png +0 -0
  306. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/style/index.css +0 -0
  307. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/style/index.js +0 -0
  308. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/style/notebook-toolbar.css +0 -0
  309. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/style/pipeline-status-bar.css +0 -0
  310. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/style/sidebar.css +0 -0
  311. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/tsconfig.json +0 -0
  312. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/tsconfig.test.json +0 -0
  313. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/ui-tests/.gitignore +0 -0
  314. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/ui-tests/README.md +0 -0
  315. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/ui-tests/jupyter_server_test_config.py +0 -0
  316. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/ui-tests/package.json +0 -0
  317. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/ui-tests/playwright.config.js +0 -0
  318. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/ui-tests/tests/calkit.spec.ts +0 -0
  319. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/ui-tests/yarn.lock +0 -0
  320. {calkit_python-0.41.19 → calkit_python-0.41.21}/jupyterlab-ext/yarn.lock +0 -0
  321. {calkit_python-0.41.19 → calkit_python-0.41.21}/scripts/generate-docs-references.py +0 -0
  322. {calkit_python-0.41.19 → calkit_python-0.41.21}/scripts/install.ps1 +0 -0
  323. {calkit_python-0.41.19 → calkit_python-0.41.21}/scripts/install.sh +0 -0
  324. {calkit_python-0.41.19 → calkit_python-0.41.21}/scripts/make-calk9.sh +0 -0
  325. {calkit_python-0.41.19 → calkit_python-0.41.21}/scripts/sync-docs.py +0 -0
  326. {calkit_python-0.41.19 → calkit_python-0.41.21}/test/dvc-md5-dir/osx-arm64.txt +0 -0
  327. {calkit_python-0.41.19 → calkit_python-0.41.21}/test/nb-julia.ipynb +0 -0
  328. {calkit_python-0.41.19 → calkit_python-0.41.21}/test/nb-params.ipynb +0 -0
  329. {calkit_python-0.41.19 → calkit_python-0.41.21}/test/nb-subdir.ipynb +0 -0
  330. {calkit_python-0.41.19 → calkit_python-0.41.21}/test/pipeline.ipynb +0 -0
  331. {calkit_python-0.41.19 → calkit_python-0.41.21}/test/script.py +0 -0
  332. {calkit_python-0.41.19 → calkit_python-0.41.21}/test/test-log.log +0 -0
  333. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/.gitignore +0 -0
  334. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/.vscodeignore +0 -0
  335. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/CHANGELOG.md +0 -0
  336. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/LICENSE +0 -0
  337. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/README.md +0 -0
  338. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/images/calkit-icon.svg +0 -0
  339. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/images/calkit-no-bg.png +0 -0
  340. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/package-lock.json +0 -0
  341. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/package.json +0 -0
  342. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/scripts/set-proposed-api.js +0 -0
  343. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/src/environments.ts +0 -0
  344. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/src/extension.ts +0 -0
  345. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/src/figures/core.ts +0 -0
  346. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/src/figures/view.ts +0 -0
  347. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/src/notebooks.ts +0 -0
  348. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/src/pipeline/core.ts +0 -0
  349. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/src/sidebar.ts +0 -0
  350. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/src/test/environments.test.ts +0 -0
  351. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/src/test/figures.test.ts +0 -0
  352. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/src/test/kernel-selection.test.ts +0 -0
  353. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/src/test/notebooks.test.ts +0 -0
  354. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/src/test/pipeline.test.ts +0 -0
  355. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/src/types.ts +0 -0
  356. {calkit_python-0.41.19 → calkit_python-0.41.21}/vscode-ext/tsconfig.json +0 -0
@@ -0,0 +1,3 @@
1
+ [submodule "examples/basic"]
2
+ path = examples/basic
3
+ url = https://github.com/calkit/example-basic.git
@@ -98,7 +98,7 @@ This guide will help you get started.
98
98
  ## 🎉 Join the community
99
99
 
100
100
  - Participate in **[GitHub Discussions](https://github.com/calkit/discussions)**.
101
- - Join our [**Discord**](https://discord.gg/ubb7gAXc) for real-time collaboration.
101
+ - Join our [**Discord**](https://discord.gg/m2MBC79HzD) for real-time collaboration.
102
102
  - Follow our updates on [**LinkedIn**](https://linkedin.com/company/calkit).
103
103
 
104
104
  We appreciate your help in making Calkit better! 🚀
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calkit-python
3
- Version: 0.41.19
3
+ Version: 0.41.21
4
4
  Summary: Reproducibility simplified.
5
5
  Project-URL: Homepage, https://calkit.org
6
6
  Project-URL: Issues, https://github.com/calkit/calkit/issues
@@ -26,7 +26,7 @@ Requires-Dist: arithmeval
26
26
  Requires-Dist: bibtexparser
27
27
  Requires-Dist: checksumdir
28
28
  Requires-Dist: docx2pdf
29
- Requires-Dist: dvc==3.67.1
29
+ Requires-Dist: dvc[all]==3.67.1
30
30
  Requires-Dist: fastapi
31
31
  Requires-Dist: fsspec>=2026.4.0
32
32
  Requires-Dist: gitpython
@@ -78,15 +78,16 @@ Description-Content-Type: text/markdown
78
78
 
79
79
  <!-- INCLUDE: docs/index.md -->
80
80
 
81
- It's six months since you submitted your paper,
82
- do you know exactly how your figures and results were generated?
83
-
84
- You will if they're part of a Calkit project.
81
+ Typical research workflows are horizontally-siloed, i.e.,
82
+ various stages--data collection, analysis, writing--are performed in
83
+ disconnected systems,
84
+ turning research into a slow, error-prone, and tedious
85
+ [waterfall](https://en.wikipedia.org/wiki/Waterfall_model) process.
85
86
 
86
87
  Calkit helps you integrate code, data, figures, results, publications,
87
88
  and more into a cohesive, traceable, and portable _knowledge creation system_,
88
- so every output can be traced back to its source and reproduced with a
89
- single command.
89
+ so every output can be traced back to its source (provenance)
90
+ and reproduced with a single command.
90
91
 
91
92
  With industry standard tools combined into a unified and simplified experience
92
93
  tailored for research,
@@ -19,15 +19,16 @@
19
19
 
20
20
  <!-- INCLUDE: docs/index.md -->
21
21
 
22
- It's six months since you submitted your paper,
23
- do you know exactly how your figures and results were generated?
24
-
25
- You will if they're part of a Calkit project.
22
+ Typical research workflows are horizontally-siloed, i.e.,
23
+ various stages--data collection, analysis, writing--are performed in
24
+ disconnected systems,
25
+ turning research into a slow, error-prone, and tedious
26
+ [waterfall](https://en.wikipedia.org/wiki/Waterfall_model) process.
26
27
 
27
28
  Calkit helps you integrate code, data, figures, results, publications,
28
29
  and more into a cohesive, traceable, and portable _knowledge creation system_,
29
- so every output can be traced back to its source and reproduced with a
30
- single command.
30
+ so every output can be traced back to its source (provenance)
31
+ and reproduced with a single command.
31
32
 
32
33
  With industry standard tools combined into a unified and simplified experience
33
34
  tailored for research,
@@ -174,6 +174,40 @@ def list_presentations(
174
174
  _list_artifacts("presentations", json_output, declared_only)
175
175
 
176
176
 
177
+ def _echo_question(n: int, question: str | dict) -> None:
178
+ """Print a single question in the human-readable YAML-ish listing format.
179
+
180
+ String questions are shown as a numbered line. Rich (dict) questions are
181
+ shown with their text on the first line followed by hypothesis, answer,
182
+ evidence, and any other fields in YAML-like indented form.
183
+ """
184
+ if isinstance(question, str):
185
+ typer.echo(f"{n}. {question}")
186
+ return
187
+ # Copy so popping 'question' doesn't mutate the caller's dict.
188
+ question = dict(question)
189
+ text = question.pop("question", "")
190
+ typer.echo(f"{n}. question: {text}")
191
+ for k, v in question.items():
192
+ if isinstance(v, dict):
193
+ typer.echo(f" {k}:")
194
+ for k1, v1 in v.items():
195
+ typer.echo(f" {k1}: {v1}")
196
+ elif isinstance(v, list):
197
+ typer.echo(f" {k}:")
198
+ for item in v:
199
+ if isinstance(item, dict):
200
+ for n1, (k1, v1) in enumerate(item.items()):
201
+ if n1 == 0:
202
+ typer.echo(f" - {k1}: {v1}")
203
+ else:
204
+ typer.echo(f" {k1}: {v1}")
205
+ else:
206
+ typer.echo(f" - {item}")
207
+ else:
208
+ typer.echo(f" {k}: {v}")
209
+
210
+
177
211
  @list_app.command(name="questions")
178
212
  def list_questions(
179
213
  json_output: Annotated[
@@ -186,7 +220,7 @@ def list_questions(
186
220
  typer.echo(json.dumps(questions))
187
221
  return
188
222
  for n, question in enumerate(questions, start=1):
189
- typer.echo(f"{n}. {question}")
223
+ _echo_question(n, question)
190
224
 
191
225
 
192
226
  @list_app.command(name="publications|pubs")
@@ -179,11 +179,26 @@ def init(
179
179
  typer.Option(
180
180
  "--force",
181
181
  "-f",
182
- help="Force reinitializing DVC if already initialized.",
182
+ help="Re-initialize even if the project is already initialized.",
183
183
  ),
184
184
  ] = False,
185
185
  ):
186
186
  """Initialize the current working directory."""
187
+ import git
188
+ from git.exc import InvalidGitRepositoryError
189
+
190
+ def _project_is_initialized() -> bool:
191
+ try:
192
+ git.Repo(".", search_parent_directories=False)
193
+ except InvalidGitRepositoryError:
194
+ return False
195
+ return os.path.isfile(".dvc/config") and os.path.isfile("calkit.yaml")
196
+
197
+ if _project_is_initialized() and not force:
198
+ raise_error(
199
+ "This project is already initialized. "
200
+ "Use --force to re-initialize."
201
+ )
187
202
  subprocess.run(["git", "init"])
188
203
  result = calkit.dvc.run_dvc_command(
189
204
  ["init"] + (["--force"] if force else [])
@@ -197,13 +212,15 @@ def init(
197
212
  # Commit the newly created .dvc directory
198
213
  repo = calkit.git.get_repo()
199
214
  repo.git.add(".dvc")
200
- repo.git.commit("-m", "Initialize DVC")
215
+ if calkit.git.get_staged_files(repo=repo):
216
+ repo.git.commit("-m", "Initialize DVC")
201
217
  # Create an empty calkit.yaml if one doesn't already exist
202
218
  if not os.path.isfile("calkit.yaml"):
203
219
  with open("calkit.yaml", "w"):
204
220
  pass
205
221
  repo.git.add("calkit.yaml")
206
- repo.git.commit("-m", "Initialize Calkit")
222
+ if calkit.git.get_staged_files(repo=repo):
223
+ repo.git.commit("-m", "Initialize Calkit")
207
224
  # TODO: Initialize `dvc.yaml`
208
225
  # TODO: Add a sane .gitignore file
209
226
  # TODO: Add a sane LICENSE file?
@@ -769,6 +786,8 @@ def add(
769
786
  elif to == "git":
770
787
  subprocess.call(["git", "add"] + paths)
771
788
  elif to == "dvc":
789
+ for path in paths:
790
+ calkit.git.ensure_dvc_pointer_is_not_ignored(repo, path)
772
791
  calkit.dvc.run_dvc_command(["add"] + paths)
773
792
  elif to == "dvc-zip":
774
793
  for path in paths:
@@ -882,6 +901,7 @@ def add(
882
901
  f"Adding {path} to DVC since it's already tracked "
883
902
  "with DVC"
884
903
  )
904
+ calkit.git.ensure_dvc_pointer_is_not_ignored(repo, path)
885
905
  calkit.dvc.run_dvc_command(["add", path])
886
906
  elif posix_path in pipeline_output_storage:
887
907
  # Respect storage explicitly set in the pipeline definition
@@ -926,6 +946,7 @@ def add(
926
946
  typer.echo(f"Would add {path} to DVC (per extension)")
927
947
  else:
928
948
  typer.echo(f"Adding {path} to DVC per its extension")
949
+ calkit.git.ensure_dvc_pointer_is_not_ignored(repo, path)
929
950
  calkit.dvc.run_dvc_command(["add", path])
930
951
  elif calkit.dvc.zip.is_zip_candidate(path):
931
952
  if dry_run:
@@ -946,6 +967,7 @@ def add(
946
967
  typer.echo(
947
968
  f"Adding {path} to DVC since it's greater than 1 MB"
948
969
  )
970
+ calkit.git.ensure_dvc_pointer_is_not_ignored(repo, path)
949
971
  calkit.dvc.run_dvc_command(["add", path])
950
972
  else:
951
973
  if dry_run:
@@ -3201,6 +3223,14 @@ def call_dvc(
3201
3223
  and DVC is not installed.
3202
3224
  """
3203
3225
  result = calkit.dvc.run_dvc_command(sys.argv[2:])
3226
+ if result != 0 and len(sys.argv) > 2 and sys.argv[2] == "add":
3227
+ typer.secho(
3228
+ "Hint: If DVC failed because a .dvc pointer file is git-ignored, "
3229
+ "use `calkit add --to=dvc <path>` instead so Calkit can "
3230
+ "automatically manage the gitignore exception.",
3231
+ fg=typer.colors.YELLOW,
3232
+ err=True,
3233
+ )
3204
3234
  sys.exit(result)
3205
3235
 
3206
3236
 
@@ -3174,7 +3174,7 @@ def new_release(
3174
3174
  detected_kind, artifact = calkit.releases.find_artifact(path, ck_info)
3175
3175
  if detected_kind is None:
3176
3176
  detected_kind = calkit.detect.detect_artifact_kind(
3177
- pathlib.Path(path).as_posix()
3177
+ pathlib.Path(path).as_posix(),
3178
3178
  )
3179
3179
  if release_kind is None:
3180
3180
  if detected_kind is None:
@@ -3258,16 +3258,12 @@ def new_release(
3258
3258
  # stored as-is (renamed); folders and whole-project releases are
3259
3259
  # zipped.
3260
3260
  project_name = calkit.detect_project_name(prepend_owner=False)
3261
- if path == ".":
3261
+ if path == "." or os.path.isdir(path):
3262
3262
  stored_filename = f"{project_name}-{name}.zip"
3263
3263
  is_zip = True
3264
- elif os.path.isdir(path):
3265
- folder_name = os.path.basename(os.path.normpath(path))
3266
- stored_filename = f"{project_name}-{folder_name}-{name}.zip"
3267
- is_zip = True
3268
3264
  elif os.path.isfile(path):
3269
- stem, ext = os.path.splitext(os.path.basename(path))
3270
- stored_filename = f"{project_name}-{stem}-{name}{ext}"
3265
+ _, ext = os.path.splitext(os.path.basename(path))
3266
+ stored_filename = f"{project_name}-{name}{ext}"
3271
3267
  is_zip = False
3272
3268
  else:
3273
3269
  raise_error(f"Release path '{path}' does not exist")
@@ -437,7 +437,12 @@ def sync(
437
437
  raise_error("No Overleaf sync info found")
438
438
  overleaf_sync_dirs = list(overleaf_info.keys())
439
439
  if paths is not None:
440
- paths = [os.path.dirname(p) if os.path.isfile(p) else p for p in paths]
440
+ paths = [
441
+ (os.path.dirname(p) if os.path.isfile(p) else p)
442
+ .strip()
443
+ .rstrip("/\\")
444
+ for p in paths
445
+ ]
441
446
  for path in paths:
442
447
  if path not in overleaf_sync_dirs:
443
448
  raise_error(f"Path '{path}' is not synced with Overleaf")
@@ -611,7 +616,12 @@ def get_status(
611
616
  raise_error("No Overleaf sync info found")
612
617
  overleaf_sync_dirs = list(overleaf_info.keys())
613
618
  if paths is not None:
614
- paths = [os.path.dirname(p) if os.path.isfile(p) else p for p in paths]
619
+ paths = [
620
+ (os.path.dirname(p) if os.path.isfile(p) else p)
621
+ .strip()
622
+ .rstrip("/\\")
623
+ for p in paths
624
+ ]
615
625
  for path in paths:
616
626
  if path not in overleaf_sync_dirs:
617
627
  raise_error(f"Path '{path}' is not synced with Overleaf")
@@ -13,6 +13,8 @@ import sys
13
13
  from pathlib import Path
14
14
  from typing import Literal
15
15
 
16
+ import pathspec
17
+
16
18
  NotebookLanguage = Literal["python", "julia", "r"]
17
19
 
18
20
 
@@ -1549,6 +1551,20 @@ def _is_stdlib_module(module_name: str) -> bool:
1549
1551
  return base_module in common_stdlib
1550
1552
 
1551
1553
 
1554
+ _PYTHON_IMPORT_TO_DISTRIBUTION = {
1555
+ "sklearn": "scikit-learn",
1556
+ "skimage": "scikit-image",
1557
+ "cv2": "opencv-python",
1558
+ "PIL": "Pillow",
1559
+ "bs4": "beautifulsoup4",
1560
+ "yaml": "PyYAML",
1561
+ "dateutil": "python-dateutil",
1562
+ "dotenv": "python-dotenv",
1563
+ "serial": "pyserial",
1564
+ "git": "GitPython",
1565
+ }
1566
+
1567
+
1552
1568
  def detect_python_dependencies(
1553
1569
  script_path: str | None = None,
1554
1570
  code: str | None = None,
@@ -1614,13 +1630,21 @@ def detect_python_dependencies(
1614
1630
  if not _is_stdlib_module(module_name):
1615
1631
  # Get the top-level package name
1616
1632
  top_level = module_name.split(".")[0]
1617
- dependencies.add(top_level)
1633
+ dependencies.add(
1634
+ _PYTHON_IMPORT_TO_DISTRIBUTION.get(
1635
+ top_level, top_level
1636
+ )
1637
+ )
1618
1638
  elif isinstance(node, ast.ImportFrom):
1619
1639
  if node.module and node.level == 0:
1620
1640
  module_name = node.module
1621
1641
  if not _is_stdlib_module(module_name):
1622
1642
  top_level = module_name.split(".")[0]
1623
- dependencies.add(top_level)
1643
+ dependencies.add(
1644
+ _PYTHON_IMPORT_TO_DISTRIBUTION.get(
1645
+ top_level, top_level
1646
+ )
1647
+ )
1624
1648
  return sorted(list(dependencies))
1625
1649
 
1626
1650
 
@@ -1927,6 +1951,21 @@ PRESENTATION_NAMES = {
1927
1951
  "talk.pdf",
1928
1952
  }
1929
1953
 
1954
+ DETECTION_IGNORE_FPATH = os.path.join(".calkit", "ignore")
1955
+
1956
+
1957
+ def load_detection_ignore(wdir: str | None = None) -> pathspec.PathSpec | None:
1958
+ """Load ``.calkit/ignore`` artifact detection exclusions."""
1959
+ import pathspec
1960
+
1961
+ fpath = DETECTION_IGNORE_FPATH
1962
+ if wdir is not None:
1963
+ fpath = os.path.join(wdir, fpath)
1964
+ if not os.path.isfile(fpath):
1965
+ return None
1966
+ with open(fpath, encoding="utf-8") as f:
1967
+ return pathspec.PathSpec.from_lines("gitwildmatch", f)
1968
+
1930
1969
 
1931
1970
  def _ancestor_dir_names(rel_path: str) -> set[str]:
1932
1971
  """Lower-cased names of the ancestor directories of a "/"-separated path."""
@@ -1938,8 +1977,12 @@ def _path_ext(rel_path: str) -> str:
1938
1977
  return ("." + name.rsplit(".", 1)[-1].lower()) if "." in name else ""
1939
1978
 
1940
1979
 
1941
- def is_figure_path(rel_path: str) -> bool:
1980
+ def is_figure_path(
1981
+ rel_path: str, *, ignore: pathspec.PathSpec | None = None
1982
+ ) -> bool:
1942
1983
  """Whether a repo-relative path looks like an auto-detectable figure."""
1984
+ if ignore is not None and ignore.match_file(rel_path):
1985
+ return False
1943
1986
  ancestors = _ancestor_dir_names(rel_path)
1944
1987
  ext = _path_ext(rel_path)
1945
1988
  if ext in FIGURE_EXTENSIONS and ancestors & FIGURE_DIRS:
@@ -1951,37 +1994,49 @@ def is_figure_path(rel_path: str) -> bool:
1951
1994
  return False
1952
1995
 
1953
1996
 
1954
- def is_dataset_path(rel_path: str) -> bool:
1997
+ def is_dataset_path(
1998
+ rel_path: str, *, ignore: pathspec.PathSpec | None = None
1999
+ ) -> bool:
1955
2000
  """Whether a repo-relative path looks like an auto-detectable dataset."""
2001
+ if ignore is not None and ignore.match_file(rel_path):
2002
+ return False
1956
2003
  return (
1957
2004
  _path_ext(rel_path) in DATASET_EXTENSIONS
1958
2005
  and bool(_ancestor_dir_names(rel_path) & DATA_DIRS)
1959
- and not is_figure_path(rel_path)
2006
+ and not is_figure_path(rel_path, ignore=ignore)
1960
2007
  )
1961
2008
 
1962
2009
 
1963
- def is_result_path(rel_path: str) -> bool:
2010
+ def is_result_path(
2011
+ rel_path: str, *, ignore: pathspec.PathSpec | None = None
2012
+ ) -> bool:
1964
2013
  """Whether a repo-relative path looks like an auto-detectable result.
1965
2014
 
1966
2015
  A result is a data-like file (JSON, CSV, etc.) under a ``results``-style
1967
2016
  directory. Anything already detected as a figure or dataset is excluded.
1968
2017
  """
2018
+ if ignore is not None and ignore.match_file(rel_path):
2019
+ return False
1969
2020
  return (
1970
2021
  _path_ext(rel_path) in RESULT_EXTENSIONS
1971
2022
  and bool(_ancestor_dir_names(rel_path) & RESULT_DIRS)
1972
- and not is_figure_path(rel_path)
1973
- and not is_dataset_path(rel_path)
2023
+ and not is_figure_path(rel_path, ignore=ignore)
2024
+ and not is_dataset_path(rel_path, ignore=ignore)
1974
2025
  )
1975
2026
 
1976
2027
 
1977
- def is_presentation_path(rel_path: str) -> bool:
2028
+ def is_presentation_path(
2029
+ rel_path: str, *, ignore: pathspec.PathSpec | None = None
2030
+ ) -> bool:
1978
2031
  """Whether a repo-relative path looks like an auto-detectable presentation.
1979
2032
 
1980
2033
  Either a slide-deck file (PDF/PPTX/KEY) under a ``slides``/``presentations``
1981
2034
  directory, or a file with a presentation-like name (e.g. ``slides.pdf``,
1982
2035
  ``presentation.pdf``) anywhere. Figures are excluded.
1983
2036
  """
1984
- if is_figure_path(rel_path):
2037
+ if ignore is not None and ignore.match_file(rel_path):
2038
+ return False
2039
+ if is_figure_path(rel_path, ignore=ignore):
1985
2040
  return False
1986
2041
  name = rel_path.rsplit("/", 1)[-1].lower()
1987
2042
  if name in PRESENTATION_NAMES:
@@ -2018,7 +2073,9 @@ PUBLICATION_NAMES = {
2018
2073
  }
2019
2074
 
2020
2075
 
2021
- def is_publication_path(rel_path: str) -> bool:
2076
+ def is_publication_path(
2077
+ rel_path: str, *, ignore: pathspec.PathSpec | None = None
2078
+ ) -> bool:
2022
2079
  """Whether a repo-relative path looks like an auto-detectable publication.
2023
2080
 
2024
2081
  Either a document (PDF/TeX/DOCX) under a ``paper``/``publication``/
@@ -2026,7 +2083,11 @@ def is_publication_path(rel_path: str) -> bool:
2026
2083
  ``manuscript.pdf``, ``main.tex``) anywhere. Figures and presentations are
2027
2084
  excluded.
2028
2085
  """
2029
- if is_figure_path(rel_path) or is_presentation_path(rel_path):
2086
+ if ignore is not None and ignore.match_file(rel_path):
2087
+ return False
2088
+ if is_figure_path(rel_path, ignore=ignore) or is_presentation_path(
2089
+ rel_path, ignore=ignore
2090
+ ):
2030
2091
  return False
2031
2092
  name = rel_path.rsplit("/", 1)[-1].lower()
2032
2093
  if name in PUBLICATION_NAMES:
@@ -2036,19 +2097,21 @@ def is_publication_path(rel_path: str) -> bool:
2036
2097
  )
2037
2098
 
2038
2099
 
2039
- def detect_artifact_kind(rel_path: str) -> str | None:
2100
+ def detect_artifact_kind(
2101
+ rel_path: str, *, ignore: pathspec.PathSpec | None = None
2102
+ ) -> str | None:
2040
2103
  """Infer an artifact's release kind from its repo-relative path.
2041
2104
 
2042
2105
  Returns one of ``"figure"``, ``"presentation"``, ``"publication"``, or
2043
2106
  ``"dataset"``, or ``None`` if the path doesn't look like any of these.
2044
2107
  """
2045
- if is_figure_path(rel_path):
2108
+ if is_figure_path(rel_path, ignore=ignore):
2046
2109
  return "figure"
2047
- if is_presentation_path(rel_path):
2110
+ if is_presentation_path(rel_path, ignore=ignore):
2048
2111
  return "presentation"
2049
- if is_publication_path(rel_path):
2112
+ if is_publication_path(rel_path, ignore=ignore):
2050
2113
  return "publication"
2051
- if is_dataset_path(rel_path):
2114
+ if is_dataset_path(rel_path, ignore=ignore):
2052
2115
  return "dataset"
2053
2116
  return None
2054
2117
 
@@ -2090,6 +2153,7 @@ def _collapse_dataset_folders(paths: list[str]) -> list[str]:
2090
2153
  def detect_figures(
2091
2154
  candidate_paths: list[str],
2092
2155
  reserved_paths: list[str] | tuple[str, ...] = (),
2156
+ ignore: pathspec.PathSpec | None = None,
2093
2157
  ) -> list[str]:
2094
2158
  """Auto-detected figure paths among ``candidate_paths``.
2095
2159
 
@@ -2103,7 +2167,7 @@ def detect_figures(
2103
2167
  for p in candidate_paths
2104
2168
  if not _is_hidden_path(p)
2105
2169
  and not _is_under_any_dir(p, reserved)
2106
- and is_figure_path(p)
2170
+ and is_figure_path(p, ignore=ignore)
2107
2171
  }
2108
2172
  )
2109
2173
 
@@ -2112,6 +2176,7 @@ def detect_datasets(
2112
2176
  candidate_paths: list[str],
2113
2177
  reserved_paths: list[str] | tuple[str, ...] = (),
2114
2178
  figure_paths: list[str] | tuple[str, ...] = (),
2179
+ ignore: pathspec.PathSpec | None = None,
2115
2180
  ) -> list[str]:
2116
2181
  """Auto-detected dataset paths among ``candidate_paths``.
2117
2182
 
@@ -2126,7 +2191,7 @@ def detect_datasets(
2126
2191
  if not _is_hidden_path(p)
2127
2192
  and not _is_under_any_dir(p, reserved)
2128
2193
  and p not in figset
2129
- and is_dataset_path(p)
2194
+ and is_dataset_path(p, ignore=ignore)
2130
2195
  }
2131
2196
  return _collapse_dataset_folders(sorted(files))
2132
2197
 
@@ -2134,6 +2199,7 @@ def detect_datasets(
2134
2199
  def detect_results(
2135
2200
  candidate_paths: list[str],
2136
2201
  reserved_paths: list[str] | tuple[str, ...] = (),
2202
+ ignore: pathspec.PathSpec | None = None,
2137
2203
  ) -> list[str]:
2138
2204
  """Auto-detected result paths among ``candidate_paths``.
2139
2205
 
@@ -2147,7 +2213,7 @@ def detect_results(
2147
2213
  for p in candidate_paths
2148
2214
  if not _is_hidden_path(p)
2149
2215
  and not _is_under_any_dir(p, reserved)
2150
- and is_result_path(p)
2216
+ and is_result_path(p, ignore=ignore)
2151
2217
  }
2152
2218
  )
2153
2219
 
@@ -2155,6 +2221,7 @@ def detect_results(
2155
2221
  def detect_presentations(
2156
2222
  candidate_paths: list[str],
2157
2223
  reserved_paths: list[str] | tuple[str, ...] = (),
2224
+ ignore: pathspec.PathSpec | None = None,
2158
2225
  ) -> list[str]:
2159
2226
  """Auto-detected presentation paths among ``candidate_paths``."""
2160
2227
  reserved = list(reserved_paths)
@@ -2164,7 +2231,7 @@ def detect_presentations(
2164
2231
  for p in candidate_paths
2165
2232
  if not _is_hidden_path(p)
2166
2233
  and not _is_under_any_dir(p, reserved)
2167
- and is_presentation_path(p)
2234
+ and is_presentation_path(p, ignore=ignore)
2168
2235
  }
2169
2236
  )
2170
2237
 
@@ -2254,18 +2321,28 @@ def detect_project_artifacts(
2254
2321
 
2255
2322
  if ck_info is None:
2256
2323
  ck_info = calkit.load_calkit_info(wdir=wdir)
2324
+ ignore = load_detection_ignore(wdir=wdir)
2257
2325
  reserved = _reserved_artifact_paths(wdir=wdir, ck_info=ck_info)
2258
2326
  candidates = list(
2259
2327
  dict.fromkeys(
2260
2328
  [*list_repo_files(wdir=wdir), *list_dvc_tracked_files(wdir=wdir)]
2261
2329
  )
2262
2330
  )
2263
- figures = detect_figures(candidates, reserved_paths=reserved)
2331
+ figures = detect_figures(
2332
+ candidates, reserved_paths=reserved, ignore=ignore
2333
+ )
2264
2334
  datasets = detect_datasets(
2265
- candidates, reserved_paths=reserved, figure_paths=figures
2335
+ candidates,
2336
+ reserved_paths=reserved,
2337
+ figure_paths=figures,
2338
+ ignore=ignore,
2339
+ )
2340
+ results = detect_results(
2341
+ candidates, reserved_paths=reserved, ignore=ignore
2342
+ )
2343
+ presentations = detect_presentations(
2344
+ candidates, reserved_paths=reserved, ignore=ignore
2266
2345
  )
2267
- results = detect_results(candidates, reserved_paths=reserved)
2268
- presentations = detect_presentations(candidates, reserved_paths=reserved)
2269
2346
  return {
2270
2347
  "figures": figures,
2271
2348
  "datasets": datasets,
@@ -499,6 +499,23 @@ def _sync_one(
499
499
  # pipeline output that hasn't been produced yet on a fresh run)
500
500
  if workspace_hash is None and zip_hash is None:
501
501
  return None
502
+ # Workspace doesn't exist with no prior sync record while syncing to-zip —
503
+ # there's nothing to push into the zip (e.g., the zip was pulled but
504
+ # never unzipped into the workspace)
505
+ if (
506
+ workspace_hash is None
507
+ and last_sync_record is None
508
+ and direction == "to-zip"
509
+ ):
510
+ return None
511
+ # Zip doesn't exist with no prior sync record while syncing to-workspace —
512
+ # there's nothing to pull into the workspace
513
+ if (
514
+ zip_hash is None
515
+ and last_sync_record is None
516
+ and direction == "to-workspace"
517
+ ):
518
+ return None
502
519
  # Deletion + change on the other side is a conflict
503
520
  if workspace_deleted and zip_changed and direction == "both":
504
521
  raise RuntimeError(
@@ -337,3 +337,32 @@ def ensure_path_is_not_ignored(
337
337
  if target_repo.ignored(target_path) and _depth < 10:
338
338
  ensure_path_is_not_ignored(target_repo, target_path, _depth + 1)
339
339
  return True
340
+
341
+
342
+ def ensure_dvc_pointer_is_not_ignored(repo, path: str) -> None:
343
+ """Ensure the .dvc pointer for ``path`` will not be Git-ignored.
344
+
345
+ A broad pattern in a ``.gitignore`` (e.g. ``*.pdf*``) can also match the
346
+ ``<path>.dvc`` pointer DVC commits to Git, causing ``dvc add`` to fail with
347
+ "bad DVC file name ... is git-ignored". This appends a ``!*.dvc`` negation
348
+ to the ``.gitignore`` in the pointer's own directory (which wins under Git
349
+ precedence) so pointers stay tracked. Idempotent.
350
+ """
351
+ path = path.replace("\\", "/").rstrip("/")
352
+ pointer = path + ".dvc"
353
+ if pointer not in repo.ignored(pointer):
354
+ return
355
+ pointer_dir = os.path.dirname(pointer)
356
+ gitignore_path = os.path.join(repo.working_dir, pointer_dir, ".gitignore")
357
+ exception = "!*.dvc"
358
+ existing_lines = []
359
+ if os.path.isfile(gitignore_path):
360
+ with open(gitignore_path, "r", encoding="utf-8") as f:
361
+ existing_lines = f.read().splitlines()
362
+ if exception in existing_lines:
363
+ return # Already present
364
+ os.makedirs(os.path.dirname(gitignore_path), exist_ok=True)
365
+ with open(gitignore_path, "a", encoding="utf-8") as f:
366
+ if existing_lines and existing_lines[-1] != "":
367
+ f.write("\n")
368
+ f.write(exception + "\n")
@@ -351,13 +351,23 @@ class ResultsEvidence(BaseModel):
351
351
  explanation: str | None = None
352
352
 
353
353
 
354
+ class PublicationEvidence(BaseModel):
355
+ """Evidence in the form of a publication."""
356
+
357
+ kind: Literal["publication"] = "publication"
358
+ path: str
359
+ explanation: str | None = None
360
+
361
+
354
362
  class Question(BaseModel):
355
363
  """A question the project hopes to answer."""
356
364
 
357
365
  question: str
358
366
  hypothesis: str | None = None
359
367
  answer: str | None = None
360
- evidence: list[FigureEvidence | ResultsEvidence] | None = None
368
+ evidence: (
369
+ list[FigureEvidence | ResultsEvidence | PublicationEvidence] | None
370
+ ) = None
361
371
 
362
372
 
363
373
  class Dependency(BaseModel):