openproj 0.39.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 (284) hide show
  1. openproj-0.39.0/.gcloudignore +30 -0
  2. openproj-0.39.0/.gitattributes +1 -0
  3. openproj-0.39.0/.github/shards/coedit +43 -0
  4. openproj-0.39.0/.github/shards/editor +39 -0
  5. openproj-0.39.0/.github/shards/graph +36 -0
  6. openproj-0.39.0/.github/shards/rest +44 -0
  7. openproj-0.39.0/.github/shards/views +35 -0
  8. openproj-0.39.0/.github/workflows/ci.yml +257 -0
  9. openproj-0.39.0/.github/workflows/release.yml +83 -0
  10. openproj-0.39.0/.gitignore +22 -0
  11. openproj-0.39.0/.pre-commit-config.yaml +40 -0
  12. openproj-0.39.0/AGENTS.md +595 -0
  13. openproj-0.39.0/Dockerfile +53 -0
  14. openproj-0.39.0/LICENSE +41 -0
  15. openproj-0.39.0/PKG-INFO +158 -0
  16. openproj-0.39.0/README.md +132 -0
  17. openproj-0.39.0/deploy/RUNBOOK.md +469 -0
  18. openproj-0.39.0/deploy/boot.py +87 -0
  19. openproj-0.39.0/docs/EDITOR.md +1152 -0
  20. openproj-0.39.0/docs/QUEUE.md +461 -0
  21. openproj-0.39.0/docs/architecture.md +226 -0
  22. openproj-0.39.0/docs/data-model.md +304 -0
  23. openproj-0.39.0/docs/deferred-push.md +301 -0
  24. openproj-0.39.0/docs/drawings.md +753 -0
  25. openproj-0.39.0/docs/hackmd-observed.md +380 -0
  26. openproj-0.39.0/docs/probes/README.md +198 -0
  27. openproj-0.39.0/docs/probes/ci-durations-4core.txt +5279 -0
  28. openproj-0.39.0/docs/probes/ci-durations-after-cache.txt +761 -0
  29. openproj-0.39.0/docs/probes/ci-durations.txt +1137 -0
  30. openproj-0.39.0/docs/probes/ci-speed.md +1189 -0
  31. openproj-0.39.0/docs/probes/coedit-under-load.md +206 -0
  32. openproj-0.39.0/docs/probes/concurrency-audit.md +904 -0
  33. openproj-0.39.0/docs/probes/hackmd_probe.py +184 -0
  34. openproj-0.39.0/docs/probes/load/adversarial-tail.json +973 -0
  35. openproj-0.39.0/docs/probes/load/adversarial.json +902 -0
  36. openproj-0.39.0/docs/probes/load/coedit-churn.json +5789 -0
  37. openproj-0.39.0/docs/probes/load/coedit-same-room.json +17539 -0
  38. openproj-0.39.0/docs/probes/load/detail-cost.json +41 -0
  39. openproj-0.39.0/docs/probes/load/diverge.json +350 -0
  40. openproj-0.39.0/docs/probes/load/index-warm.json +15 -0
  41. openproj-0.39.0/docs/probes/load/lockout.json +406 -0
  42. openproj-0.39.0/docs/probes/load/mixed-rtt600.json +398 -0
  43. openproj-0.39.0/docs/probes/load/mixed.json +286 -0
  44. openproj-0.39.0/docs/probes/load/read-load.json +974 -0
  45. openproj-0.39.0/docs/probes/load/same-contention.json +288 -0
  46. openproj-0.39.0/docs/probes/load/write-different.json +1261 -0
  47. openproj-0.39.0/docs/probes/load/write-different.md +168 -0
  48. openproj-0.39.0/docs/probes/load/write-same.json +1048 -0
  49. openproj-0.39.0/docs/probes/load/write-same.md +282 -0
  50. openproj-0.39.0/docs/quickstart.md +141 -0
  51. openproj-0.39.0/docs/shape-up.md +78 -0
  52. openproj-0.39.0/gcloud_deploy.sh +425 -0
  53. openproj-0.39.0/pyproject.toml +66 -0
  54. openproj-0.39.0/seed/README.md +82 -0
  55. openproj-0.39.0/seed/config/cycles.yaml +13 -0
  56. openproj-0.39.0/seed/config/defaults.yaml +23 -0
  57. openproj-0.39.0/seed/config/holidays.yaml +22 -0
  58. openproj-0.39.0/seed/config/people.yaml +36 -0
  59. openproj-0.39.0/seed/cycles/0037.md +55 -0
  60. openproj-0.39.0/seed/issues/issue-778899.md +9 -0
  61. openproj-0.39.0/seed/issues/issue-a1b2c3.md +14 -0
  62. openproj-0.39.0/seed/issues/issue-b3c4d5.md +23 -0
  63. openproj-0.39.0/seed/issues/issue-c9d0e1.md +24 -0
  64. openproj-0.39.0/seed/issues/issue-d4e5f6.md +10 -0
  65. openproj-0.39.0/seed/issues/issue-e6f7a8.md +26 -0
  66. openproj-0.39.0/seed/notes/note-11aa22.md +22 -0
  67. openproj-0.39.0/seed/notes/note-33bb44.md +16 -0
  68. openproj-0.39.0/seed/notes/note-55cc66.md +15 -0
  69. openproj-0.39.0/seed/notes/note-66dd77.md +36 -0
  70. openproj-0.39.0/seed/notes/note-77ee88.md +27 -0
  71. openproj-0.39.0/seed/notes/note-88ff99.md +25 -0
  72. openproj-0.39.0/seed/pitches/pitch-0a0001--testing-rank-reproducibility.md +49 -0
  73. openproj-0.39.0/seed/pitches/pitch-0b0001--porting-throughflow.md +49 -0
  74. openproj-0.39.0/seed/pitches/pitch-0c0001--porting-the-bed.md +48 -0
  75. openproj-0.39.0/seed/pitches/pitch-0d0001--aroma-transport-convergence.md +48 -0
  76. openproj-0.39.0/seed/pitches/pitch-0e0001--burner-radiation-port.md +51 -0
  77. openproj-0.39.0/seed/pitches/pitch-0f0001--scan-operator-on-the-gpu-backend.md +76 -0
  78. openproj-0.39.0/seed/pitches/pitch-0f0002--chaff-optics-in-the-burner.md +64 -0
  79. openproj-0.39.0/seed/pitches/pitch-0f0003--tap-deck-rewrite.md +69 -0
  80. openproj-0.39.0/seed/products/prod-0f0001--kiln4py.md +27 -0
  81. openproj-0.39.0/seed/products/prod-0f0002--hearth.md +22 -0
  82. openproj-0.39.0/seed/projects/proj-000001--whole-roast.md +45 -0
  83. openproj-0.39.0/seed/projects/proj-000002--scan-backend.md +54 -0
  84. openproj-0.39.0/seed/tasks/task-0a1001--ci-for-the-standalone-driver-v1-5.md +47 -0
  85. openproj-0.39.0/seed/tasks/task-0a1002--implement-deterministic-means.md +44 -0
  86. openproj-0.39.0/seed/tasks/task-0b1001--port-the-air-side-throughflow.md +47 -0
  87. openproj-0.39.0/seed/tasks/task-0b1002--port-the-bed-side-throughflow.md +48 -0
  88. openproj-0.39.0/seed/tasks/task-0c1001--port-the-bed-solver.md +49 -0
  89. openproj-0.39.0/seed/tasks/task-0c1002--coordinate-with-kettleworth.md +47 -0
  90. openproj-0.39.0/seed/tasks/task-0d1001--flume-blend-coefficients.md +47 -0
  91. openproj-0.39.0/seed/tasks/task-0d1002--transport-module-halo-exchanges.md +46 -0
  92. openproj-0.39.0/seed/tasks/task-0d1003--drum-convergence-study.md +47 -0
  93. openproj-0.39.0/seed/tasks/task-0e1001--port-the-near-ir-burner.md +48 -0
  94. openproj-0.39.0/seed/tasks/task-0e1002--port-the-far-ir-burner.md +49 -0
  95. openproj-0.39.0/seed/tasks/task-0f1001--lower-the-scan-to-the-gpu-backend.md +54 -0
  96. openproj-0.39.0/seed/tasks/task-0f1002--bench-the-scan-against-the-cpu-reference.md +53 -0
  97. openproj-0.39.0/seed/tasks/task-0f1003--port-the-tap-deck-reader.md +42 -0
  98. openproj-0.39.0/seed/tasks/task-0f1004--wire-the-tap-deck-writer.md +46 -0
  99. openproj-0.39.0/seed/tasks/task-0f1005--retire-the-f2py-shim.md +45 -0
  100. openproj-0.39.0/src/openproj/__init__.py +3 -0
  101. openproj-0.39.0/src/openproj/auth.py +147 -0
  102. openproj-0.39.0/src/openproj/cli.py +782 -0
  103. openproj-0.39.0/src/openproj/coedit.py +405 -0
  104. openproj-0.39.0/src/openproj/github.py +290 -0
  105. openproj-0.39.0/src/openproj/index.py +834 -0
  106. openproj-0.39.0/src/openproj/model.py +3188 -0
  107. openproj-0.39.0/src/openproj/pusher.py +214 -0
  108. openproj-0.39.0/src/openproj/query.py +294 -0
  109. openproj-0.39.0/src/openproj/render/__init__.py +230 -0
  110. openproj-0.39.0/src/openproj/render/controls.py +4052 -0
  111. openproj-0.39.0/src/openproj/render/cycles.py +2239 -0
  112. openproj-0.39.0/src/openproj/render/deck.py +1712 -0
  113. openproj-0.39.0/src/openproj/render/detail.py +3435 -0
  114. openproj-0.39.0/src/openproj/render/editor.py +1752 -0
  115. openproj-0.39.0/src/openproj/render/env.py +165 -0
  116. openproj-0.39.0/src/openproj/render/export.py +71 -0
  117. openproj-0.39.0/src/openproj/render/graph.py +1519 -0
  118. openproj-0.39.0/src/openproj/render/help.py +369 -0
  119. openproj-0.39.0/src/openproj/render/hill.py +478 -0
  120. openproj-0.39.0/src/openproj/render/icons.py +297 -0
  121. openproj-0.39.0/src/openproj/render/markdown.py +614 -0
  122. openproj-0.39.0/src/openproj/render/records.py +340 -0
  123. openproj-0.39.0/src/openproj/render/rows.py +179 -0
  124. openproj-0.39.0/src/openproj/render/shell.py +3446 -0
  125. openproj-0.39.0/src/openproj/render/slides.py +949 -0
  126. openproj-0.39.0/src/openproj/render/styles.py +1360 -0
  127. openproj-0.39.0/src/openproj/render/table.py +4104 -0
  128. openproj-0.39.0/src/openproj/render/timeline.py +1016 -0
  129. openproj-0.39.0/src/openproj/render/tokens.py +814 -0
  130. openproj-0.39.0/src/openproj/schedule.py +570 -0
  131. openproj-0.39.0/src/openproj/store.py +2134 -0
  132. openproj-0.39.0/src/openproj/themes.py +164 -0
  133. openproj-0.39.0/src/openproj/vendor.py +238 -0
  134. openproj-0.39.0/src/openproj/web.py +4502 -0
  135. openproj-0.39.0/static/SHA256SUMS +8 -0
  136. openproj-0.39.0/static/VENDOR.md +594 -0
  137. openproj-0.39.0/static/ace-LICENSE.txt +24 -0
  138. openproj-0.39.0/static/ace.js +23 -0
  139. openproj-0.39.0/static/cytoscape.min.js +32 -0
  140. openproj-0.39.0/static/elk-LICENSE.txt +264 -0
  141. openproj-0.39.0/static/elk.bundled.js +6696 -0
  142. openproj-0.39.0/static/excalidraw-LICENSE.txt +21 -0
  143. openproj-0.39.0/static/excalidraw-fonts-LICENSE.txt +655 -0
  144. openproj-0.39.0/static/excalidraw.js +562 -0
  145. openproj-0.39.0/static/inter-LICENSE.txt +93 -0
  146. openproj-0.39.0/static/inter-latin-wght-normal.woff2 +0 -0
  147. openproj-0.39.0/static/keybinding-vim.js +8 -0
  148. openproj-0.39.0/static/mermaid-LICENSE.txt +21 -0
  149. openproj-0.39.0/static/mermaid.min.js +3636 -0
  150. openproj-0.39.0/static/yjs-LICENSE.txt +23 -0
  151. openproj-0.39.0/static/yjs.bundle.mjs +8 -0
  152. openproj-0.39.0/tests/browser.py +476 -0
  153. openproj-0.39.0/tests/cascade.py +379 -0
  154. openproj-0.39.0/tests/conftest.py +35 -0
  155. openproj-0.39.0/tests/fixtures/corpus/README.md +74 -0
  156. openproj-0.39.0/tests/fixtures/corpus/config/cycles.yaml +9 -0
  157. openproj-0.39.0/tests/fixtures/corpus/config/defaults.yaml +11 -0
  158. openproj-0.39.0/tests/fixtures/corpus/config/holidays.yaml +15 -0
  159. openproj-0.39.0/tests/fixtures/corpus/cycles/0037.md +44 -0
  160. openproj-0.39.0/tests/fixtures/corpus/cycles/0038.md +39 -0
  161. openproj-0.39.0/tests/fixtures/corpus/issues/issue-8e1a37.md +38 -0
  162. openproj-0.39.0/tests/fixtures/corpus/issues/issue-9f2b48.md +41 -0
  163. openproj-0.39.0/tests/fixtures/corpus/notes/note-a03c59.md +39 -0
  164. openproj-0.39.0/tests/fixtures/corpus/notes/note-b14d6a.md +33 -0
  165. openproj-0.39.0/tests/fixtures/corpus/pitches/pitch-1b3f9a--mpi-on-ci-verify-with-serial.md +72 -0
  166. openproj-0.39.0/tests/fixtures/corpus/pitches/pitch-2a7f3e--transport-port-blend-coeffs.md +143 -0
  167. openproj-0.39.0/tests/fixtures/corpus/pitches/pitch-3c9a41--throughflow-1d-cpu-single-node.md +123 -0
  168. openproj-0.39.0/tests/fixtures/corpus/pitches/pitch-48ea9e--transport-needs-exchange.md +69 -0
  169. openproj-0.39.0/tests/fixtures/corpus/pitches/pitch-5e7b1c--ci-standalone-driver-v1-5.md +133 -0
  170. openproj-0.39.0/tests/fixtures/corpus/pitches/pitch-6f2d18--scan-operator-on-the-gpu-backend.md +64 -0
  171. openproj-0.39.0/tests/fixtures/corpus/pitches/pitch-7b3e94--year-end-backend-hardening.md +66 -0
  172. openproj-0.39.0/tests/fixtures/corpus/products/prod-6d1a70--kiln4py.md +20 -0
  173. openproj-0.39.0/tests/fixtures/corpus/products/prod-7c2b81--hearth.md +37 -0
  174. openproj-0.39.0/tests/fixtures/corpus/projects/proj-7e57a0--testing.md +20 -0
  175. openproj-0.39.0/tests/fixtures/corpus/projects/proj-9a4c25--hearth-backend-for-the-bed-solver.md +47 -0
  176. openproj-0.39.0/tests/fixtures/corpus/tasks/task-0e4b7a--add-unit-tests-for-bed2drum.md +85 -0
  177. openproj-0.39.0/tests/fixtures/corpus/tasks/task-2b6c94--cleanup-mesh-shape-compute-coeffs-interface.md +42 -0
  178. openproj-0.39.0/tests/fixtures/corpus/tasks/task-31f6c4--verify-the-throughflow-scheme-in-kiln4py-using-recorded-test-data.md +37 -0
  179. openproj-0.39.0/tests/fixtures/corpus/tasks/task-3a52d8--compile-the-wrapper-with-f2py-to-generate-the-bedstep-f2py-module.md +36 -0
  180. openproj-0.39.0/tests/fixtures/corpus/tasks/task-3d84e9--create-the-interface-for-running-throughflow-in-kiln4py.md +42 -0
  181. openproj-0.39.0/tests/fixtures/corpus/tasks/task-3e07b2--generate-python-bindings-with-f2py-for-bedstep-setup-config-and-bedstep-run.md +37 -0
  182. openproj-0.39.0/tests/fixtures/corpus/tasks/task-53a9f0--reproduce-2gpu-seam-artefact.md +33 -0
  183. openproj-0.39.0/tests/fixtures/corpus/tasks/task-58d7c6--per-field-delta-tolerances.md +27 -0
  184. openproj-0.39.0/tests/fixtures/corpus/tasks/task-5a4e39--read-the-2014-stable-summation-paper.md +28 -0
  185. openproj-0.39.0/tests/fixtures/corpus/tasks/task-5c1d84--try-deterministic-means.md +30 -0
  186. openproj-0.39.0/tests/fixtures/corpus/tasks/task-5f062b--downgrade-numpy-for-global-sums.md +28 -0
  187. openproj-0.39.0/tests/fixtures/corpus/tasks/task-6a5c02--lower-the-scan-operator.md +41 -0
  188. openproj-0.39.0/tests/fixtures/corpus/tasks/task-6b7d31--bench-the-scan-against-the-cpu-reference.md +36 -0
  189. openproj-0.39.0/tests/fixtures/corpus/tasks/task-7c8e40--freeze-the-backend-api.md +54 -0
  190. openproj-0.39.0/tests/fixtures/corpus/tasks/task-7d9f52--rerun-the-backend-suite-after-the-shutdown.md +42 -0
  191. openproj-0.39.0/tests/fixtures/roundtrip.md +39 -0
  192. openproj-0.39.0/tests/js/drive.js +921 -0
  193. openproj-0.39.0/tests/js/yjs-seed.js +64 -0
  194. openproj-0.39.0/tests/load/adversarial.py +1026 -0
  195. openproj-0.39.0/tests/load/churn.py +1328 -0
  196. openproj-0.39.0/tests/load/coedit_same_room.py +834 -0
  197. openproj-0.39.0/tests/load/corpus.py +160 -0
  198. openproj-0.39.0/tests/load/counters.py +138 -0
  199. openproj-0.39.0/tests/load/detail_cost.py +166 -0
  200. openproj-0.39.0/tests/load/diverge.py +233 -0
  201. openproj-0.39.0/tests/load/divergence.py +215 -0
  202. openproj-0.39.0/tests/load/drive.py +177 -0
  203. openproj-0.39.0/tests/load/harness.py +427 -0
  204. openproj-0.39.0/tests/load/herd.py +111 -0
  205. openproj-0.39.0/tests/load/index_warm.py +131 -0
  206. openproj-0.39.0/tests/load/lockout.py +282 -0
  207. openproj-0.39.0/tests/load/lost_line.py +115 -0
  208. openproj-0.39.0/tests/load/measure.py +164 -0
  209. openproj-0.39.0/tests/load/merge_probe.py +83 -0
  210. openproj-0.39.0/tests/load/micro.py +127 -0
  211. openproj-0.39.0/tests/load/probe_ceiling.py +89 -0
  212. openproj-0.39.0/tests/load/probe_emoji.py +118 -0
  213. openproj-0.39.0/tests/load/probe_fanout.py +109 -0
  214. openproj-0.39.0/tests/load/probe_manyrooms.py +157 -0
  215. openproj-0.39.0/tests/load/probe_saverace.py +144 -0
  216. openproj-0.39.0/tests/load/probe_seats.py +128 -0
  217. openproj-0.39.0/tests/load/probe_shutdown.py +129 -0
  218. openproj-0.39.0/tests/load/probe_twoinstances.py +112 -0
  219. openproj-0.39.0/tests/load/probe_writecost.py +178 -0
  220. openproj-0.39.0/tests/load/queueing.py +212 -0
  221. openproj-0.39.0/tests/load/readload.py +526 -0
  222. openproj-0.39.0/tests/load/room.py +340 -0
  223. openproj-0.39.0/tests/load/rooms.py +139 -0
  224. openproj-0.39.0/tests/load/run.py +362 -0
  225. openproj-0.39.0/tests/load/run.sh +78 -0
  226. openproj-0.39.0/tests/load/serve_load.py +109 -0
  227. openproj-0.39.0/tests/load/server.py +70 -0
  228. openproj-0.39.0/tests/load/users.py +591 -0
  229. openproj-0.39.0/tests/load/verify.py +435 -0
  230. openproj-0.39.0/tests/load/write_same.py +738 -0
  231. openproj-0.39.0/tests/load/writer_ceiling.py +190 -0
  232. openproj-0.39.0/tests/pages.py +336 -0
  233. openproj-0.39.0/tests/plans.py +156 -0
  234. openproj-0.39.0/tests/test_auth.py +602 -0
  235. openproj-0.39.0/tests/test_card.py +601 -0
  236. openproj-0.39.0/tests/test_cascade.py +1373 -0
  237. openproj-0.39.0/tests/test_cli.py +912 -0
  238. openproj-0.39.0/tests/test_coedit.py +4888 -0
  239. openproj-0.39.0/tests/test_config.py +259 -0
  240. openproj-0.39.0/tests/test_credential.py +275 -0
  241. openproj-0.39.0/tests/test_deck.py +1356 -0
  242. openproj-0.39.0/tests/test_delete.py +400 -0
  243. openproj-0.39.0/tests/test_edges.py +222 -0
  244. openproj-0.39.0/tests/test_editor.py +8753 -0
  245. openproj-0.39.0/tests/test_exclusion.py +400 -0
  246. openproj-0.39.0/tests/test_facets.py +570 -0
  247. openproj-0.39.0/tests/test_gitdoor.py +641 -0
  248. openproj-0.39.0/tests/test_graph_layout.py +740 -0
  249. openproj-0.39.0/tests/test_harness.py +108 -0
  250. openproj-0.39.0/tests/test_headers.py +130 -0
  251. openproj-0.39.0/tests/test_help.py +352 -0
  252. openproj-0.39.0/tests/test_hill.py +918 -0
  253. openproj-0.39.0/tests/test_identity.py +198 -0
  254. openproj-0.39.0/tests/test_index.py +1278 -0
  255. openproj-0.39.0/tests/test_injection.py +944 -0
  256. openproj-0.39.0/tests/test_issues.py +491 -0
  257. openproj-0.39.0/tests/test_mermaid.py +430 -0
  258. openproj-0.39.0/tests/test_model.py +400 -0
  259. openproj-0.39.0/tests/test_notes.py +656 -0
  260. openproj-0.39.0/tests/test_parse.py +268 -0
  261. openproj-0.39.0/tests/test_payload.py +235 -0
  262. openproj-0.39.0/tests/test_product.py +579 -0
  263. openproj-0.39.0/tests/test_records.py +741 -0
  264. openproj-0.39.0/tests/test_remote.py +2084 -0
  265. openproj-0.39.0/tests/test_render.py +6256 -0
  266. openproj-0.39.0/tests/test_schedule.py +994 -0
  267. openproj-0.39.0/tests/test_search.py +470 -0
  268. openproj-0.39.0/tests/test_seats.py +1340 -0
  269. openproj-0.39.0/tests/test_socket_offer.py +108 -0
  270. openproj-0.39.0/tests/test_status_gate.py +314 -0
  271. openproj-0.39.0/tests/test_store.py +1551 -0
  272. openproj-0.39.0/tests/test_table.py +5840 -0
  273. openproj-0.39.0/tests/test_teaching.py +609 -0
  274. openproj-0.39.0/tests/test_themes.py +317 -0
  275. openproj-0.39.0/tests/test_validate.py +1046 -0
  276. openproj-0.39.0/tests/test_web.py +6292 -0
  277. openproj-0.39.0/tests/test_writes.py +693 -0
  278. openproj-0.39.0/tests/wsclient.py +105 -0
  279. openproj-0.39.0/tools/README.md +87 -0
  280. openproj-0.39.0/tools/build-excalidraw.mjs +282 -0
  281. openproj-0.39.0/tools/excalidraw-entry.js +22 -0
  282. openproj-0.39.0/tools/excalidraw-package-lock.json +3442 -0
  283. openproj-0.39.0/tools/excalidraw-package.json +13 -0
  284. openproj-0.39.0/uv.lock +915 -0
@@ -0,0 +1,30 @@
1
+ # What NOT to upload to Cloud Build. Without this file gcloud uploads the whole
2
+ # directory — .git is 20 MB and .venv is 101 MB, none of which the image uses,
3
+ # on every single build.
4
+ #
5
+ # Deliberately not the default gcloudignore behaviour of honouring .gitignore:
6
+ # .venv is git-ignored and would be excluded, but so is nothing else that
7
+ # matters, and an ignore file that silently follows another file is one more
8
+ # place a build can lose something it needed. Everything here is explicit.
9
+
10
+ .git/
11
+ .gitignore
12
+ .worktrees/
13
+ .venv/
14
+ **/__pycache__/
15
+ **/*.pyc
16
+ .pytest_cache/
17
+ .ruff_cache/
18
+
19
+ # Not part of the running service.
20
+ #
21
+ # `docs/` was here and is not: the Help page reads those files off the disk at
22
+ # render time, so they ARE part of the running service now. The build failed at
23
+ # `COPY docs/` with "file not found in build context or excluded by
24
+ # .dockerignore" — on a commit whose whole test suite was green, because nothing
25
+ # in CI builds an image. `test_the_container_carries_the_documents` reads this
26
+ # file as well as the Dockerfile for exactly that reason: two files decide
27
+ # whether a document reaches production and only one of them was being checked.
28
+ tests/
29
+ deploy/RUNBOOK.md
30
+ gcloud_deploy.sh
@@ -0,0 +1 @@
1
+ uv.lock merge=binary
@@ -0,0 +1,43 @@
1
+ # coedit — 106.5s exact, anchored on tests/test_coedit.py (76.4s).
2
+ #
3
+ # Seconds are per FILE and EXACT, from docs/probes/ci-durations-4core.txt — every
4
+ # phase of every test on the 4-core public runner, run 32754826551. The older
5
+ # tables in docs/probes/ were taken on the 2-core box a PRIVATE repository gets,
6
+ # AND at `--durations-min=0.05`, and both halves of that were wrong for this cut:
7
+ # the leg order inverted when the cores doubled, and the 50ms floor hid 37s of
8
+ # the suite on exactly the leg that had 1103 tests. Modelling that hidden cost
9
+ # was tried and the fit did not hold — see docs/probes/ci-speed.md § 0b. The
10
+ # threshold is zero here, so there is nothing left to model.
11
+ #
12
+ # The cut is what a longest-first pass makes of those numbers, and it lands at
13
+ # 106.4-106.5s a leg: a 0.1s spread, against 93.0-119.8s on the cut it replaces.
14
+ # `tests/test_editor.py` is 101.6s of the 106.4s it sits in, so this is 5s off
15
+ # the floor and there is nothing further to win by moving files.
16
+ #
17
+ # **Balance is not what to spend the next hour on.** Runs of the SAME lists put a
18
+ # leg 44s apart — `views` measured 76.57s and 120.37s across four — so no re-cut
19
+ # is visible in one run, and four were not enough to separate this cut from the
20
+ # one it replaced (120.4s against 122.8s, in either direction depending on which
21
+ # runs you take). What would actually move the gate is the floor, and the floor
22
+ # is one file.
23
+ #
24
+ # Real sockets, a real uvicorn on a real port, and literal `sleep(0.5)` budgets.
25
+ # This leg exists so those stay serial, in file order, in one process, on a
26
+ # machine nothing else is running AT THE SAME TIME — the condition every one of
27
+ # those margins was measured under. Ballast is fine and always was: the other
28
+ # files here run before or after it in the same process, never beside it.
29
+ #
30
+ # **What must never happen is this leg being internally parallelised**, and that
31
+ # is also why `pytest-xdist` cannot help the gate even where it helps a leg: the
32
+ # critical path is bounded by a leg that is not allowed to split, so making the
33
+ # other four faster buys total machine seconds and zero wall clock. Measured, in
34
+ # docs/probes/ci-speed.md § 0b.
35
+ tests/test_coedit.py
36
+ tests/test_hill.py
37
+ tests/test_deck.py
38
+ tests/test_gitdoor.py
39
+ tests/test_cli.py
40
+ tests/test_headers.py
41
+ tests/test_product.py
42
+ tests/test_identity.py
43
+ tests/test_socket_offer.py
@@ -0,0 +1,39 @@
1
+ # editor — 106.4s exact, anchored on tests/test_editor.py (101.6s).
2
+ #
3
+ # Seconds are per FILE and EXACT, from docs/probes/ci-durations-4core.txt — every
4
+ # phase of every test on the 4-core public runner, run 32754826551. The older
5
+ # tables in docs/probes/ were taken on the 2-core box a PRIVATE repository gets,
6
+ # AND at `--durations-min=0.05`, and both halves of that were wrong for this cut:
7
+ # the leg order inverted when the cores doubled, and the 50ms floor hid 37s of
8
+ # the suite on exactly the leg that had 1103 tests. Modelling that hidden cost
9
+ # was tried and the fit did not hold — see docs/probes/ci-speed.md § 0b. The
10
+ # threshold is zero here, so there is nothing left to model.
11
+ #
12
+ # The cut is what a longest-first pass makes of those numbers, and it lands at
13
+ # 106.4-106.5s a leg: a 0.1s spread, against 93.0-119.8s on the cut it replaces.
14
+ # `tests/test_editor.py` is 101.6s of the 106.4s it sits in, so this is 5s off
15
+ # the floor and there is nothing further to win by moving files.
16
+ #
17
+ # **Balance is not what to spend the next hour on.** Runs of the SAME lists put a
18
+ # leg 44s apart — `views` measured 76.57s and 120.37s across four — so no re-cut
19
+ # is visible in one run, and four were not enough to separate this cut from the
20
+ # one it replaced (120.4s against 122.8s, in either direction depending on which
21
+ # runs you take). What would actually move the gate is the floor, and the floor
22
+ # is one file.
23
+ #
24
+ # THE FLOOR. 105 tests, 53 of the suite's real-Chrome call sites, each a fresh
25
+ # browser laying out a page carrying 594 KB of inlined Ace. 101.6s on its own, so
26
+ # no arrangement of these lists puts a leg below it and the gate cannot go under
27
+ # about 102s of pytest however many machines it is given. It is also the reason
28
+ # `pytest-xdist` was measured and refused: `-n 2` on this leg is 27% SLOWER, not
29
+ # faster, because the cost is process startup and two Chromes on four cores
30
+ # contend. Going below 101.6s means splitting the file, and its natural seam is
31
+ # still `?editor=ace` against `?editor=plain`.
32
+ tests/test_editor.py
33
+ tests/test_writes.py
34
+ tests/test_notes.py
35
+ tests/test_issues.py
36
+ tests/test_harness.py
37
+ tests/test_config.py
38
+ tests/test_model.py
39
+ tests/test_auth.py
@@ -0,0 +1,36 @@
1
+ # graph — 106.5s exact, anchored on tests/test_graph_layout.py (32.2s).
2
+ #
3
+ # Seconds are per FILE and EXACT, from docs/probes/ci-durations-4core.txt — every
4
+ # phase of every test on the 4-core public runner, run 32754826551. The older
5
+ # tables in docs/probes/ were taken on the 2-core box a PRIVATE repository gets,
6
+ # AND at `--durations-min=0.05`, and both halves of that were wrong for this cut:
7
+ # the leg order inverted when the cores doubled, and the 50ms floor hid 37s of
8
+ # the suite on exactly the leg that had 1103 tests. Modelling that hidden cost
9
+ # was tried and the fit did not hold — see docs/probes/ci-speed.md § 0b. The
10
+ # threshold is zero here, so there is nothing left to model.
11
+ #
12
+ # The cut is what a longest-first pass makes of those numbers, and it lands at
13
+ # 106.4-106.5s a leg: a 0.1s spread, against 93.0-119.8s on the cut it replaces.
14
+ # `tests/test_editor.py` is 101.6s of the 106.4s it sits in, so this is 5s off
15
+ # the floor and there is nothing further to win by moving files.
16
+ #
17
+ # **Balance is not what to spend the next hour on.** Runs of the SAME lists put a
18
+ # leg 44s apart — `views` measured 76.57s and 120.37s across four — so no re-cut
19
+ # is visible in one run, and four were not enough to separate this cut from the
20
+ # one it replaced (120.4s against 122.8s, in either direction depending on which
21
+ # runs you take). What would actually move the gate is the floor, and the floor
22
+ # is one file.
23
+ #
24
+ # The files whose cost is starting something — a fresh headless Chrome per canvas
25
+ # assertion, a fresh node per injection census — rather than rendering. This is
26
+ # the group that gained most from four cores: it was 131.2s on two and 93.0s on
27
+ # four, which is most of why the old cut inverted.
28
+ tests/test_graph_layout.py
29
+ tests/test_facets.py
30
+ tests/test_injection.py
31
+ tests/test_web.py
32
+ tests/test_themes.py
33
+ tests/test_payload.py
34
+ tests/test_records.py
35
+ tests/test_exclusion.py
36
+ tests/test_credential.py
@@ -0,0 +1,44 @@
1
+ # rest — 106.4s exact, anchored on tests/test_table.py (38.9s).
2
+ #
3
+ # Seconds are per FILE and EXACT, from docs/probes/ci-durations-4core.txt — every
4
+ # phase of every test on the 4-core public runner, run 32754826551. The older
5
+ # tables in docs/probes/ were taken on the 2-core box a PRIVATE repository gets,
6
+ # AND at `--durations-min=0.05`, and both halves of that were wrong for this cut:
7
+ # the leg order inverted when the cores doubled, and the 50ms floor hid 37s of
8
+ # the suite on exactly the leg that had 1103 tests. Modelling that hidden cost
9
+ # was tried and the fit did not hold — see docs/probes/ci-speed.md § 0b. The
10
+ # threshold is zero here, so there is nothing left to model.
11
+ #
12
+ # The cut is what a longest-first pass makes of those numbers, and it lands at
13
+ # 106.4-106.5s a leg: a 0.1s spread, against 93.0-119.8s on the cut it replaces.
14
+ # `tests/test_editor.py` is 101.6s of the 106.4s it sits in, so this is 5s off
15
+ # the floor and there is nothing further to win by moving files.
16
+ #
17
+ # **Balance is not what to spend the next hour on.** Runs of the SAME lists put a
18
+ # leg 44s apart — `views` measured 76.57s and 120.37s across four — so no re-cut
19
+ # is visible in one run, and four were not enough to separate this cut from the
20
+ # one it replaced (120.4s against 122.8s, in either direction depending on which
21
+ # runs you take). What would actually move the gate is the floor, and the floor
22
+ # is one file.
23
+ #
24
+ # NO LONGER "everything else". It is the table, the query language, the hover
25
+ # card and the resolved cascade, plus what balances them. The name is kept
26
+ # anyway: renaming a matrix value renames a status check, the one check branch
27
+ # protection requires is `check`, which fans these in, and a rename is a
28
+ # branch-protection edit in exchange for a word.
29
+ #
30
+ # **Where a new test file goes.** It used to be "here, unless it is slow enough
31
+ # to be worth naming", and that rule went with the property that justified it. It
32
+ # goes on whichever leg the newest table in docs/probes/ says is cheapest, and
33
+ # `test_every_test_file_is_in_exactly_one_ci_shard` in tests/test_harness.py is
34
+ # what stops it going on no leg at all.
35
+ tests/test_table.py
36
+ tests/test_mermaid.py
37
+ tests/test_search.py
38
+ tests/test_card.py
39
+ tests/test_cascade.py
40
+ tests/test_edges.py
41
+ tests/test_teaching.py
42
+ tests/test_index.py
43
+ tests/test_parse.py
44
+ tests/test_store.py
@@ -0,0 +1,35 @@
1
+ # views — 106.4s exact, anchored on tests/test_render.py (76.4s).
2
+ #
3
+ # Seconds are per FILE and EXACT, from docs/probes/ci-durations-4core.txt — every
4
+ # phase of every test on the 4-core public runner, run 32754826551. The older
5
+ # tables in docs/probes/ were taken on the 2-core box a PRIVATE repository gets,
6
+ # AND at `--durations-min=0.05`, and both halves of that were wrong for this cut:
7
+ # the leg order inverted when the cores doubled, and the 50ms floor hid 37s of
8
+ # the suite on exactly the leg that had 1103 tests. Modelling that hidden cost
9
+ # was tried and the fit did not hold — see docs/probes/ci-speed.md § 0b. The
10
+ # threshold is zero here, so there is nothing left to model.
11
+ #
12
+ # The cut is what a longest-first pass makes of those numbers, and it lands at
13
+ # 106.4-106.5s a leg: a 0.1s spread, against 93.0-119.8s on the cut it replaces.
14
+ # `tests/test_editor.py` is 101.6s of the 106.4s it sits in, so this is 5s off
15
+ # the floor and there is nothing further to win by moving files.
16
+ #
17
+ # **Balance is not what to spend the next hour on.** Runs of the SAME lists put a
18
+ # leg 44s apart — `views` measured 76.57s and 120.37s across four — so no re-cut
19
+ # is visible in one run, and four were not enough to separate this cut from the
20
+ # one it replaced (120.4s against 122.8s, in either direction depending on which
21
+ # runs you take). What would actually move the gate is the floor, and the floor
22
+ # is one file.
23
+ #
24
+ # `tests/test_render.py` draws every page and then measures it, and it is the
25
+ # second-largest file in the suite. `tests/test_seats.py` is beside it rather
26
+ # than on `coedit` because a seat band is drawn by the same rendering path, and
27
+ # because `coedit` had no room for it.
28
+ tests/test_render.py
29
+ tests/test_help.py
30
+ tests/test_seats.py
31
+ tests/test_delete.py
32
+ tests/test_status_gate.py
33
+ tests/test_schedule.py
34
+ tests/test_remote.py
35
+ tests/test_validate.py
@@ -0,0 +1,257 @@
1
+ name: CI
2
+
3
+ # Pull requests before the merge, and `main` after it. Gating only the PR leaves
4
+ # the branch everything is cut from unverified the moment two separately-green
5
+ # PRs land and interact.
6
+ #
7
+ # EVERY pull request, and not only the ones based on `main`. The filter that used
8
+ # to be here was `branches: [main]`, and what it did to a stacked PR is the worst
9
+ # available failure: #92 was opened against the `thinking` branch, GitHub matched
10
+ # no trigger, and `gh pr checks` answered "no checks reported" — which is the same
11
+ # answer it gives while a run is still queuing. A branch built on an unmerged one
12
+ # therefore sat un-gated and looked exactly like a branch waiting its turn.
13
+ #
14
+ # AGENTS.md already documents the sibling trap, where a `CONFLICTING` PR has no
15
+ # `refs/pull/<n>/merge` for the workflow to check out, and says to ask
16
+ # `gh pr view <n> --json mergeable` before asking anything else. That question
17
+ # answers `MERGEABLE`/`CLEAN` here, so the documented check does not find this
18
+ # one. Removing the filter is what makes the check trustworthy again.
19
+ #
20
+ # `push` stays pinned to `main`: a push run gates the branch everything is cut
21
+ # from, and every other branch already reaches this workflow through its PR.
22
+ on:
23
+ pull_request:
24
+ push:
25
+ branches: [main]
26
+
27
+ # A PR's third push makes the first two runs' answers worthless, and they go on
28
+ # holding a runner while the answer that matters queues behind them. With six
29
+ # jobs per run instead of one this matters six times as much.
30
+ concurrency:
31
+ group: ci-${{ github.ref }}
32
+ cancel-in-progress: true
33
+
34
+ # Three job kinds, and the reason there are three is measured in
35
+ # `docs/probes/ci-speed.md`: a job on this repository costs about a dozen seconds
36
+ # of setup, not the ninety everybody assumes, so buying a whole extra machine is
37
+ # cheaper than any arrangement of processes on one machine.
38
+ #
39
+ # **The runner is probed, not assumed, and the answer changed under this file.**
40
+ # It said 2-core / 8 GB, citing run 32633361097, and added "`ubuntu-latest` is
41
+ # 4-core only for public repositories and this one is private". The repository
42
+ # went public on 2026-08-23 and the box is now **4-core / 15.6 GB**, probed again
43
+ # in run 32753487844 — the probe step below prints it on every leg, because "the
44
+ # runner" is five runners and nothing promises they stay the same size.
45
+ #
46
+ # That did not change the answer and it did change the sums. Five machines still
47
+ # beat processes on one, and the shard cut was re-made from
48
+ # `docs/probes/ci-durations-4core.txt` because the old one had INVERTED: the leg
49
+ # order on 2 cores was editor > views > graph > rest > coedit and on 4 cores it
50
+ # is rest > views > coedit > editor > graph. The Chrome-heavy legs took the extra
51
+ # cores and the plain-Python one did not.
52
+ jobs:
53
+ # Ruff alone, on its own machine, finishing in about twenty-five seconds.
54
+ #
55
+ # Worth ZERO seconds of wall clock on a green run, and that is worth saying
56
+ # plainly because the theory it looks like it comes from — "ruff runs serially
57
+ # in front of a twenty-minute suite" — was measured at 0s and is false. What it
58
+ # buys is how fast a RED run answers: a misplaced import is reported in
59
+ # twenty-five seconds instead of after the whole suite. The shards deliberately
60
+ # do NOT `needs: [lint]`; a lint error and a test failure are different
61
+ # questions and you want both answers from one push.
62
+ lint:
63
+ runs-on: ubuntu-latest
64
+ steps:
65
+ - uses: actions/checkout@v7
66
+
67
+ # Pinned to an exact release rather than a major, and alone in that: astral
68
+ # stopped publishing moving major tags after v7, so `@v10` is a 404 and not
69
+ # a pin. Bump this deliberately; the other two follow their major.
70
+ - uses: astral-sh/setup-uv@v10.0.1
71
+ with:
72
+ # 3.12 because that is what the Dockerfile ships and what ruff targets.
73
+ # A gate that is green on a version the deploy never runs is not a gate.
74
+ python-version: "3.12"
75
+ enable-cache: true
76
+
77
+ # `--locked`, as in the Dockerfile: it fails on a `pyproject.toml` edited
78
+ # without relocking, instead of quietly testing a resolution that will
79
+ # never be the one deployed. Doing it here as well as in the shards is not
80
+ # duplication for its own sake — this is the job that reaches the sync step
81
+ # first, so a bump-without-relock is red in twenty-five seconds instead of
82
+ # in whatever the slowest shard costs.
83
+ - run: uv sync --locked --group dev
84
+
85
+ - run: uv run ruff check .
86
+
87
+ # No `ruff format --check` yet, on purpose: 32 of the 103 files predate any
88
+ # formatter and would fail it today, so adding it now commits a gate that
89
+ # is red on arrival — and a gate that is red on arrival teaches everybody
90
+ # to read past red. One `uv run ruff format .`, as its own commit touching
91
+ # nothing else, is what makes this line safe to add.
92
+ #
93
+ # Nothing here checks the shard lists, on purpose. They are hand-written
94
+ # and a hand-written list of files fails OPEN — add `tests/test_new.py`,
95
+ # forget the list, and it runs on no machine at all while every job stays
96
+ # green — but that is already held, once, by
97
+ # `test_every_test_file_is_in_exactly_one_ci_shard` in `tests/test_harness.py`,
98
+ # which reads the same files and answers on a laptop as well as here. An
99
+ # invariant written twice gets guarded once.
100
+
101
+ # The suite, cut five ways across five machines.
102
+ #
103
+ # The cut is in `.github/shards/<name>`, one path per line, and it is made from
104
+ # `docs/probes/ci-durations-4core.txt` — this hardware's table, not either of
105
+ # the 2-core ones, because the legs did not merely shrink when the cores
106
+ # doubled, they changed order. And at `--durations-min=0`, because the 0.05
107
+ # threshold the older tables used hid 37s of the suite, almost none of it
108
+ # evenly: 16s of that was on the leg with 1103 tests. 532.3s of exact rows over
109
+ # 42 files. Per leg the cut is 106.4-106.5s, a spread of 0.1s, against
110
+ # 93.0-119.8s for the lists it replaces.
111
+ #
112
+ # **That is a 13s improvement in expectation and it is invisible in any single
113
+ # run.** Two runs of identical lists put one leg 23s apart. Quote a median over
114
+ # several runs or quote nothing, and do not spend an afternoon re-cutting: what
115
+ # bounds this gate is `tests/test_editor.py` at 101.6s, which is one file.
116
+ #
117
+ # FIVE, and five is still the ceiling on four cores — re-derived rather than
118
+ # inherited. The break-even for a shard is `T/(K·(K−1)) > setup`; at T≈501s of
119
+ # attributed cost and ~12s of setup that allows six, and balance stops it
120
+ # first, exactly as it did before. `tests/test_editor.py` is 94.4s on its own —
121
+ # 105 tests, 53 of the suite's real-Chrome call sites, every one laying out a
122
+ # page carrying 594 KB of inlined Ace — and no file-level scheme goes below
123
+ # that floor. A perfect five-way is 100.3s of attributed cost; a perfect
124
+ # six-way would be 83.6s and cannot be reached, because the sixth machine lands
125
+ # on this one file at 94.4s. Six machines to save six seconds, and a seventh
126
+ # billing rounding to pay for it. Going below 94.4s means splitting that one
127
+ # file, and its natural seam is still `?editor=ace` against `?editor=plain`.
128
+ #
129
+ # `coedit` is a shard for the opposite reason to `editor`: it is the only leg
130
+ # with real sockets, a real uvicorn on a real port, and literal `sleep(0.5)`
131
+ # budgets. It must not be reordered or internally parallelised, and its own
132
+ # machine is how that stays true for free. That is also the one constraint the
133
+ # re-cut was not allowed to optimise away, and it is why a `-n 2` inside each
134
+ # shard could never be a blanket answer even if it paid on the other four.
135
+ suite:
136
+ strategy:
137
+ # One red shard must not cancel the other four. The whole point of a
138
+ # three-minute gate is getting every piece of bad news from one push.
139
+ fail-fast: false
140
+ matrix:
141
+ # Longest first: legs start roughly in this order, and `editor` is the
142
+ # critical path, so it should never be the one waiting for a runner.
143
+ shard: [editor, views, graph, coedit, rest]
144
+ runs-on: ubuntu-latest
145
+ steps:
146
+ # Full checkout, never sparse — the obvious optimisation here is wrong. 27
147
+ # of the 47 files in `tests/` import from another file in `tests/`
148
+ # (`test_editor.py` alone imports `test_web`, `test_store`,
149
+ # `test_injection`, `pages` and `browser`), and `tests/test_remote.py:375`
150
+ # asks for a fixture defined in `test_store` by name. A shard missing an
151
+ # import hub fails at COLLECTION, which is a shard reporting nothing rather
152
+ # than a shard reporting a failure. The whole tree is one second.
153
+ - uses: actions/checkout@v7
154
+
155
+ # Pinned to an exact release rather than a major, and alone in that: astral
156
+ # stopped publishing moving major tags after v7, so `@v10` is a 404 and not
157
+ # a pin. Bump this deliberately; the other two follow their major.
158
+ - uses: astral-sh/setup-uv@v10.0.1
159
+ with:
160
+ # 3.12 because that is what the Dockerfile ships and what ruff targets.
161
+ # A gate that is green on a version the deploy never runs is not a gate.
162
+ python-version: "3.12"
163
+ enable-cache: true
164
+
165
+ # Thirty-four tests drive the shipped JavaScript through `tests/js/drive.js`
166
+ # and skip themselves when node is missing, so without this step the suite
167
+ # goes green by not running them — green for the wrong reason, which is the
168
+ # one failure a gate must never have. node is needed to test, never to
169
+ # build or run: nothing here is a build step, and there is no npm. Every
170
+ # shard installs it, including the ones nobody thinks need it: a shard that
171
+ # silently stops driving JS is exactly the failure this step exists to
172
+ # prevent, and eight seconds of cached tarball is not worth reasoning about
173
+ # per leg.
174
+ - uses: actions/setup-node@v7
175
+ with:
176
+ node-version: "24"
177
+
178
+ # WHAT THIS MACHINE ACTUALLY IS. The paragraph above `jobs:` said 2-core /
179
+ # 8 GB and cited a run id, which was the right way to settle it and is now
180
+ # the wrong answer: that measurement was taken while this repository was
181
+ # PRIVATE, and a public repository gets a different box. The number decided
182
+ # the whole shape of this file — five machines rather than `-n auto` on one
183
+ # — so it is not a number to inherit from a document. It is printed on
184
+ # every leg rather than probed once, because "the runner" is five runners
185
+ # and nothing promises they are the same size.
186
+ - run: |
187
+ echo "nproc: $(nproc)"
188
+ echo "memory: $(awk '/MemTotal/{printf "%.1f GB", $2/1048576}' /proc/meminfo)"
189
+ echo "chrome: $(google-chrome --version 2>/dev/null || echo absent)"
190
+ echo "node: $(node --version)"
191
+ echo "runner: ${RUNNER_OS} ${RUNNER_ARCH}"
192
+
193
+ # `--locked`, as in the Dockerfile: it fails on a `pyproject.toml` edited
194
+ # without relocking, instead of quietly testing a resolution that will
195
+ # never be the one deployed.
196
+ - run: uv sync --locked --group dev
197
+
198
+ # The pixel tests look for `google-chrome` on PATH and ubuntu-latest ships
199
+ # one, so headless Chrome needs no step of its own here. If that stops
200
+ # being true they skip with a stated reason rather than fail, and `-ra`
201
+ # (set in pyproject, deliberately not `-q`) prints every skip — so a suite
202
+ # that has quietly stopped checking pixels says so in the log instead of
203
+ # just looking green.
204
+ #
205
+ # `$files` is unquoted on purpose: the shard file is one path per line and
206
+ # the words are meant to split into separate pytest arguments. `sed` strips
207
+ # the `#` commentary the lists carry, matching what the census test in
208
+ # `tests/test_harness.py` accepts — a shard file the test reads and this
209
+ # line cannot is a trap set for whoever adds the next comment. It is a
210
+ # command substitution rather than `xargs` because `xargs` is allowed to
211
+ # split a long list into two invocations, and two pytest runs in one step
212
+ # would report two summary lines and one exit code. The emptiness check is
213
+ # what makes a missing or all-comment list red here: bare `pytest` with no
214
+ # paths falls back to `testpaths` and runs the WHOLE suite in every shard,
215
+ # which is slow rather than wrong, and therefore invisible.
216
+ #
217
+ # `--durations=25` stays after the measurement flags came out. The probe
218
+ # was `--durations=0 --durations-min=0.05`, which is 732 rows and is what
219
+ # `docs/probes/ci-durations*.txt` hold; 25 is the number that keeps this
220
+ # line useful without keeping the probe. What decides the critical path is
221
+ # shard BALANCE, and balance drifts every time a test file is added — 25
222
+ # rows per leg is enough to see which file grew and re-cut the lists,
223
+ # without anybody having to re-run a measurement workflow to find out. It
224
+ # is display-only: pytest times every phase whether or not it prints them.
225
+ - run: |
226
+ files=$(sed 's/#.*//' .github/shards/${{ matrix.shard }})
227
+ [ -n "$files" ] || { echo "shard ${{ matrix.shard }} names no test files"; exit 1; }
228
+ uv run pytest -q --durations=25 $files
229
+
230
+ # The one required status check, and the reason it is a separate job.
231
+ #
232
+ # Branch protection on `main` requires exactly one context, literally `check`,
233
+ # with `enforce_admins: true`. A `strategy.matrix` on a job named `check`
234
+ # renames the contexts to `check (editor)`, `check (views)` and so on, and
235
+ # separate jobs remove the name entirely. GitHub does not fail a PR for a
236
+ # required context that stopped existing in the way people expect: the PR sits
237
+ # pending for ever, or — if the old name is dropped from the required list
238
+ # without the new ones being added — every shard is red and the merge button is
239
+ # green. That is the sharding failure mode that ships a bug instead of blocking
240
+ # one, so the name does not move. The shards are called `suite`, and this job
241
+ # keeps the name the protection rule already knows.
242
+ check:
243
+ needs: [suite, lint]
244
+ # `!cancelled()` and NOT `always()`. `always()` runs this gate even while the
245
+ # run is being torn down, which is precisely what `cancel-in-progress` above
246
+ # does on every second push — reporting a red required check on a run nobody
247
+ # was waiting for. `!cancelled()` still runs when a shard FAILS, which is the
248
+ # case that has to be caught.
249
+ if: '!cancelled()'
250
+ runs-on: ubuntu-latest
251
+ steps:
252
+ # `needs.suite.result` on a matrix job is the aggregate: `success` only if
253
+ # every leg succeeded. Skipped and cancelled are not `success`, so both
254
+ # `[ … ]` tests fail closed on anything that is not a clean green.
255
+ - run: |
256
+ [ "${{ needs.suite.result }}" = success ] || exit 1
257
+ [ "${{ needs.lint.result }}" = success ] || exit 1
@@ -0,0 +1,83 @@
1
+ name: Release
2
+
3
+ # Publish to PyPI, so that `uvx openproj new …` is the whole of what somebody has
4
+ # to type. It already works without this file — `uvx --from
5
+ # git+https://github.com/jcanton/openproj openproj new …` builds the wheel from
6
+ # a public clone in about four seconds — and the difference PyPI buys is only
7
+ # that a person or an agent does not have to know the repository URL to reach
8
+ # the tool. That is a real difference for a command whose whole reason to exist
9
+ # is that nobody should have to look anything up first.
10
+ #
11
+ # On a tag and nothing else. `main` is pushed to many times a day and a version
12
+ # on PyPI cannot be replaced or re-used — deleting a release only burns the
13
+ # number — so the thing that publishes has to be a deliberate act, and a tag is
14
+ # the deliberate act this project already makes.
15
+ on:
16
+ push:
17
+ tags: ["v*"]
18
+ # Publishing by hand, for the first release, and for the day a tag's run is
19
+ # cancelled by an outage after the tag exists and cannot be pushed again.
20
+ workflow_dispatch:
21
+
22
+ # One at a time, and never cancelled mid-flight: `cancel-in-progress` on a job
23
+ # that uploads to an index is a way to produce half a release.
24
+ concurrency:
25
+ group: release
26
+ cancel-in-progress: false
27
+
28
+ jobs:
29
+ release:
30
+ runs-on: ubuntu-latest
31
+ environment: pypi
32
+ permissions:
33
+ # Trusted Publishing: PyPI is told to trust *this workflow in this
34
+ # repository*, and the job exchanges a GitHub OIDC token for a
35
+ # short-lived upload token at publish time. Nothing long-lived is stored,
36
+ # so there is no API token in this repository's secrets to leak, to
37
+ # rotate, or to forget to rotate.
38
+ #
39
+ # It needs one thing done by hand, once, by somebody signed in to PyPI:
40
+ # add a pending publisher at https://pypi.org/manage/account/publishing/
41
+ # naming project `openproj`, owner `jcanton`, repository `openproj`,
42
+ # workflow `release.yml`, environment `pypi`. Until that exists this job
43
+ # fails at the last step with `invalid-publisher`, having built and
44
+ # checked the distributions perfectly well.
45
+ id-token: write
46
+ steps:
47
+ - uses: actions/checkout@v7
48
+
49
+ - uses: astral-sh/setup-uv@v10.0.1
50
+ with:
51
+ # 3.12, as in `ci.yml` and the Dockerfile. The wheel is pure Python and
52
+ # `requires-python = ">=3.12"`, so the building interpreter does not
53
+ # end up in the artefact — this is here so the whole repository agrees
54
+ # about one version rather than because this job needs a specific one.
55
+ python-version: "3.12"
56
+ enable-cache: true
57
+
58
+ # The tag and the version in `pyproject.toml` are two copies of one fact,
59
+ # and the failure when they disagree is the expensive kind: a `v0.39.0` tag
60
+ # publishing 0.38.2, with the number now spent and 0.39.0 never on the
61
+ # index. Checked before anything is built, because after the upload there
62
+ # is nothing to do about it.
63
+ - name: The tag and the version must agree
64
+ if: startsWith(github.ref, 'refs/tags/v')
65
+ run: |
66
+ tagged="${GITHUB_REF_NAME#v}"
67
+ declared="$(uv run --no-project python -c \
68
+ 'import tomllib,pathlib;print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')"
69
+ if [ "$tagged" != "$declared" ]; then
70
+ echo "tag $GITHUB_REF_NAME says $tagged and pyproject.toml says $declared" >&2
71
+ exit 1
72
+ fi
73
+ echo "both say $declared"
74
+
75
+ - run: uv build
76
+
77
+ # Read back before it is published, the refusal every write path in this
78
+ # project makes. `twine check` is what catches a README that renders as
79
+ # plain text on the project page — which cannot be fixed in place
80
+ # afterwards, only in the next version.
81
+ - run: uvx twine check dist/*
82
+
83
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,22 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .env
5
+ .DS_Store
6
+
7
+ out/
8
+ .worktrees/
9
+
10
+ # What `uv build` writes. The release workflow builds these on a runner and
11
+ # uploads them from there; a copy on somebody's laptop is a build artefact, and
12
+ # one committed by accident is a wheel in the repository that nothing rebuilds.
13
+ dist/
14
+
15
+ # The writer's flock (store.LOCK_FILE). A running server writes its pid into
16
+ # one of these beside the plan it is serving, and a copy of seed/ was carrying a
17
+ # committed one — see the commit that removed it.
18
+ openproj.lock
19
+
20
+ # The subagent-driven-development workspace: ledger, briefs and review packages for a
21
+ # plan in flight. Scratch by construction — git log is the record once the branch lands.
22
+ .superpowers/
@@ -0,0 +1,40 @@
1
+ # `uvx pre-commit install` turns these on. Deliberately not run from CI: CI runs
2
+ # ruff out of the locked dev group, and two ruffs on two versions disagreeing
3
+ # about one file is a worse failure than either check missing.
4
+ #
5
+ # There is no hook here that runs the test suite. It takes four and a half
6
+ # minutes, and a pre-commit hook that slow does not get waited for — it gets
7
+ # `--no-verify`d, and then uninstalled, by exactly the person it was written to
8
+ # help. The suite is the merge gate's job; this file is only for what is cheap
9
+ # enough to never think about.
10
+ repos:
11
+ # Same version as the ruff resolved in `uv.lock` for the dev group. Bump the
12
+ # two together: a hook that formats a file the way CI's ruff then rejects is
13
+ # the whole problem this is supposed to prevent.
14
+ - repo: https://github.com/astral-sh/ruff-pre-commit
15
+ rev: v0.16.2
16
+ hooks:
17
+ - id: ruff-check
18
+ args: [--fix]
19
+ # This repo is not format-clean yet (32 files of 103), and the hook only
20
+ # touches what you staged — so the first commit to an old file carries
21
+ # that file's reformat into an unrelated diff. `uv run ruff format .` as
22
+ # its own commit gets the noise out of everybody's review at once, and is
23
+ # the same commit that lets CI start checking format.
24
+ - id: ruff-format
25
+
26
+ - repo: https://github.com/pre-commit/pre-commit-hooks
27
+ rev: v6.0.0
28
+ hooks:
29
+ - id: trailing-whitespace
30
+ - id: end-of-file-fixer
31
+ # Conflict markers reach a branch that two other worktrees are cut from,
32
+ # and history here does not move — so a marker committed is a marker
33
+ # fixed forward, in public, forever.
34
+ - id: check-merge-conflict
35
+ # 1000, not the default 500. Vendoring libraries is policy here rather
36
+ # than an accident (`static/VENDOR.md`), and `static/cytoscape.min.js` is
37
+ # already 365K; a guard that fires on the documented practice is a guard
38
+ # people learn to skip. This is for the stray screenshot or database dump.
39
+ - id: check-added-large-files
40
+ args: [--maxkb=1000]