@r1z4x/tezgah 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (429) hide show
  1. package/CHANGELOG.md +98 -1
  2. package/MANIFEST +382 -144
  3. package/README.de.md +1 -1
  4. package/README.es.md +1 -1
  5. package/README.fr.md +1 -1
  6. package/README.md +1 -1
  7. package/README.tr.md +1 -1
  8. package/bin/codegen +5 -2
  9. package/bin/tezgah-design +288 -1
  10. package/bin/tezgah-docs +1 -1
  11. package/bin/tezgah-doctor +39 -2
  12. package/bin/tezgah-import-ai-research +26 -12
  13. package/bin/tezgah-ocr-review +296 -0
  14. package/bin/tezgah-research +216 -40
  15. package/bin/tezgah-review-quote +185 -0
  16. package/bin/tezgah-setup +136 -33
  17. package/bin/tezgah-skill +1 -1
  18. package/bin/tezgah-status +201 -3
  19. package/bin/tezgah-task +203 -3
  20. package/commands/adhd.md +2 -2
  21. package/commands/ponytail.md +4 -4
  22. package/docs/architecture.md +2 -2
  23. package/docs/citations-baseline.json +2 -2
  24. package/docs/clarity-baseline.json +2 -2
  25. package/docs/contract.md +87 -42
  26. package/docs/environment.md +5 -3
  27. package/docs/evidence.md +28 -9
  28. package/docs/feature-audit.md +2 -2
  29. package/docs/gate.md +54 -31
  30. package/docs/glossary.md +5 -2
  31. package/docs/hosts.md +17 -3
  32. package/docs/index.json +34 -8
  33. package/docs/judge.md +4 -2
  34. package/docs/layers.md +3 -3
  35. package/docs/models.md +31 -9
  36. package/docs/operations.md +148 -8
  37. package/docs/research.md +786 -5
  38. package/docs/skills.md +137 -13
  39. package/docs/status-line.md +115 -1
  40. package/hooks/hooks.json +1 -1
  41. package/hooks/lang/en.json +10 -0
  42. package/hooks/lang/tr.json +11 -1
  43. package/hooks/tezgah_agents.py +38 -22
  44. package/hooks/tezgah_apps.py +18 -0
  45. package/hooks/tezgah_components.py +4 -0
  46. package/hooks/tezgah_context.py +189 -50
  47. package/hooks/tezgah_extension.py +337 -0
  48. package/hooks/tezgah_gate.py +188 -56
  49. package/hooks/tezgah_integrity.py +652 -105
  50. package/hooks/tezgah_judge.py +6 -4
  51. package/hooks/tezgah_models.py +59 -19
  52. package/hooks/tezgah_orca.py +1 -1
  53. package/hooks/tezgah_paths.py +102 -6
  54. package/hooks/tezgah_policy.py +94 -45
  55. package/hooks/tezgah_rank.py +9 -6
  56. package/hooks/tezgah_replay.py +4 -4
  57. package/hooks/tezgah_research.py +1260 -65
  58. package/hooks/tezgah_research_capture.py +1544 -0
  59. package/hooks/tezgah_research_evidence.py +842 -0
  60. package/hooks/tezgah_research_stats.py +258 -0
  61. package/hooks/tezgah_skill_pick.py +408 -106
  62. package/hooks/tezgah_stopspec.py +8 -1
  63. package/hooks/tezgah_task.py +12 -4
  64. package/hooks/tezgah_taste_ledger.py +1 -1
  65. package/hosts/cursor/hook.py +29 -2
  66. package/hosts/opencode/plugins/tezgah.js +307 -46
  67. package/hosts/opencode/tui/cli/tui.tsx +100 -0
  68. package/hosts/opencode/tui/tezgah-tui.tsx +2 -1
  69. package/output-styles/tezgah.md +10 -10
  70. package/package.json +1 -1
  71. package/packaging/install.ps1 +36 -3
  72. package/packaging/install.sh +13 -3
  73. package/packaging/upgrade.sh +61 -7
  74. package/skills/ai-research/SKILL.md +21 -14
  75. package/skills/ai-research/SOURCE +7 -5
  76. package/skills/ai-research/index/1-frame.md +6 -6
  77. package/skills/ai-research/index/2-data.md +9 -9
  78. package/skills/ai-research/index/3-train.md +36 -36
  79. package/skills/ai-research/index/4-measure.md +13 -13
  80. package/skills/ai-research/index/5-run.md +11 -11
  81. package/skills/ai-research/index/6-write.md +23 -23
  82. package/skills/ai-research/library.json +104 -104
  83. package/skills/analyze-app/SKILL.md +17 -12
  84. package/skills/design-contract/SKILL.md +42 -7
  85. package/skills/design-contract/data-views.md +2 -2
  86. package/skills/design-library/EVALS.md +1 -1
  87. package/skills/design-library/INDEX.md +39 -39
  88. package/skills/design-library/SKILL.md +19 -13
  89. package/skills/design-library/SOURCE +45 -43
  90. package/skills/feature-audit/SKILL.md +20 -14
  91. package/skills/hallmark/LICENSE +21 -0
  92. package/skills/hallmark/SKILL.md +154 -0
  93. package/skills/hallmark/SOURCE +207 -0
  94. package/skills/hallmark/agents/openai.yaml +13 -0
  95. package/skills/hallmark/references/anatomy.md +71 -0
  96. package/skills/hallmark/references/brand-truth.md +40 -0
  97. package/skills/hallmark/references/comp.md +104 -0
  98. package/skills/hallmark/references/craft.md +116 -0
  99. package/skills/hallmark/references/dark-mode.md +49 -0
  100. package/skills/hallmark/references/data-viz.md +48 -0
  101. package/skills/hallmark/references/design-md.md +53 -0
  102. package/skills/hallmark/references/direction.md +52 -0
  103. package/skills/hallmark/references/export-formats.md +80 -0
  104. package/skills/hallmark/references/flow.md +131 -0
  105. package/skills/hallmark/references/fonts.md +58 -0
  106. package/skills/hallmark/references/gates.md +243 -0
  107. package/skills/hallmark/references/genres/atmospheric.md +24 -0
  108. package/skills/hallmark/references/genres/editorial.md +24 -0
  109. package/skills/hallmark/references/genres/modern-minimal.md +24 -0
  110. package/skills/hallmark/references/genres/playful.md +24 -0
  111. package/skills/hallmark/references/imagery.md +99 -0
  112. package/skills/hallmark/references/interaction-and-states.md +89 -0
  113. package/skills/hallmark/references/layout.md +104 -0
  114. package/skills/hallmark/references/macrostructures/01-bento-grid.md +21 -0
  115. package/skills/hallmark/references/macrostructures/02-long-document.md +35 -0
  116. package/skills/hallmark/references/macrostructures/03-marquee-hero.md +21 -0
  117. package/skills/hallmark/references/macrostructures/04-stat-led.md +21 -0
  118. package/skills/hallmark/references/macrostructures/05-workbench.md +21 -0
  119. package/skills/hallmark/references/macrostructures/06-conversational-faq.md +28 -0
  120. package/skills/hallmark/references/macrostructures/07-manifesto.md +28 -0
  121. package/skills/hallmark/references/macrostructures/08-photographic.md +21 -0
  122. package/skills/hallmark/references/macrostructures/09-quote-led.md +21 -0
  123. package/skills/hallmark/references/macrostructures/10-specimen.md +21 -0
  124. package/skills/hallmark/references/macrostructures/11-catalogue.md +21 -0
  125. package/skills/hallmark/references/macrostructures/12-letter.md +21 -0
  126. package/skills/hallmark/references/macrostructures/13-index-first.md +21 -0
  127. package/skills/hallmark/references/macrostructures/14-narrative-workflow.md +21 -0
  128. package/skills/hallmark/references/macrostructures/15-split-studio.md +21 -0
  129. package/skills/hallmark/references/macrostructures/16-feature-stack.md +23 -0
  130. package/skills/hallmark/references/macrostructures/17-type-specimen.md +19 -0
  131. package/skills/hallmark/references/macrostructures/18-portfolio-grid.md +21 -0
  132. package/skills/hallmark/references/macrostructures/19-map-diagram.md +21 -0
  133. package/skills/hallmark/references/macrostructures/20-ecosystem-index.md +19 -0
  134. package/skills/hallmark/references/macrostructures/21-component-playground.md +21 -0
  135. package/skills/hallmark/references/macrostructures.md +119 -0
  136. package/skills/hallmark/references/microinteractions.md +80 -0
  137. package/skills/hallmark/references/patterns.md +202 -0
  138. package/skills/hallmark/references/redesign.md +88 -0
  139. package/skills/hallmark/references/responsive.md +46 -0
  140. package/skills/hallmark/references/review-finish.md +19 -0
  141. package/skills/hallmark/references/review-hero.md +20 -0
  142. package/skills/hallmark/references/scroll-choreography.md +110 -0
  143. package/skills/hallmark/references/texture.md +90 -0
  144. package/skills/hallmark/references/themes/README.md +58 -0
  145. package/skills/hallmark/references/themes/almanac.jpg +0 -0
  146. package/skills/hallmark/references/themes/almanac.md +103 -0
  147. package/skills/hallmark/references/themes/arcade.jpg +0 -0
  148. package/skills/hallmark/references/themes/arcade.md +102 -0
  149. package/skills/hallmark/references/themes/atelier.jpg +0 -0
  150. package/skills/hallmark/references/themes/atelier.md +98 -0
  151. package/skills/hallmark/references/themes/bloom.jpg +0 -0
  152. package/skills/hallmark/references/themes/bloom.md +102 -0
  153. package/skills/hallmark/references/themes/blueprint.jpg +0 -0
  154. package/skills/hallmark/references/themes/blueprint.md +103 -0
  155. package/skills/hallmark/references/themes/broadside.jpg +0 -0
  156. package/skills/hallmark/references/themes/broadside.md +108 -0
  157. package/skills/hallmark/references/themes/brutal.jpg +0 -0
  158. package/skills/hallmark/references/themes/brutal.md +95 -0
  159. package/skills/hallmark/references/themes/carnival.jpg +0 -0
  160. package/skills/hallmark/references/themes/carnival.md +104 -0
  161. package/skills/hallmark/references/themes/cobalt.jpg +0 -0
  162. package/skills/hallmark/references/themes/cobalt.md +101 -0
  163. package/skills/hallmark/references/themes/console.jpg +0 -0
  164. package/skills/hallmark/references/themes/console.md +99 -0
  165. package/skills/hallmark/references/themes/coral.jpg +0 -0
  166. package/skills/hallmark/references/themes/coral.md +102 -0
  167. package/skills/hallmark/references/themes/darkroom.jpg +0 -0
  168. package/skills/hallmark/references/themes/darkroom.md +99 -0
  169. package/skills/hallmark/references/themes/dose.jpg +0 -0
  170. package/skills/hallmark/references/themes/dose.md +98 -0
  171. package/skills/hallmark/references/themes/dusk.jpg +0 -0
  172. package/skills/hallmark/references/themes/dusk.md +105 -0
  173. package/skills/hallmark/references/themes/editorial.jpg +0 -0
  174. package/skills/hallmark/references/themes/editorial.md +97 -0
  175. package/skills/hallmark/references/themes/formwork.jpg +0 -0
  176. package/skills/hallmark/references/themes/formwork.md +107 -0
  177. package/skills/hallmark/references/themes/furrow.jpg +0 -0
  178. package/skills/hallmark/references/themes/furrow.md +98 -0
  179. package/skills/hallmark/references/themes/garden.jpg +0 -0
  180. package/skills/hallmark/references/themes/garden.md +97 -0
  181. package/skills/hallmark/references/themes/grid.jpg +0 -0
  182. package/skills/hallmark/references/themes/grid.md +101 -0
  183. package/skills/hallmark/references/themes/halftone.jpg +0 -0
  184. package/skills/hallmark/references/themes/halftone.md +106 -0
  185. package/skills/hallmark/references/themes/hedgerow.jpg +0 -0
  186. package/skills/hallmark/references/themes/hedgerow.md +104 -0
  187. package/skills/hallmark/references/themes/hum.jpg +0 -0
  188. package/skills/hallmark/references/themes/hum.md +99 -0
  189. package/skills/hallmark/references/themes/index.json +2191 -0
  190. package/skills/hallmark/references/themes/ink.jpg +0 -0
  191. package/skills/hallmark/references/themes/ink.md +106 -0
  192. package/skills/hallmark/references/themes/kiln.jpg +0 -0
  193. package/skills/hallmark/references/themes/kiln.md +108 -0
  194. package/skills/hallmark/references/themes/loom.jpg +0 -0
  195. package/skills/hallmark/references/themes/loom.md +106 -0
  196. package/skills/hallmark/references/themes/lumen.jpg +0 -0
  197. package/skills/hallmark/references/themes/lumen.md +103 -0
  198. package/skills/hallmark/references/themes/manifesto.jpg +0 -0
  199. package/skills/hallmark/references/themes/manifesto.md +101 -0
  200. package/skills/hallmark/references/themes/midnight.jpg +0 -0
  201. package/skills/hallmark/references/themes/midnight.md +97 -0
  202. package/skills/hallmark/references/themes/newsprint.jpg +0 -0
  203. package/skills/hallmark/references/themes/newsprint.md +102 -0
  204. package/skills/hallmark/references/themes/overprint.jpg +0 -0
  205. package/skills/hallmark/references/themes/overprint.md +101 -0
  206. package/skills/hallmark/references/themes/pasteup.jpg +0 -0
  207. package/skills/hallmark/references/themes/pasteup.md +100 -0
  208. package/skills/hallmark/references/themes/piste.jpg +0 -0
  209. package/skills/hallmark/references/themes/piste.md +99 -0
  210. package/skills/hallmark/references/themes/ribbon.jpg +0 -0
  211. package/skills/hallmark/references/themes/ribbon.md +106 -0
  212. package/skills/hallmark/references/themes/rim.jpg +0 -0
  213. package/skills/hallmark/references/themes/rim.md +96 -0
  214. package/skills/hallmark/references/themes/riso.jpg +0 -0
  215. package/skills/hallmark/references/themes/riso.md +98 -0
  216. package/skills/hallmark/references/themes/solstice.jpg +0 -0
  217. package/skills/hallmark/references/themes/solstice.md +104 -0
  218. package/skills/hallmark/references/themes/specimen.jpg +0 -0
  219. package/skills/hallmark/references/themes/specimen.md +100 -0
  220. package/skills/hallmark/references/themes/spore.jpg +0 -0
  221. package/skills/hallmark/references/themes/spore.md +104 -0
  222. package/skills/hallmark/references/themes/sport.jpg +0 -0
  223. package/skills/hallmark/references/themes/sport.md +101 -0
  224. package/skills/hallmark/references/themes/squeeze.jpg +0 -0
  225. package/skills/hallmark/references/themes/squeeze.md +99 -0
  226. package/skills/hallmark/references/themes/sticker.jpg +0 -0
  227. package/skills/hallmark/references/themes/sticker.md +106 -0
  228. package/skills/hallmark/references/themes/studio.jpg +0 -0
  229. package/skills/hallmark/references/themes/studio.md +99 -0
  230. package/skills/hallmark/references/themes/terminal.jpg +0 -0
  231. package/skills/hallmark/references/themes/terminal.md +96 -0
  232. package/skills/hallmark/references/themes/terrazzo.jpg +0 -0
  233. package/skills/hallmark/references/themes/terrazzo.md +106 -0
  234. package/skills/hallmark/references/themes/ultraviolet.jpg +0 -0
  235. package/skills/hallmark/references/themes/ultraviolet.md +106 -0
  236. package/skills/hallmark/references/themes/vellum.jpg +0 -0
  237. package/skills/hallmark/references/themes/vellum.md +105 -0
  238. package/skills/hallmark/references/themes/wayfind.jpg +0 -0
  239. package/skills/hallmark/references/themes/wayfind.md +105 -0
  240. package/skills/hallmark/references/themes/werkstatt.jpg +0 -0
  241. package/skills/hallmark/references/themes/werkstatt.md +105 -0
  242. package/skills/hallmark/scripts/capture.mjs +125 -0
  243. package/skills/hallmark/scripts/fixtures/exemplars/atmospheric.html +324 -0
  244. package/skills/hallmark/scripts/fixtures/exemplars/editorial.html +363 -0
  245. package/skills/hallmark/scripts/fixtures/exemplars/modern-minimal.html +527 -0
  246. package/skills/hallmark/scripts/fixtures/exemplars/playful.html +353 -0
  247. package/skills/hallmark/scripts/fixtures/eyebrow-fail/index.html +118 -0
  248. package/skills/hallmark/scripts/fixtures/eyebrow-pass/index.html +137 -0
  249. package/skills/hallmark/scripts/fixtures/kept/after.html +27 -0
  250. package/skills/hallmark/scripts/fixtures/kept/before.html +26 -0
  251. package/skills/hallmark/scripts/fixtures/slopped.html +145 -0
  252. package/skills/hallmark/scripts/fixtures/tailwind-fail/Feature.vue +14 -0
  253. package/skills/hallmark/scripts/fixtures/tailwind-fail/Hero.tsx +33 -0
  254. package/skills/hallmark/scripts/fixtures/tailwind-fail/index.html +74 -0
  255. package/skills/hallmark/scripts/fixtures/tailwind-fail/style.css +6 -0
  256. package/skills/hallmark/scripts/fixtures/tailwind-pass/Hero.tsx +33 -0
  257. package/skills/hallmark/scripts/fixtures/tailwind-pass/index.html +57 -0
  258. package/skills/hallmark/scripts/fixtures/tailwind-pass/style.css +15 -0
  259. package/skills/hallmark/scripts/fixtures/waiver/index.html +54 -0
  260. package/skills/hallmark/scripts/imagegen.mjs +85 -0
  261. package/skills/hallmark/scripts/install-hook.mjs +70 -0
  262. package/skills/hallmark/scripts/install.mjs +141 -0
  263. package/skills/hallmark/scripts/lint-hook.mjs +49 -0
  264. package/skills/hallmark/scripts/photos.json +1 -0
  265. package/skills/hallmark/scripts/picture.mjs +71 -0
  266. package/skills/hallmark/scripts/refcheck.mjs +352 -0
  267. package/skills/hallmark/scripts/sloplint-render.mjs +1252 -0
  268. package/skills/hallmark/scripts/sloplint.mjs +4422 -0
  269. package/skills/hallmark/scripts/theme.mjs +650 -0
  270. package/skills/harness/SKILL.md +15 -8
  271. package/skills/i-have-adhd/SKILL.md +22 -11
  272. package/skills/no-ai-slop/SKILL.md +15 -1
  273. package/skills/plan-add/SKILL.md +33 -26
  274. package/skills/plan-status/SKILL.md +13 -7
  275. package/skills/plan-sync/SKILL.md +18 -12
  276. package/skills/pm-frameworks/SKILL.md +19 -10
  277. package/skills/pm-frameworks/SOURCE +7 -5
  278. package/skills/ponytail/SKILL.md +19 -14
  279. package/skills/product-analysis/SKILL.md +22 -18
  280. package/skills/product-flow/SKILL.md +14 -9
  281. package/skills/research/SKILL.md +75 -39
  282. package/skills/rl-env/SKILL.md +25 -13
  283. package/skills/rl-env/SOURCE +8 -6
  284. package/skills/tezgah-contract/SKILL.md +47 -42
  285. package/bin/tezgah-skill.py +0 -59
  286. /package/skills/ai-research/0-autoresearch-skill/{SKILL.md → ENTRY.md} +0 -0
  287. /package/skills/ai-research/01-model-architecture/litgpt/{SKILL.md → ENTRY.md} +0 -0
  288. /package/skills/ai-research/01-model-architecture/mamba/{SKILL.md → ENTRY.md} +0 -0
  289. /package/skills/ai-research/01-model-architecture/nanogpt/{SKILL.md → ENTRY.md} +0 -0
  290. /package/skills/ai-research/01-model-architecture/rwkv/{SKILL.md → ENTRY.md} +0 -0
  291. /package/skills/ai-research/01-model-architecture/torchtitan/{SKILL.md → ENTRY.md} +0 -0
  292. /package/skills/ai-research/02-tokenization/huggingface-tokenizers/{SKILL.md → ENTRY.md} +0 -0
  293. /package/skills/ai-research/02-tokenization/sentencepiece/{SKILL.md → ENTRY.md} +0 -0
  294. /package/skills/ai-research/03-fine-tuning/axolotl/{SKILL.md → ENTRY.md} +0 -0
  295. /package/skills/ai-research/03-fine-tuning/llama-factory/{SKILL.md → ENTRY.md} +0 -0
  296. /package/skills/ai-research/03-fine-tuning/peft/{SKILL.md → ENTRY.md} +0 -0
  297. /package/skills/ai-research/03-fine-tuning/unsloth/{SKILL.md → ENTRY.md} +0 -0
  298. /package/skills/ai-research/04-mechanistic-interpretability/nnsight/{SKILL.md → ENTRY.md} +0 -0
  299. /package/skills/ai-research/04-mechanistic-interpretability/pyvene/{SKILL.md → ENTRY.md} +0 -0
  300. /package/skills/ai-research/04-mechanistic-interpretability/saelens/{SKILL.md → ENTRY.md} +0 -0
  301. /package/skills/ai-research/04-mechanistic-interpretability/transformer-lens/{SKILL.md → ENTRY.md} +0 -0
  302. /package/skills/ai-research/05-data-processing/nemo-curator/{SKILL.md → ENTRY.md} +0 -0
  303. /package/skills/ai-research/05-data-processing/ray-data/{SKILL.md → ENTRY.md} +0 -0
  304. /package/skills/ai-research/06-post-training/grpo-rl-training/{SKILL.md → ENTRY.md} +0 -0
  305. /package/skills/ai-research/06-post-training/miles/{SKILL.md → ENTRY.md} +0 -0
  306. /package/skills/ai-research/06-post-training/openrlhf/{SKILL.md → ENTRY.md} +0 -0
  307. /package/skills/ai-research/06-post-training/simpo/{SKILL.md → ENTRY.md} +0 -0
  308. /package/skills/ai-research/06-post-training/slime/{SKILL.md → ENTRY.md} +0 -0
  309. /package/skills/ai-research/06-post-training/torchforge/{SKILL.md → ENTRY.md} +0 -0
  310. /package/skills/ai-research/06-post-training/trl-fine-tuning/{SKILL.md → ENTRY.md} +0 -0
  311. /package/skills/ai-research/06-post-training/verl/{SKILL.md → ENTRY.md} +0 -0
  312. /package/skills/ai-research/07-safety-alignment/constitutional-ai/{SKILL.md → ENTRY.md} +0 -0
  313. /package/skills/ai-research/07-safety-alignment/llamaguard/{SKILL.md → ENTRY.md} +0 -0
  314. /package/skills/ai-research/07-safety-alignment/nemo-guardrails/{SKILL.md → ENTRY.md} +0 -0
  315. /package/skills/ai-research/07-safety-alignment/prompt-guard/{SKILL.md → ENTRY.md} +0 -0
  316. /package/skills/ai-research/08-distributed-training/accelerate/{SKILL.md → ENTRY.md} +0 -0
  317. /package/skills/ai-research/08-distributed-training/deepspeed/{SKILL.md → ENTRY.md} +0 -0
  318. /package/skills/ai-research/08-distributed-training/megatron-core/{SKILL.md → ENTRY.md} +0 -0
  319. /package/skills/ai-research/08-distributed-training/pytorch-fsdp2/{SKILL.md → ENTRY.md} +0 -0
  320. /package/skills/ai-research/08-distributed-training/pytorch-lightning/{SKILL.md → ENTRY.md} +0 -0
  321. /package/skills/ai-research/08-distributed-training/ray-train/{SKILL.md → ENTRY.md} +0 -0
  322. /package/skills/ai-research/09-infrastructure/lambda-labs/{SKILL.md → ENTRY.md} +0 -0
  323. /package/skills/ai-research/09-infrastructure/modal/{SKILL.md → ENTRY.md} +0 -0
  324. /package/skills/ai-research/09-infrastructure/skypilot/{SKILL.md → ENTRY.md} +0 -0
  325. /package/skills/ai-research/10-optimization/awq/{SKILL.md → ENTRY.md} +0 -0
  326. /package/skills/ai-research/10-optimization/bitsandbytes/{SKILL.md → ENTRY.md} +0 -0
  327. /package/skills/ai-research/10-optimization/flash-attention/{SKILL.md → ENTRY.md} +0 -0
  328. /package/skills/ai-research/10-optimization/gguf/{SKILL.md → ENTRY.md} +0 -0
  329. /package/skills/ai-research/10-optimization/gptq/{SKILL.md → ENTRY.md} +0 -0
  330. /package/skills/ai-research/10-optimization/hqq/{SKILL.md → ENTRY.md} +0 -0
  331. /package/skills/ai-research/10-optimization/ml-training-recipes/{SKILL.md → ENTRY.md} +0 -0
  332. /package/skills/ai-research/11-evaluation/bigcode-evaluation-harness/{SKILL.md → ENTRY.md} +0 -0
  333. /package/skills/ai-research/11-evaluation/lm-evaluation-harness/{SKILL.md → ENTRY.md} +0 -0
  334. /package/skills/ai-research/11-evaluation/nemo-evaluator/{SKILL.md → ENTRY.md} +0 -0
  335. /package/skills/ai-research/12-inference-serving/llama-cpp/{SKILL.md → ENTRY.md} +0 -0
  336. /package/skills/ai-research/12-inference-serving/sglang/{SKILL.md → ENTRY.md} +0 -0
  337. /package/skills/ai-research/12-inference-serving/tensorrt-llm/{SKILL.md → ENTRY.md} +0 -0
  338. /package/skills/ai-research/12-inference-serving/vllm/{SKILL.md → ENTRY.md} +0 -0
  339. /package/skills/ai-research/13-mlops/mlflow/{SKILL.md → ENTRY.md} +0 -0
  340. /package/skills/ai-research/13-mlops/swanlab/{SKILL.md → ENTRY.md} +0 -0
  341. /package/skills/ai-research/13-mlops/tensorboard/{SKILL.md → ENTRY.md} +0 -0
  342. /package/skills/ai-research/13-mlops/weights-and-biases/{SKILL.md → ENTRY.md} +0 -0
  343. /package/skills/ai-research/14-agents/a-evolve/{SKILL.md → ENTRY.md} +0 -0
  344. /package/skills/ai-research/14-agents/autogpt/{SKILL.md → ENTRY.md} +0 -0
  345. /package/skills/ai-research/14-agents/crewai/{SKILL.md → ENTRY.md} +0 -0
  346. /package/skills/ai-research/14-agents/langchain/{SKILL.md → ENTRY.md} +0 -0
  347. /package/skills/ai-research/14-agents/llamaindex/{SKILL.md → ENTRY.md} +0 -0
  348. /package/skills/ai-research/15-rag/chroma/{SKILL.md → ENTRY.md} +0 -0
  349. /package/skills/ai-research/15-rag/faiss/{SKILL.md → ENTRY.md} +0 -0
  350. /package/skills/ai-research/15-rag/pinecone/{SKILL.md → ENTRY.md} +0 -0
  351. /package/skills/ai-research/15-rag/qdrant/{SKILL.md → ENTRY.md} +0 -0
  352. /package/skills/ai-research/15-rag/sentence-transformers/{SKILL.md → ENTRY.md} +0 -0
  353. /package/skills/ai-research/16-prompt-engineering/dspy/{SKILL.md → ENTRY.md} +0 -0
  354. /package/skills/ai-research/16-prompt-engineering/guidance/{SKILL.md → ENTRY.md} +0 -0
  355. /package/skills/ai-research/16-prompt-engineering/instructor/{SKILL.md → ENTRY.md} +0 -0
  356. /package/skills/ai-research/16-prompt-engineering/outlines/{SKILL.md → ENTRY.md} +0 -0
  357. /package/skills/ai-research/17-observability/langsmith/{SKILL.md → ENTRY.md} +0 -0
  358. /package/skills/ai-research/17-observability/phoenix/{SKILL.md → ENTRY.md} +0 -0
  359. /package/skills/ai-research/18-multimodal/audiocraft/{SKILL.md → ENTRY.md} +0 -0
  360. /package/skills/ai-research/18-multimodal/blip-2/{SKILL.md → ENTRY.md} +0 -0
  361. /package/skills/ai-research/18-multimodal/clip/{SKILL.md → ENTRY.md} +0 -0
  362. /package/skills/ai-research/18-multimodal/cosmos-policy/{SKILL.md → ENTRY.md} +0 -0
  363. /package/skills/ai-research/18-multimodal/llava/{SKILL.md → ENTRY.md} +0 -0
  364. /package/skills/ai-research/18-multimodal/openpi/{SKILL.md → ENTRY.md} +0 -0
  365. /package/skills/ai-research/18-multimodal/openvla-oft/{SKILL.md → ENTRY.md} +0 -0
  366. /package/skills/ai-research/18-multimodal/segment-anything/{SKILL.md → ENTRY.md} +0 -0
  367. /package/skills/ai-research/18-multimodal/stable-diffusion/{SKILL.md → ENTRY.md} +0 -0
  368. /package/skills/ai-research/18-multimodal/whisper/{SKILL.md → ENTRY.md} +0 -0
  369. /package/skills/ai-research/19-emerging-techniques/knowledge-distillation/{SKILL.md → ENTRY.md} +0 -0
  370. /package/skills/ai-research/19-emerging-techniques/long-context/{SKILL.md → ENTRY.md} +0 -0
  371. /package/skills/ai-research/19-emerging-techniques/model-merging/{SKILL.md → ENTRY.md} +0 -0
  372. /package/skills/ai-research/19-emerging-techniques/model-pruning/{SKILL.md → ENTRY.md} +0 -0
  373. /package/skills/ai-research/19-emerging-techniques/moe-training/{SKILL.md → ENTRY.md} +0 -0
  374. /package/skills/ai-research/19-emerging-techniques/speculative-decoding/{SKILL.md → ENTRY.md} +0 -0
  375. /package/skills/ai-research/20-ml-paper-writing/academic-plotting/{SKILL.md → ENTRY.md} +0 -0
  376. /package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/{SKILL.md → ENTRY.md} +0 -0
  377. /package/skills/ai-research/20-ml-paper-writing/presenting-conference-talks/{SKILL.md → ENTRY.md} +0 -0
  378. /package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/{SKILL.md → ENTRY.md} +0 -0
  379. /package/skills/ai-research/21-research-ideation/brainstorming-research-ideas/{SKILL.md → ENTRY.md} +0 -0
  380. /package/skills/ai-research/21-research-ideation/creative-thinking-for-research/{SKILL.md → ENTRY.md} +0 -0
  381. /package/skills/ai-research/22-agent-native-research-artifact/compiler/{SKILL.md → ENTRY.md} +0 -0
  382. /package/skills/ai-research/22-agent-native-research-artifact/research-manager/{SKILL.md → ENTRY.md} +0 -0
  383. /package/skills/ai-research/22-agent-native-research-artifact/rigor-reviewer/{SKILL.md → ENTRY.md} +0 -0
  384. /package/skills/design-library/adaptive-interfaces/skills/colour-independence/{SKILL.md → ENTRY.md} +0 -0
  385. /package/skills/design-library/adaptive-interfaces/skills/flexible-typography/{SKILL.md → ENTRY.md} +0 -0
  386. /package/skills/design-library/adaptive-interfaces/skills/information-density/{SKILL.md → ENTRY.md} +0 -0
  387. /package/skills/design-library/adaptive-interfaces/skills/preference-audit/{SKILL.md → ENTRY.md} +0 -0
  388. /package/skills/design-library/adaptive-interfaces/skills/responsive-accessibility/{SKILL.md → ENTRY.md} +0 -0
  389. /package/skills/design-library/adaptive-interfaces/skills/responsive-review/{SKILL.md → ENTRY.md} +0 -0
  390. /package/skills/design-library/adaptive-interfaces/skills/simplified-views/{SKILL.md → ENTRY.md} +0 -0
  391. /package/skills/design-library/adaptive-interfaces/skills/specify/{SKILL.md → ENTRY.md} +0 -0
  392. /package/skills/design-library/adaptive-interfaces/skills/user-preference-respect/{SKILL.md → ENTRY.md} +0 -0
  393. /package/skills/design-library/cognitive-accessibility/skills/adaptive-personalisation/{SKILL.md → ENTRY.md} +0 -0
  394. /package/skills/design-library/cognitive-accessibility/skills/assess-load/{SKILL.md → ENTRY.md} +0 -0
  395. /package/skills/design-library/cognitive-accessibility/skills/cognitive-load-assessment/{SKILL.md → ENTRY.md} +0 -0
  396. /package/skills/design-library/cognitive-accessibility/skills/contextual-help-design/{SKILL.md → ENTRY.md} +0 -0
  397. /package/skills/design-library/cognitive-accessibility/skills/error-prevention-recovery/{SKILL.md → ENTRY.md} +0 -0
  398. /package/skills/design-library/cognitive-accessibility/skills/focus-attention-design/{SKILL.md → ENTRY.md} +0 -0
  399. /package/skills/design-library/cognitive-accessibility/skills/memory-load-reduction/{SKILL.md → ENTRY.md} +0 -0
  400. /package/skills/design-library/cognitive-accessibility/skills/plain-language-design/{SKILL.md → ENTRY.md} +0 -0
  401. /package/skills/design-library/cognitive-accessibility/skills/review/{SKILL.md → ENTRY.md} +0 -0
  402. /package/skills/design-library/cognitive-accessibility/skills/simplify/{SKILL.md → ENTRY.md} +0 -0
  403. /package/skills/design-library/cognitive-accessibility/skills/wayfinding-navigation/{SKILL.md → ENTRY.md} +0 -0
  404. /package/skills/design-library/design-research/skills/behavioural-analytics/{SKILL.md → ENTRY.md} +0 -0
  405. /package/skills/design-library/design-research/skills/qual-quant-triangulation/{SKILL.md → ENTRY.md} +0 -0
  406. /package/skills/design-library/inclusive-interaction/skills/audit/{SKILL.md → ENTRY.md} +0 -0
  407. /package/skills/design-library/inclusive-interaction/skills/design-flow/{SKILL.md → ENTRY.md} +0 -0
  408. /package/skills/design-library/inclusive-interaction/skills/feedback-and-status/{SKILL.md → ENTRY.md} +0 -0
  409. /package/skills/design-library/inclusive-interaction/skills/gesture-alternatives/{SKILL.md → ENTRY.md} +0 -0
  410. /package/skills/design-library/inclusive-interaction/skills/keyboard-navigation/{SKILL.md → ENTRY.md} +0 -0
  411. /package/skills/design-library/inclusive-interaction/skills/keyboard-review/{SKILL.md → ENTRY.md} +0 -0
  412. /package/skills/design-library/inclusive-interaction/skills/motion-sensitivity/{SKILL.md → ENTRY.md} +0 -0
  413. /package/skills/design-library/inclusive-interaction/skills/multi-modal-input/{SKILL.md → ENTRY.md} +0 -0
  414. /package/skills/design-library/inclusive-interaction/skills/touch-target-design/{SKILL.md → ENTRY.md} +0 -0
  415. /package/skills/design-library/inclusive-interaction/skills/voice-interaction/{SKILL.md → ENTRY.md} +0 -0
  416. /package/skills/design-library/visual-critique/skills/critique-affordance/{SKILL.md → ENTRY.md} +0 -0
  417. /package/skills/design-library/visual-critique/skills/critique-brand-consistency/{SKILL.md → ENTRY.md} +0 -0
  418. /package/skills/design-library/visual-critique/skills/critique-color/{SKILL.md → ENTRY.md} +0 -0
  419. /package/skills/design-library/visual-critique/skills/critique-composition/{SKILL.md → ENTRY.md} +0 -0
  420. /package/skills/design-library/visual-critique/skills/critique-information-density/{SKILL.md → ENTRY.md} +0 -0
  421. /package/skills/design-library/visual-critique/skills/critique-typography/{SKILL.md → ENTRY.md} +0 -0
  422. /package/skills/design-library/visual-critique/skills/critique-visual-hierarchy/{SKILL.md → ENTRY.md} +0 -0
  423. /package/skills/pm-frameworks/intended-vs-implemented/{SKILL.md → ENTRY.md} +0 -0
  424. /package/skills/pm-frameworks/opportunity-solution-tree/{SKILL.md → ENTRY.md} +0 -0
  425. /package/skills/rl-env/generate-nemo-gym-env/{SKILL.md → ENTRY.md} +0 -0
  426. /package/skills/rl-env/generate-openenv-env/{SKILL.md → ENTRY.md} +0 -0
  427. /package/skills/rl-env/generate-ors-env/{SKILL.md → ENTRY.md} +0 -0
  428. /package/skills/rl-env/generate-verifiers-env/{SKILL.md → ENTRY.md} +0 -0
  429. /package/skills/rl-env/rl-env-from-description/{SKILL.md → ENTRY.md} +0 -0
@@ -83,6 +83,7 @@ the offset just past its last newline (`tezgah_integrity.committed_size`) - so a
83
83
  fragment a killed writer left behind is neither read as a record by the readers
84
84
  here nor appended beside as one.
85
85
  """
86
+ import decimal
86
87
  import fnmatch
87
88
  import json
88
89
  import math
@@ -142,7 +143,7 @@ SOURCE_CLASSES = ("formal", "grey", "agent-report")
142
143
  # that owes no variants), 6 the selection warnings (`_check_selection`: the
143
144
  # held-out set, repeats per arm, the noise band), 7 the exploratory method (an
144
145
  # arena locked before the first idea, the idea/stage/tournament rows and their
145
- # rules, `_check_exploratory`). A `rules` above `RULES` is
146
+ # rules, `_check_exploratory`). A `rules` above `EVIDENCE_RULES` is
146
147
  # a line a later tezgah wrote, and every reader and writer here refuses it by name
147
148
  # (`newer_rules`) rather than judging it by rules it does not know.
148
149
  STANDARDS_RULES = 2
@@ -150,7 +151,11 @@ PROTOCOL_RULES = 4
150
151
  SCOPE_RULES = 5
151
152
  HOLDOUT_RULES = 6
152
153
  EXPLORE_RULES = 7
153
- RULES = EXPLORE_RULES
154
+ # Rules 8 (plan 127): evidence captured by the tool and claims narrowed rather
155
+ # than lines refused. `init` writes it since the plan's last wave moved `RULES`
156
+ # here.
157
+ EVIDENCE_RULES = 8
158
+ RULES = EVIDENCE_RULES
154
159
  # How a rules-5 line answers its question. `qualitative` is a source or
155
160
  # reference review and owes no metric; `measured` runs experiments and locks
156
161
  # one. The question picks it, not the tooling: a qualitative ask answered with a
@@ -485,6 +490,20 @@ def _ws_rel(repo, path):
485
490
  return rel if os.path.exists(os.path.join(ws, ".git")) else None
486
491
 
487
492
 
493
+ def folded(repo):
494
+ """True when `<repo>/.tezgah` is a folded workspace: a directory the project
495
+ itself tracks and that holds no repository of its own, so the project's
496
+ history is the one that carries it (`ensure_workspace` answers None for it by
497
+ design). A symlinked `.tezgah` and a tree with no `.git` are not folded: the
498
+ first is refused as repository-provided, the second has no history at all.
499
+ `workspace_tracked` wants positive evidence, so an index that cannot be read
500
+ is not folded either."""
501
+ ws = tp.workspace(repo)
502
+ return (os.path.isdir(ws) and not os.path.islink(ws)
503
+ and not os.path.lexists(os.path.join(ws, ".git"))
504
+ and tp.workspace_tracked(repo))
505
+
506
+
488
507
  def _ws_log(repo, path, *args):
489
508
  """(shas, error) of the private repository's `git log <args>` over `path`'s
490
509
  own layout names (`_own_names`) along its HEAD, ([], None) when `path` is
@@ -992,7 +1011,7 @@ def _check_success(state, errors, warnings, strict):
992
1011
  unjudged = [sid for sid, _c, verdict, _e in rows if verdict not in VERDICTS]
993
1012
  if unjudged:
994
1013
  errors.append("concluded with no verdict for %s: judge each criterion "
995
- "(`tezgah-research verdict`) before concluding"
1014
+ "(`~/.config/tezgah/bin/tezgah-research verdict`) before concluding"
996
1015
  % ", ".join(unjudged))
997
1016
  if unanswered(state) and not (state.get("closed") or {}).get("ack") \
998
1017
  and not _user_event(state):
@@ -1237,7 +1256,7 @@ def _check_findings(base, errors, warnings, strict):
1237
1256
  missing = [s for s in FINDINGS_SECTIONS if ("## " + s) not in text]
1238
1257
  if missing:
1239
1258
  errors.append("findings.md does not answer: %s" % ", ".join(missing))
1240
- _check_patterns(text, errors, warnings, strict)
1259
+ _check_patterns(text, errors, warnings, strict, _evidence(_state(base)))
1241
1260
 
1242
1261
 
1243
1262
  PATTERNS_BULLET = re.compile(r"^\s*(?:[-*+]|\d+[.)])\s+(.*)$")
@@ -1256,7 +1275,7 @@ def _section(text, name):
1256
1275
  return out
1257
1276
 
1258
1277
 
1259
- def _check_patterns(text, errors, warnings, strict):
1278
+ def _check_patterns(text, errors, warnings, strict, wrapped):
1260
1279
  """Every bullet under `## Patterns` names the source it came from.
1261
1280
 
1262
1281
  A pattern is the one finding a reader is meant to carry into other work, so
@@ -1265,9 +1284,17 @@ def _check_patterns(text, errors, warnings, strict):
1265
1284
  none is a guess wearing the clothes of a finding, which is exactly the
1266
1285
  failure the review exists to catch, and it is reported once per line with the
1267
1286
  count so a line with seventeen of them says so in one row rather than
1268
- seventeen."""
1269
- bullets = [m.group(1).strip() for line in _section(text, "Patterns")
1270
- for m in [PATTERNS_BULLET.match(line)] if m]
1287
+ seventeen. With `wrapped` (a rules-8 line, `_evidence`) a bullet is its first
1288
+ line plus every following indented, non-blank line before the next bullet
1289
+ (spec 4.7): a source wrapped onto a continuation line is still the bullet's
1290
+ source. An older line reads each bullet's first line, as it was opened under."""
1291
+ bullets = []
1292
+ for line in _section(text, "Patterns"):
1293
+ found = PATTERNS_BULLET.match(line)
1294
+ if found:
1295
+ bullets.append(found.group(1).strip())
1296
+ elif wrapped and bullets and line[:1].isspace() and line.strip():
1297
+ bullets[-1] += " " + line.strip()
1271
1298
  unsourced = [b for b in bullets if not PATTERNS_SOURCE.search(b)]
1272
1299
  if unsourced:
1273
1300
  _soft(errors, warnings, strict,
@@ -1707,14 +1734,17 @@ def _check_claims(base, errors, warnings, roots=(), strict=False):
1707
1734
  # held to their proof here. Warning on the historical statement too would
1708
1735
  # make a correction that rewords a figure impossible to land (it already
1709
1736
  # cannot edit the row), for a sentence no reader is meant to keep.
1710
- if cid not in superseded:
1737
+ # rules 8 replaces this rule by `untraced-number`, a step that never refuses
1738
+ if cid not in superseded and not _evidence(line_state):
1711
1739
  _check_claim_numbers(cid, claim, base, repo, errors, warnings, strict)
1712
1740
  _check_claim_raters(cid, claim, base, errors, warnings, strict)
1713
1741
  if sections and cid not in superseded:
1714
1742
  problem = _token_claim_problem(cid, claim, base)
1715
1743
  if problem:
1716
1744
  errors.append(problem)
1717
- _check_supersedes(parsed, errors, warnings, strict)
1745
+ _check_supersedes(parsed, errors, warnings, strict,
1746
+ _note_text(os.path.join(base, "to_human", "report.md"))
1747
+ if _evidence(line_state) else None)
1718
1748
  if not count:
1719
1749
  warnings.append("no claims recorded yet")
1720
1750
  return count
@@ -1738,7 +1768,7 @@ def _token_claim_problem(cid, claim, base):
1738
1768
  return None
1739
1769
 
1740
1770
 
1741
- def _check_supersedes(parsed, errors, warnings, strict):
1771
+ def _check_supersedes(parsed, errors, warnings, strict, report=None):
1742
1772
  """A claim that supersedes another names one this line holds, and says so.
1743
1773
 
1744
1774
  The record is append-only: a claim whose number went stale is corrected by a
@@ -1760,6 +1790,10 @@ def _check_supersedes(parsed, errors, warnings, strict):
1760
1790
  errors.append("claim %s supersedes %s, and no claim in this line "
1761
1791
  "carries that id" % (cid, ref))
1762
1792
  continue
1793
+ # rules 8 (spec 4.6): a report line naming both ids tells the reader
1794
+ if report is not None and any(_mentions(cid, line) and _mentions(ref, line)
1795
+ for line in report.splitlines()):
1796
+ continue
1763
1797
  _soft(errors, warnings, strict,
1764
1798
  "claim %s supersedes %s: a reader who opens %s alone reads the "
1765
1799
  "statement this line has since replaced" % (cid, ref, ref))
@@ -1787,6 +1821,11 @@ def _supersedes(claim):
1787
1821
  return ids, None
1788
1822
 
1789
1823
 
1824
+ def _superseded(rows):
1825
+ """The claim ids some row supersedes (`_supersedes`): the historical rows."""
1826
+ return {ref for row in rows for ref in _supersedes(row)[0]}
1827
+
1828
+
1790
1829
  def _claim_ids(base, handle=None):
1791
1830
  """The ids `claims.jsonl` already carries, for the rule that a claim may only
1792
1831
  supersede one this line holds. A row that does not parse is skipped here: the
@@ -2027,6 +2066,12 @@ def claim_problems(claim, base, repo, held=None):
2027
2066
  if scopes and all(s == "fixture" for s in scopes):
2028
2067
  problems.append("claim %s declares scope real and every row it rests on "
2029
2068
  "records fixture" % cid)
2069
+ # rules 8: the claim refusals `_core_rules8` applies, from the same functions, and
2070
+ # like it never to a claim a row supersedes (the correction is that row)
2071
+ if _evidence(_state(base)) and cid not in _superseded(
2072
+ _row_objects(os.path.join(base, "claims.jsonl"))):
2073
+ problems.extend(derived_problems(cid, claim, base))
2074
+ problems.extend(fired_problems(cid, claim, base))
2030
2075
  return problems
2031
2076
 
2032
2077
 
@@ -2329,10 +2374,17 @@ def _check_tracking(repo, base, errors, warnings, strict):
2329
2374
  if not pattern:
2330
2375
  continue
2331
2376
  rel = _ws_rel(repo, path)
2332
- fix = ("`git -C .tezgah add -f %s`" % rel if rel is not None else
2333
- "`.tezgah` has no git repository of its own - `tezgah-research "
2334
- "commit %s \"<message>\"` creates it and commits the line there"
2335
- % os.path.basename(base))
2377
+ if rel is not None:
2378
+ fix = "`git -C .tezgah add -f %s`" % rel
2379
+ elif folded(repo):
2380
+ # a forced add of a `.tezgah/` path is the control rule's to refuse
2381
+ fix = ("`.tezgah` is tracked by the project - drop that ignore line, "
2382
+ "then `~/.config/tezgah/bin/tezgah-research commit %s \"<message>\"` commits the "
2383
+ "line in the project's history" % os.path.basename(base))
2384
+ else:
2385
+ fix = ("`.tezgah` has no git repository of its own - `~/.config/tezgah/bin/tezgah-research "
2386
+ "commit %s \"<message>\"` creates it and commits the line there"
2387
+ % os.path.basename(base))
2336
2388
  _soft(errors, warnings, strict,
2337
2389
  "experiment %s: %s is ignored by %s, so no commit can show the "
2338
2390
  "protocol predates the results; %s"
@@ -2532,7 +2584,7 @@ def _check_literature(base, errors, warnings, strict):
2532
2584
  _soft(errors, warnings, note in legacy or newer,
2533
2585
  "literature/%s is not in INDEX.jsonl: a note the index does not "
2534
2586
  "name is a source no reader can weigh" % note)
2535
- _index_fields(rows, errors, warnings, strict)
2587
+ _index_fields(rows, errors, warnings, strict, _evidence(_state(base)))
2536
2588
 
2537
2589
 
2538
2590
  def _read_index(index, literature, errors, warnings, strict):
@@ -2572,9 +2624,10 @@ def _read_index(index, literature, errors, warnings, strict):
2572
2624
  return rows, named
2573
2625
 
2574
2626
 
2575
- def _index_fields(rows, errors, warnings, strict):
2627
+ def _index_fields(rows, errors, warnings, strict, evidence=False):
2576
2628
  """The index entries the rules read: a grey source's quality, a verification
2577
- against two records, and the three fields a note has to carry."""
2629
+ against two records (rules 8 reads its receipts instead), and the three fields a
2630
+ note has to carry."""
2578
2631
  total = len(rows)
2579
2632
  if not total:
2580
2633
  return
@@ -2585,8 +2638,8 @@ def _index_fields(rows, errors, warnings, strict):
2585
2638
  "literature/INDEX.jsonl: %d of %d rows are a grey source with no "
2586
2639
  "quality note, so nothing says how good the practice evidence is"
2587
2640
  % (len(grey), total))
2588
- verified = [r for r in rows
2589
- if not isinstance(r.get("verified"), list) or len(r["verified"]) < 2]
2641
+ verified = [] if evidence else [
2642
+ r for r in rows if not isinstance(r.get("verified"), list) or len(r["verified"]) < 2]
2590
2643
  if verified:
2591
2644
  _soft(errors, warnings, strict,
2592
2645
  "literature/INDEX.jsonl: %d of %d rows are verified by fewer than "
@@ -2761,6 +2814,11 @@ def _rules(state):
2761
2814
  return value if isinstance(value, int) and not isinstance(value, bool) else 1
2762
2815
 
2763
2816
 
2817
+ def _evidence(state):
2818
+ """True for a line opened under the evidence rules (`EVIDENCE_RULES`)."""
2819
+ return _rules(state) >= EVIDENCE_RULES
2820
+
2821
+
2764
2822
  def _state(base):
2765
2823
  """state.json as an object, or {} when it is missing or unreadable."""
2766
2824
  state, exc = _read_json(os.path.join(base, "state.json"))
@@ -2770,16 +2828,16 @@ def _state(base):
2770
2828
  def newer_rules(state):
2771
2829
  """The refusal for a line a later tezgah wrote, or None.
2772
2830
 
2773
- A `rules` above `RULES` names rules this code does not implement. Reading the
2774
- line by the rules it does know would pass what the later set refuses, and a
2775
- writer would append rows judged by the older set. So the line is refused by
2776
- name, before any rule reads it."""
2831
+ A `rules` above `EVIDENCE_RULES` names rules this code does not implement.
2832
+ Reading the line by the rules it does know would pass what the later set
2833
+ refuses, and a writer would append rows judged by the older set. So the line
2834
+ is refused by name, before any rule reads it."""
2777
2835
  found = _rules(state)
2778
- if found <= RULES:
2836
+ if found <= EVIDENCE_RULES:
2779
2837
  return None
2780
2838
  return ("state.json rules %d is newer than the rules this tezgah implements (%d): "
2781
2839
  "a later tezgah wrote this line, so it is refused rather than read as if "
2782
- "current - upgrade tezgah to read or write it" % (found, RULES))
2840
+ "current - upgrade tezgah to read or write it" % (found, EVIDENCE_RULES))
2783
2841
 
2784
2842
 
2785
2843
  def _concluding(base, state):
@@ -2791,20 +2849,22 @@ def _concluding(base, state):
2791
2849
  or os.path.isfile(os.path.join(base, "to_human", "report.md")))
2792
2850
 
2793
2851
 
2794
- def file_versions(repo, path):
2852
+ def file_versions(repo, path, every_ref=True):
2795
2853
  """[(sha, text)] of every committed version of `path`, oldest first; [] when git
2796
2854
  cannot say. The append-only rule compares versions, which the added/last-touched
2797
2855
  pair cannot. The project's history is read first and the private `.tezgah`
2798
2856
  repository's after it, like `added_commits`: a line committed in the project
2799
2857
  before the move has its older versions there.
2800
2858
 
2801
- The order rules read HEAD's lineage; this reads every ref of the private
2802
- repository (`--all`), because it looks for a rewrite and not an order: a row
2803
- amended away while a tag keeps the old commit is a result rewritten after the
2804
- run, which HEAD alone no longer shows (an internal research decision). A side
2805
- ref can only add a version, so it can refuse a line and never pass one."""
2859
+ By default this reads every ref of the private repository (`--all`), because
2860
+ the results rule looks for a rewrite: a row amended away while a tag keeps the
2861
+ old commit is a result rewritten after the run, which HEAD alone no longer
2862
+ shows (an internal research decision); the append-only capture ledgers read it
2863
+ the same way. `every_ref=False` reads HEAD's lineage only, for the order rule of
2864
+ the captures: a backdated side branch read through `--all` supplied the oldest
2865
+ version and satisfied the stopping-rule order."""
2806
2866
  return [v for top, rel in _histories(repo, path)
2807
- for v in _versions(top, rel, ("--all",) if top != repo else ())]
2867
+ for v in _versions(top, rel, ("--all",) if every_ref and top != repo else ())]
2808
2868
 
2809
2869
 
2810
2870
  def _versions(top, rel, extra=()):
@@ -2890,19 +2950,22 @@ def _evaluation_commit(repo, path):
2890
2950
  return {top: _first_lock(top, rel) for top, rel in _histories(repo, path)}
2891
2951
 
2892
2952
 
2953
+ def _orders_lock(state):
2954
+ """True when a line's results are ordered against its evaluation lock: the
2955
+ arena is an exploratory line's lock, and `_check_arena_order` orders every
2956
+ results.jsonl against it instead; a state with no evaluation has none."""
2957
+ return not _exploratory(state) and isinstance(state.get("evaluation"), dict)
2958
+
2959
+
2893
2960
  def _check_evaluation_order(repo, base, state, errors, warnings, strict):
2894
2961
  """The evaluation is locked before the first result, and `locked_at` names a
2895
2962
  commit the history holds. `_check_evaluation` reads that the fields are filled;
2896
2963
  this reads when: a metric and a baseline committed after the results are the
2897
2964
  criterion the results were measured against after the fact."""
2898
- if _exploratory(state):
2899
- # the arena is an exploratory line's lock, and `_check_arena_order` orders
2900
- # every results.jsonl against it the way this orders them against a metric
2965
+ if not _orders_lock(state):
2901
2966
  return
2902
2967
  hard = strict or _rules(state) >= STANDARDS_RULES
2903
- evaluation = state.get("evaluation")
2904
- if not isinstance(evaluation, dict):
2905
- return
2968
+ evaluation = state["evaluation"]
2906
2969
  for token in SHA_TOKEN.findall(str(evaluation.get("locked_at", ""))):
2907
2970
  if is_ancestor(repo, token, "HEAD") is not True:
2908
2971
  _soft(errors, warnings, hard,
@@ -3533,7 +3596,8 @@ def _check_review_integrity(base, errors, warnings, strict):
3533
3596
  "to_human/review.json names no %s: a review says who produced the line "
3534
3597
  "and who judged it, and those are two readers"
3535
3598
  % ("reviewer" if not reviewer else "producer"))
3536
- elif reviewer == producer:
3599
+ elif reviewer == producer or (_evidence(state) and re.sub(r"\s+", "", reviewer).casefold()
3600
+ == re.sub(r"\s+", "", producer).casefold()):
3537
3601
  errors.append("to_human/review.json reviewer %r is the producer: a line graded "
3538
3602
  "by the session that wrote it is not reviewed" % reviewer)
3539
3603
  if review.get("findings") == []:
@@ -3551,14 +3615,977 @@ def _check_review_integrity(base, errors, warnings, strict):
3551
3615
  % (" and ".join(high), len(order), order[0][:80]))
3552
3616
 
3553
3617
 
3554
- def check_line(repo, slug, git=True, strict=False, code=None):
3618
+ # --- rules 8 (plan 127): steps, routing and the execution trail ----------------
3619
+ # A line opened under `EVIDENCE_RULES` is read for the steps that narrow each
3620
+ # claim's certainty (spec section 2) beside its refusals. `_check_rules8` threads
3621
+ # one `steps` dict - claim id -> step names in the order found - through the
3622
+ # capture module, the checks below and the evidence module, which turns the steps
3623
+ # into levels. Keys starting with `#` are the channel, never claim ids: `#flags`
3624
+ # (what `check` was asked), `#report` (the report lines `check` prints and never
3625
+ # counts), `#certainty` (each claim's level, for `check --json`) and `#stats`
3626
+ # ("experiment <h>" -> the statistics error `_check_rules8` refuses).
3627
+
3628
+ QUESTION_CLASSES = tuple("R%d" % n for n in range(13))
3629
+ CLASS_METHODS = dict(
3630
+ [(c, ("qualitative",)) for c in ("R1", "R2", "R7", "R8", "R9", "R10")]
3631
+ + [(c, ("qualitative", "measured")) for c in ("R3", "R12")]
3632
+ + [(c, ("measured",)) for c in ("R4", "R5", "R6")] + [("R11", ("exploratory",))])
3633
+ SEARCH_CLASSES = ("R1", "R2", "R7", "R8", "R9", "R10")
3634
+ # sources / searches / runs per tier: starting values, to re-measure (spec 3.3)
3635
+ BUDGETS = {"quick": (10, 10, 10), "study": (30, 40, 40), "program": (100, 150, 200)}
3636
+ LITERATURE_STEPS = ("header-mismatch", "unindexed-source", "online-drift",
3637
+ "abstract-only", "source-not-captured", "one-provider")
3638
+ SEARCH_STEPS = ("one-connector", "no-counter-search", "no-stopping-rule",
3639
+ "stopping-not-met", "known-item-missed", "screened-outside-search")
3640
+ EXPERIMENT_STEPS = ("no-replication", "no-power-plan", "below-planned-n", "no-interval",
3641
+ "interval-includes-zero", "below-achieved-mde",
3642
+ "no-multiplicity-correction", "fails-multiplicity", "no-sesoi",
3643
+ "tost-not-passed", "instrument-unvalidated", "instrument-rejected",
3644
+ "judged-row-incomplete", "falsifier-not-evaluable")
3645
+ CLASS_STEPS = {"R2": ("no-grey-quality",), "R3": ("no-population",),
3646
+ "R4": ("no-confounders",),
3647
+ "R5": ("fewer-than-two-rivals", "no-positive-control"),
3648
+ "R6": ("fewer-than-two-rivals",),
3649
+ "R7": ("no-dual-screening", "no-recall-stopping"),
3650
+ "R8": ("no-dual-screening",), "R9": ("no-amstar",), "R10": ("no-codebook",),
3651
+ "R12": ("fewer-than-two-approaches",)}
3652
+ TERMINAL_STATUSES = ("supported", "refuted", "weakened", "revised", "untested",
3653
+ "inconclusive")
3654
+ DEAD_ROWS = ("aborted", "failed")
3655
+ # The comparator words match ASCII-case-insensitively (`(?a:...)`): a Unicode
3656
+ # IGNORECASE lets U+017F match `s`, a spelling FALSIFIER_OPS holds no key for
3657
+ # (review round two R2-2).
3658
+ FALSIFIER_COMPARATOR = re.compile(
3659
+ r"(?i)(?:<=|>=|≤|≥|<|>|=|\b(?a:at most|at least|above|below|less than|more than"
3660
+ r"|fewer than|greater than|exceeds?|under|over))\s*-?\d")
3661
+ FALSIFIER_TEST = re.compile(
3662
+ r"(?i)\b(?:t-test|welch|wilcoxon|mann-whitney|chi-square|fisher|binomial|tost)\b")
3663
+ FALSIFIER_ALPHA = re.compile(r"(?i)\balpha\s*=?\s*0?\.\d+")
3664
+ # The structured form (spec 2.2) takes every comparator the grammar above names; each
3665
+ # maps to the operator it means. Anything else stays unevaluated (review D3-2).
3666
+ FALSIFIER_OPS = {"<=": "<=", ">=": ">=", "≤": "<=", "≥": ">=", "<": "<", ">": ">", "=": "=",
3667
+ "at most": "<=", "at least": ">=", "above": ">", "below": "<",
3668
+ "less than": "<", "more than": ">", "fewer than": "<", "greater than": ">",
3669
+ "exceeds": ">", "exceed": ">", "under": "<", "over": ">"}
3670
+ FALSIFIER_FORM = re.compile(
3671
+ r"(?i)^([\w.-]+)(?:\s*(<=|>=|≤|≥|<|>|=)|\s+(?a:(at\s+most|at\s+least|above|below"
3672
+ r"|less\s+than|more\s+than|fewer\s+than|greater\s+than|exceeds?|under|over)))"
3673
+ r"\s*(-?\d+(?:\.\d+)?)(?:\s+on\s+(\S+))?\s*$")
3674
+ # A protocol number (spec 2.3): `0.05`, `.05`, `5%` (0.05), `1e-1`.
3675
+ PROTOCOL_NUMBER = re.compile(r"([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?)(\s*%)?")
3676
+ # Each checked protocol number's open range (lower, upper; None = unbounded).
3677
+ PROTOCOL_RANGES = {"alpha": (0.0, 1.0), "power": (0.0, 1.0), "sesoi": (0.0, None),
3678
+ "mde": (0.0, None)}
3679
+ COMPARE_FORM = re.compile(
3680
+ r"(?i)^(\S+)\s+vs\s+(\S+)\s+on\s+([\w.-]+)\s*,\s*(lower|higher)\s+is\s+better")
3681
+ CLAIM_NUMBER = re.compile(r"(?<![\w.])-?\d+(?:\.\d+)?(?![\w.])")
3682
+ DERIVED_REF = re.compile(r"^([\w.-]+)/(#\d+|[\w.-]+):([\w.-]+)$")
3683
+ UNIVERSAL_WORDS = re.compile(r"(?i)\b(every|all|always|never|any)\b")
3684
+ CAUSAL_WORDS = re.compile(r"(?i)\b(causes|caused|cause|leads to|due to|because of)\b")
3685
+ CODE_SUFFIXES = (".py", ".sh", ".js", ".ts", ".rs", ".go")
3686
+
3687
+
3688
+ def obligations(cls, method=""):
3689
+ """The step names of spec section 2 a line of class `cls` can trigger."""
3690
+ out = ["no-question-class", "class-unreviewed", "method-not-allowed",
3691
+ "untraced-number", "uncalibrated-verifier"] + list(LITERATURE_STEPS)
3692
+ if cls in SEARCH_CLASSES or method == "qualitative":
3693
+ out += SEARCH_STEPS
3694
+ if method == "measured" or "measured" in CLASS_METHODS.get(cls, ()):
3695
+ out += EXPERIMENT_STEPS
3696
+ return out + list(CLASS_STEPS.get(cls, ()))
3697
+
3698
+
3699
+ def _step(steps, cid, name):
3700
+ """Name `name` once for claim `cid`, in the order found."""
3701
+ found = steps.setdefault(cid, [])
3702
+ if name not in found:
3703
+ found.append(name)
3704
+
3705
+
3706
+ def _fields(text):
3707
+ """{key: [values]} of every column-0 `key: value` line of a protocol."""
3708
+ out = {}
3709
+ for line in text.lstrip("\ufeff").splitlines():
3710
+ found = PROTOCOL_LABEL.match(line)
3711
+ if found:
3712
+ out.setdefault(found.group(1).lower(), []).append(found.group(2).strip())
3713
+ return out
3714
+
3715
+
3716
+ def _first(fields, key):
3717
+ return (fields.get(key) or [""])[0]
3718
+
3719
+
3720
+ def _number(value):
3721
+ """The first number of a protocol value (`PROTOCOL_NUMBER`), a percent read as its
3722
+ fraction; None without one."""
3723
+ found = PROTOCOL_NUMBER.search(value or "")
3724
+ if not found:
3725
+ return None
3726
+ number = float(found.group(1))
3727
+ return number / 100 if found.group(2) else number
3728
+
3729
+
3730
+ def protocol_number(key, value):
3731
+ """(number, problem) of the protocol line `key: value`, `key` one of PROTOCOL_RANGES:
3732
+ the one reader `check`, `claim`, the session context and the instruments share.
3733
+ (None, None) when the line is absent or empty; a value with no number, or one
3734
+ outside its range (alpha, power in (0, 1); sesoi, mde above 0), is an invalid
3735
+ record: (None, the refusal naming the key and the value). So is an alpha so close
3736
+ to 0 that alpha/2 or 1 - alpha/2, the quantiles the intervals read, is 0 or 1 as a
3737
+ float (`1e-20`, review round two R2-1)."""
3738
+ value = (value or "").strip()
3739
+ if not value:
3740
+ return None, None
3741
+ number = _number(value)
3742
+ lo, hi = PROTOCOL_RANGES[key]
3743
+ if number is not None and math.isfinite(number) and number > lo \
3744
+ and (hi is None or number < hi):
3745
+ if key == "alpha" and not (number / 2 > 0 and 1 - number / 2 < 1):
3746
+ return None, "protocol.md alpha: %s leaves a quantile at 0 or 1" % value
3747
+ return number, None
3748
+ return None, "protocol.md %s: %s is not a number %s" % (
3749
+ key, value, "above 0" if hi is None else "in (0, 1)")
3750
+
3751
+
3752
+ def _numeric(value):
3753
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
3754
+
3755
+
3756
+ def _live(rows):
3757
+ """Rows that count. A refile is a correction: the latest row filed for the same
3758
+ `run`, `arm`, `item` and `instrument` stands for it, so one run measured over
3759
+ several arms or items keeps a row for each; a row with no `run` counts on its
3760
+ own. An `aborted` or `failed` run stays a row and counts nowhere."""
3761
+ key = lambda r: tuple(str(r.get(k)) for k in ("run", "arm", "item", "instrument")) # noqa: E731
3762
+ last = {key(r): n for n, r in enumerate(rows) if r.get("run") is not None}
3763
+ return [r for n, r in enumerate(rows)
3764
+ if (r.get("run") is None or last[key(r)] == n)
3765
+ and r.get("status") not in DEAD_ROWS]
3766
+
3767
+
3768
+ def _groups(rows):
3769
+ """{group: rows}: by `arm`, else `phase`, else one group `all`."""
3770
+ live = _live(rows)
3771
+ key = next((k for k in ("arm", "phase") if any(k in r for r in live)), None)
3772
+ out = {}
3773
+ for row in live:
3774
+ out.setdefault(str(row[key]) if key in row else "all", []).append(row)
3775
+ return out
3776
+
3777
+
3778
+ def _paired(rows_a, rows_b):
3779
+ """The two groups aligned by `item` when both carry the same item set, else None."""
3780
+ items_a, items_b = [r.get("item") for r in rows_a], [r.get("item") for r in rows_b]
3781
+ if None in items_a or None in items_b or len(set(map(str, items_a))) != len(items_a):
3782
+ return None
3783
+ if sorted(map(str, items_a)) != sorted(map(str, items_b)):
3784
+ return None
3785
+ order = {str(i): n for n, i in enumerate(items_a)}
3786
+ return rows_a, sorted(rows_b, key=lambda r: order[str(r.get("item"))])
3787
+
3788
+
3789
+ def _effect(fields, groups, alpha, power):
3790
+ """The `compare:` line's effect (spec 2.3), or None without one."""
3791
+ import tezgah_research_stats as stats
3792
+ found = COMPARE_FORM.match(_first(fields, "compare"))
3793
+ if not found:
3794
+ return None
3795
+ t, c, field, better = found.groups()
3796
+ rows_t, rows_c = groups.get(t, []), groups.get(c, [])
3797
+ out = {"t": t, "c": c, "field": field, "n": (len(rows_t), len(rows_c)),
3798
+ "effect": None, "ci": None, "mde": None, "p": None, "a": [], "b": [],
3799
+ "paired": False}
3800
+ rows_t = [r for r in rows_t if _numeric(r.get(field))]
3801
+ rows_c = [r for r in rows_c if _numeric(r.get(field))]
3802
+ if better.lower() == "lower":
3803
+ rows_t, rows_c = rows_c, rows_t
3804
+ aligned = _paired(rows_t, rows_c) if len(rows_t) >= 2 else None
3805
+ if aligned:
3806
+ rows_t, rows_c = aligned
3807
+ a, b = [float(r[field]) for r in rows_t], [float(r[field]) for r in rows_c]
3808
+ out.update(a=a, b=b, paired=bool(aligned))
3809
+ if a and b:
3810
+ out["effect"] = sum(a) / len(a) - sum(b) / len(b)
3811
+ if len(a) >= 2 and len(b) >= 2:
3812
+ diff = stats.mean_diff(a, b, alpha, paired=bool(aligned))
3813
+ out.update(effect=diff["effect"], ci=(diff["lo"], diff["hi"]), p=diff["p"],
3814
+ mde=stats.achieved_mde(diff["se"], alpha, power))
3815
+ return out
3816
+
3817
+
3818
+ def _stats_failed(exp, exc):
3819
+ """Keep an experiment's first statistics error (ValueError, ArithmeticError);
3820
+ `_check_rules8` turns it into the experiment's one refusal (review round two R2-1)."""
3821
+ if not exp.get("stats_error"):
3822
+ exp["stats_error"] = str(exc) or type(exc).__name__
3823
+
3824
+
3825
+ def experiment_record(base, h):
3826
+ """One experiment as the rules-8 checks read it: protocol fields, rows, groups,
3827
+ the plan's numbers and the `compare:` effect. `invalid` holds the refusal of each
3828
+ protocol number outside its range; an invalid alpha or power is None and leaves
3829
+ the effect uncomputed, so no statistic runs on it. A statistics error leaves the
3830
+ effect None and lands in `stats_error`."""
3831
+ d = os.path.join(base, "experiments", h)
3832
+ fields = _fields(_note_text(os.path.join(d, "protocol.md")))
3833
+ rows = _row_objects(os.path.join(d, "results.jsonl"))
3834
+ groups = _groups(rows)
3835
+ plan = {k: protocol_number(k, _first(fields, k)) for k in PROTOCOL_RANGES}
3836
+ alpha, power = (None if plan[k][1] else plan[k][0] or default
3837
+ for k, default in (("alpha", 0.05), ("power", 0.80)))
3838
+ exp = {"h": h, "dir": d, "fields": fields, "rows": rows, "groups": groups,
3839
+ "alpha": alpha, "power": power, "sesoi": plan["sesoi"][0],
3840
+ "invalid": [problem for _, problem in plan.values() if problem],
3841
+ "effect": None, "stats_error": None}
3842
+ if alpha is not None and power is not None:
3843
+ try:
3844
+ exp["effect"] = _effect(fields, groups, alpha, power)
3845
+ except (ValueError, ArithmeticError) as exc:
3846
+ _stats_failed(exp, exc)
3847
+ return exp
3848
+
3849
+
3850
+ def falsifier_unscoped(text, groups):
3851
+ """True when a structured falsifier names no `on <group>` while the rows hold
3852
+ more than one group: its mean would pool the control with the arm it judges."""
3853
+ found = FALSIFIER_FORM.match(text)
3854
+ return bool(found and not found.group(5) and len(groups) > 1)
3855
+
3856
+
3857
+ def falsifier_fired(exp):
3858
+ """(falsifier, mean) when the structured `falsifier:` holds over its rows, else
3859
+ None - also when the group holds no row carrying the field, and when it names no
3860
+ group over several (`falsifier_unscoped`)."""
3861
+ text = _first(exp["fields"], "falsifier")
3862
+ found = FALSIFIER_FORM.match(text)
3863
+ if not found or falsifier_unscoped(text, exp["groups"]):
3864
+ return None
3865
+ field, symbol, words, number, group = found.groups()
3866
+ op = FALSIFIER_OPS.get(" ".join((symbol or words).casefold().split()))
3867
+ if op is None:
3868
+ return None
3869
+ rows = exp["groups"].get(group, []) if group else _live(exp["rows"])
3870
+ values = [float(r[field]) for r in rows if _numeric(r.get(field))]
3871
+ if not values:
3872
+ return None
3873
+ mean, x = sum(values) / len(values), float(number)
3874
+ holds = {"<": mean < x, "<=": mean <= x, ">": mean > x, ">=": mean >= x,
3875
+ "=": mean == x}[op]
3876
+ return (text, mean) if holds else None
3877
+
3878
+
3879
+ def _claim_experiments(claim, base):
3880
+ return [h for h in _experiments_named(_cited(claim.get("proof")))
3881
+ if os.path.isdir(os.path.join(base, "experiments", h))]
3882
+
3883
+
3884
+ def fired_problems(cid, claim, base):
3885
+ """The refusal for a `supported` claim whose cited experiment's falsifier fired."""
3886
+ if claim.get("status") != "supported":
3887
+ return []
3888
+ out = []
3889
+ for h in _claim_experiments(claim, base):
3890
+ fired = falsifier_fired(experiment_record(base, h))
3891
+ if fired:
3892
+ out.append("claim %s is supported while its falsifier fired: experiments/%s "
3893
+ "`falsifier: %s` holds at a mean of %s" % (cid, h, fired[0],
3894
+ "%g" % fired[1]))
3895
+ return out
3896
+
3897
+
3898
+ def _ref_value(base, ref):
3899
+ """The number `<h>/<run>:<field>` or `<h>/#<row>:<field>` names, as the Decimal of
3900
+ the value written in the row (`repr` gives back a JSON float's shortest digits), or
3901
+ None."""
3902
+ found = DERIVED_REF.match(str(ref).strip())
3903
+ if not found:
3904
+ return None
3905
+ h, which, field = found.groups()
3906
+ rows = _row_objects(os.path.join(base, "experiments", h, "results.jsonl"))
3907
+ if which.startswith("#"):
3908
+ k = int(which[1:])
3909
+ row = rows[k - 1] if 1 <= k <= len(rows) else None
3910
+ else:
3911
+ row = next((r for r in reversed(rows) if str(r.get("run")) == which), None)
3912
+ value = row.get(field) if row else None
3913
+ if not _numeric(value) or (isinstance(value, float) and not math.isfinite(value)):
3914
+ return None
3915
+ return decimal.Decimal(repr(value))
3916
+
3917
+
3918
+ def _recompute(spec, base):
3919
+ if not isinstance(spec, dict) or not isinstance(spec.get("of"), list):
3920
+ return None
3921
+ op, refs = spec.get("op"), spec["of"]
3922
+ if op == "count":
3923
+ return decimal.Decimal(len(refs))
3924
+ values = [_ref_value(base, ref) for ref in refs]
3925
+ if not values or None in values:
3926
+ return None
3927
+ if op in ("diff", "ratio"):
3928
+ if len(values) != 2 or (op == "ratio" and values[1] == 0):
3929
+ return None
3930
+ return values[0] - values[1] if op == "diff" else values[0] / values[1]
3931
+ return {"mean": lambda v: sum(v) / len(v), "sum": sum, "min": min,
3932
+ "max": max}.get(op, lambda v: None)(values)
3933
+
3934
+
3935
+ def _rounds_to(value, written):
3936
+ """True when `value` (a Decimal) rounded half-up or half-even at the precision of the
3937
+ number as written equals it."""
3938
+ try:
3939
+ target = decimal.Decimal(written)
3940
+ return any(value.quantize(target, rounding=mode) == target
3941
+ for mode in (decimal.ROUND_HALF_UP, decimal.ROUND_HALF_EVEN))
3942
+ except decimal.DecimalException:
3943
+ return False
3944
+
3945
+
3946
+ def derived_problems(cid, claim, base):
3947
+ """The refusals for `derived_from` entries whose recomputed value differs from
3948
+ the number as written (spec 2.4); writer and checker read this one rule. The
3949
+ arithmetic is decimal over the rows' written values, so 2.675 is 2.675, not the
3950
+ binary float below it."""
3951
+ derived = claim.get("derived_from")
3952
+ if derived is None:
3953
+ return []
3954
+ if not isinstance(derived, dict):
3955
+ return ["claim %s derived_from is not an object of number -> {op, of}" % cid]
3956
+ out = []
3957
+ with decimal.localcontext() as ctx:
3958
+ ctx.prec = 50
3959
+ for written, spec in derived.items():
3960
+ value = _recompute(spec, base)
3961
+ if value is None or not _rounds_to(value, written):
3962
+ out.append("claim %s derived_from %s does not recompute: %s gives %s"
3963
+ % (cid, written, json.dumps(spec),
3964
+ "nothing" if value is None else "%g" % value))
3965
+ return out
3966
+
3967
+
3968
+ def _log_tokens(exp, row, cache):
3969
+ rel = row.get("log")
3970
+ if not isinstance(rel, str) or not rel:
3971
+ return set()
3972
+ for root in (exp["dir"], os.path.dirname(os.path.dirname(exp["dir"]))):
3973
+ path = os.path.join(root, rel)
3974
+ if os.path.isfile(path):
3975
+ if path not in cache:
3976
+ cache[path] = set(CLAIM_NUMBER.findall(_note_text(path)))
3977
+ return cache[path]
3978
+ return set()
3979
+
3980
+
3981
+ def _untraced(claim, cited):
3982
+ """The numbers of an evidence claim no cited row holds with its log, and no
3983
+ `derived_from` entry names (spec 2.4)."""
3984
+ derived = claim.get("derived_from")
3985
+ derived = derived if isinstance(derived, dict) else {}
3986
+ cache, out = {}, []
3987
+ for token in CLAIM_NUMBER.findall(str(claim.get("statement", ""))):
3988
+ if token in derived:
3989
+ continue
3990
+ value = float(token)
3991
+ if not any(token in _log_tokens(exp, row, cache)
3992
+ and any(_numeric(v) and float(v) == value for v in row.values())
3993
+ for exp in cited for row in _live(exp["rows"])):
3994
+ out.append(token)
3995
+ return out
3996
+
3997
+
3998
+ def _rivals(fields):
3999
+ return [v for v in fields.get("rival", [])
4000
+ if re.match(r"\S.*\sexcludes:\s*\S", v)]
4001
+
4002
+
4003
+ def _observational(fields):
4004
+ design = _first(fields, "design").lower()
4005
+ return design == "observational" or (not design and not _first(fields, "change"))
4006
+
4007
+
4008
+ def _experiment_steps(claim, cited, cls, measured, rejected):
4009
+ """The section-2 steps an experiment claim takes from the experiments it cites."""
4010
+ import tezgah_research_stats as stats
4011
+ out = []
4012
+ no_difference = claim.get("effect") == "no-difference"
4013
+ for exp in cited:
4014
+ fields, groups, eff = exp["fields"], exp["groups"], exp["effect"]
4015
+ if cls in ("R5", "R6") and len(_rivals(fields)) < 2:
4016
+ out.append("fewer-than-two-rivals")
4017
+ if not groups or any(len(rows) < 2 for rows in groups.values()):
4018
+ out.append("no-replication")
4019
+ if measured and not all(_first(fields, k) for k in ("alpha", "power", "mde", "n")):
4020
+ out.append("no-power-plan")
4021
+ falsifier = _first(fields, "falsifier")
4022
+ if measured and falsifier and (not falsifier_testable(falsifier)
4023
+ or falsifier_unscoped(falsifier, groups)):
4024
+ out.append("falsifier-not-evaluable")
4025
+ if eff:
4026
+ planned = _number(_first(fields, "n"))
4027
+ if planned is not None and min(eff["n"]) < planned:
4028
+ out.append("below-planned-n")
4029
+ if eff["ci"] is None:
4030
+ out.append("no-interval")
4031
+ elif not no_difference:
4032
+ if eff["ci"][0] <= 0:
4033
+ out.append("interval-includes-zero")
4034
+ if eff["effect"] < eff["mde"]:
4035
+ out.append("below-achieved-mde")
4036
+ correction = _first(fields, "correction")
4037
+ if len(groups) > 2 and not correction:
4038
+ out.append("no-multiplicity-correction")
4039
+ if correction and exp["h"] not in rejected:
4040
+ out.append("fails-multiplicity")
4041
+ if no_difference:
4042
+ sesoi = exp["sesoi"]
4043
+ if sesoi is None:
4044
+ out.append("no-sesoi")
4045
+ else:
4046
+ try:
4047
+ passed = bool(eff and eff["ci"] and stats.tost(
4048
+ eff["a"], eff["b"], sesoi, exp["alpha"], eff["paired"]))
4049
+ except (ValueError, ArithmeticError) as exc:
4050
+ _stats_failed(exp, exc)
4051
+ passed = False
4052
+ if not passed:
4053
+ out.append("tost-not-passed")
4054
+ return out
4055
+
4056
+
4057
+ def _multiplicity(base, exps):
4058
+ """The experiments their declared `correction:` rejects, corrected together."""
4059
+ import tezgah_research_stats as stats
4060
+ tests = {"bh": stats.bh, "holm": stats.holm, "bonferroni": stats.bonferroni}
4061
+ by = {}
4062
+ for h, exp in sorted(exps.items()):
4063
+ method = _first(exp["fields"], "correction").strip().lower()
4064
+ if method and exp["effect"]:
4065
+ by.setdefault(method, []).append(h)
4066
+ rejected = set()
4067
+ for method, hs in by.items():
4068
+ alpha = min(exps[h]["alpha"] for h in hs)
4069
+ try:
4070
+ if method == "e-bh":
4071
+ values = [_number(_first(_fields(_note_text(os.path.join(
4072
+ exps[h]["dir"], "analysis.md"))), "e-value")) or 0.0 for h in hs]
4073
+ flags = stats.ebh(values, alpha)
4074
+ elif method in tests:
4075
+ p = [exps[h]["effect"]["p"] for h in hs]
4076
+ flags = tests[method]([1.0 if v is None else v for v in p], alpha)
4077
+ else:
4078
+ continue
4079
+ except (ValueError, ArithmeticError) as exc:
4080
+ for h in hs:
4081
+ _stats_failed(exps[h], exc)
4082
+ continue
4083
+ rejected.update(h for h, flag in zip(hs, flags) if flag)
4084
+ return rejected
4085
+
4086
+
4087
+ def _format_ci(lo, hi):
4088
+ return "[%.4f, %.4f]" % (lo, hi)
4089
+
4090
+
4091
+ def falsifier_testable(text):
4092
+ """True when a falsifier holds a comparator and a number, or a named test with its
4093
+ alpha (spec 2.2)."""
4094
+ return bool(FALSIFIER_COMPARATOR.search(text)
4095
+ or (FALSIFIER_TEST.search(text) and FALSIFIER_ALPHA.search(text)))
4096
+
4097
+
4098
+ # A `metric:` list splits at the commas that separate names: never inside parentheses
4099
+ # (`(ms, median of 5 runs)`), never between digit groups (`10,000-piece`), and never
4100
+ # inside the locked name, which is one name (plan 128 K1, K3).
4101
+ METRIC_COMMA = re.compile(r",(?![^()]*\))(?!(?<=\d,)\d)")
4102
+ # A word of a metric name, for the reworded comparison: hyphen and underscore split,
4103
+ # digits kept (`p95`), and the joining words a reworded name may drop or add
4104
+ # (`of`, `the`, `per`) ignored.
4105
+ METRIC_WORD = re.compile(r"[0-9a-z]+")
4106
+ METRIC_JOIN = {"a", "an", "the", "of", "for", "in", "on", "to", "per", "by", "with"}
4107
+
4108
+
4109
+ def _metric_key(name):
4110
+ """A metric name as the rules compare it: casefolded, whitespace collapsed, a
4111
+ trailing parenthetical (`(ms)`) dropped."""
4112
+ return " ".join(re.sub(r"\s*\([^)]*\)\s*$", "", str(name or "")).casefold().split())
4113
+
4114
+
4115
+ def _metric_words(key):
4116
+ return {w for w in METRIC_WORD.findall(key) if w not in METRIC_JOIN}
4117
+
4118
+
4119
+ def _same_metric(a, b):
4120
+ """True when one metric key is the other or a word-boundary prefix of it, either
4121
+ way (plan 128 K2), or a rewording of it: every word of `b` sits in `a`, in any
4122
+ order and with the joining words ignored (`read-path p95 latency` is
4123
+ `p95 latency of the read path`, K6 raters X6 and Y6)."""
4124
+ if a == b or a.startswith(b + " ") or b.startswith(a + " "):
4125
+ return True
4126
+ words = _metric_words(b)
4127
+ return bool(words) and words <= _metric_words(a)
4128
+
4129
+
4130
+ def _registered(state, fields):
4131
+ """(names, keys, locked key): the metrics a protocol's `metric:` lines register,
4132
+ split at METRIC_COMMA with the locked name kept whole, and the line's lock."""
4133
+ evaluation = state.get("evaluation") if isinstance(state.get("evaluation"), dict) else {}
4134
+ locked = _metric_key(evaluation.get("metric"))
4135
+ whole = re.compile(r"\s*,\s*".join(r"\s+".join(map(re.escape, part.split()))
4136
+ for part in locked.split(",")), re.I) \
4137
+ if "," in locked else None
4138
+ names = []
4139
+ for value in fields.get("metric", []):
4140
+ if whole:
4141
+ value = whole.sub(lambda m: m.group(0).replace(",", "\0"), value)
4142
+ names += [m.strip().replace("\0", ",") for m in METRIC_COMMA.split(value)
4143
+ if m.strip()]
4144
+ return names, [_metric_key(m) for m in names], locked
4145
+
4146
+
4147
+ def metric_problem(state, fields):
4148
+ """The locked-metric refusal of a protocol's `metric:` lines, or None (spec 2.2):
4149
+ one of them has to be the locked metric (`_same_metric`). `source` asks it
4150
+ before a rules-8 capture freezes the protocol (plan 128 K4)."""
4151
+ names, keys, locked = _registered(state, fields)
4152
+ if locked and keys and not any(_same_metric(k, locked) for k in keys):
4153
+ return ("protocol.md registers a metric other than the locked one (%s, locked: %s)"
4154
+ % (", ".join(names), locked))
4155
+ return None
4156
+
4157
+
4158
+ def _check_experiment8(repo, slug, base, state, exp, report, errors, warnings, strict,
4159
+ git, lines, code=None):
4160
+ """One experiment's rules-8 refusals (2.2, 2.4) and report lines (2.3)."""
4161
+ import tezgah_research_stats as stats
4162
+ h, fields = exp["h"], exp["fields"]
4163
+ label = "experiment %s" % h
4164
+ errors.extend("%s: %s" % (label, problem) for problem in exp["invalid"])
4165
+ problem = metric_problem(state, fields)
4166
+ if problem:
4167
+ errors.append("%s: %s" % (label, problem))
4168
+ if report is not None:
4169
+ metrics, norm, locked = _registered(state, fields)
4170
+ excused = {_metric_key(v.split(":", 1)[0])
4171
+ for v in fields.get("not_reported", []) if ":" in v}
4172
+ text = " ".join(report.casefold().split())
4173
+ # protocol.md is frozen once the run is filed (`changed after the run`), so
4174
+ # `not_reported:` is an exit only before it (plan 128 K5)
4175
+ exit_ = ("the run is filed, so protocol.md is frozen: name it in to_human/report.md"
4176
+ if exp["rows"] else
4177
+ "name it in to_human/report.md, or before the run mark it "
4178
+ "`not_reported: <metric>: <reason>` in protocol.md")
4179
+ for metric, key in zip(metrics, norm):
4180
+ # a registered metric that is the locked one (the rule above's
4181
+ # equivalence) is reported where the report names the locked metric
4182
+ refines = locked and _same_metric(key, locked)
4183
+ if (key not in text and not (refines and locked in text)
4184
+ and key not in excused):
4185
+ errors.append("%s: registered metric %r is neither reported nor marked "
4186
+ "not_reported - %s" % (label, metric, exit_))
4187
+ evaluator = _first(fields, "evaluator")
4188
+ if evaluator:
4189
+ _check_evaluator(repo, base, state, exp, evaluator, label, errors, git, code)
4190
+ select = re.match(r"(?i)^(min|max)\s+([\w.-]+)", _first(fields, "select"))
4191
+ if select:
4192
+ field = select.group(2)
4193
+ values = {g: [float(r[field]) for r in rows if _numeric(r.get(field))]
4194
+ for g, rows in exp["groups"].items()}
4195
+ means = {g: sum(v) / len(v) for g, v in values.items() if v}
4196
+ if means:
4197
+ pick = (min if select.group(1).lower() == "min" else max)(
4198
+ sorted(means), key=means.get)
4199
+ found = re.search(r"(?im)^\W*selected\W*:\s*`?([\w.-]+)",
4200
+ _note_text(os.path.join(exp["dir"], "analysis.md")))
4201
+ if not found:
4202
+ _soft(errors, warnings, strict,
4203
+ "%s: analysis states no selected group, and `select:` picks %s"
4204
+ % (label, pick))
4205
+ elif found.group(1) != pick:
4206
+ errors.append("%s: select rule picks %s, and analysis.md selected: %s"
4207
+ % (label, pick, found.group(1)))
4208
+ for group, rows in exp["groups"].items():
4209
+ # binary: every outcome 0 or 1, a JSON boolean counting as 1/0 (True == 1)
4210
+ if rows and exp["alpha"] is not None and all(r.get("outcome") in (0, 1)
4211
+ for r in rows):
4212
+ k, n = sum(int(r["outcome"]) for r in rows), len(rows)
4213
+ try:
4214
+ wilson = _format_ci(*stats.wilson(k, n, exp["alpha"]))
4215
+ cp = _format_ci(*stats.clopper_pearson(k, n, exp["alpha"]))
4216
+ except (ValueError, ArithmeticError) as exc:
4217
+ _stats_failed(exp, exc)
4218
+ continue
4219
+ lines.append("stats %s: %s %s n=%d k=%d wilson=%s cp=%s"
4220
+ % (slug, h, group, n, k, wilson, cp))
4221
+ eff = exp["effect"]
4222
+ if eff:
4223
+ lines.append("effect %s: %s %s vs %s on %s effect=%s ci=%s mde=%s"
4224
+ % (slug, h, eff["t"], eff["c"], eff["field"],
4225
+ "none" if eff["effect"] is None else "%.4f" % eff["effect"],
4226
+ _format_ci(*eff["ci"]) if eff["ci"] else "none",
4227
+ "none" if eff["mde"] is None else "%.4f" % eff["mde"]))
4228
+
4229
+
4230
+ def evaluator_file(repo, base, code, rel):
4231
+ """(path, root): an `evaluator:` path looked up in the line, the repository and
4232
+ the checkout `check` runs from (`code`, plan 128 K9), with the repository whose
4233
+ history holds it; (None, None) when none holds the file."""
4234
+ for root, at in ((repo, base), (repo, repo), (code, code)):
4235
+ if at and os.path.isfile(os.path.join(at, rel)):
4236
+ return os.path.join(at, rel), root
4237
+ return None, None
4238
+
4239
+
4240
+ def _commit_time(repo, path, sha):
4241
+ """The commit time of `sha` in the first history of `path` that holds it, or ""."""
4242
+ stamp = ""
4243
+ for top, _rel in _histories(repo, path):
4244
+ out, err = _git_out(top, "show", "-s", "--format=%cI", sha)
4245
+ stamp = stamp or ("" if err else out.strip())
4246
+ return stamp
4247
+
4248
+
4249
+ def _text_at(root, path, sha, stamp):
4250
+ """`path`'s committed text at commit `sha` - in a history that does not hold that
4251
+ commit (a project file read against a line in `.tezgah`'s repository), its latest
4252
+ version not after `stamp` - or None."""
4253
+ found = None
4254
+ for top, rel in _histories(root, path):
4255
+ rev = sha if _holds(top, sha) else None
4256
+ if rev is None and stamp:
4257
+ out, err = _git_out(top, "log", "-1", "--format=%H", "--until=" + stamp,
4258
+ "--", rel)
4259
+ rev = None if err else out.strip()
4260
+ if rev:
4261
+ out, err = _git_out(top, "show", "%s:%s" % (rev, rel))
4262
+ found = found if err else out
4263
+ return found
4264
+
4265
+
4266
+ def _sealed_evaluator(repo, base, state, h, root, path, git):
4267
+ """The evaluator's sha256 at the line's conclusion, or None for a line not
4268
+ concluded (plan 128 K8): the hash `conclude` sealed, and for a seal older than
4269
+ that field the evaluator at the first committed state.json holding the seal."""
4270
+ import hashlib
4271
+ seal = state.get(SEAL)
4272
+ if not isinstance(seal, dict):
4273
+ return None
4274
+ rows = seal.get("experiments") if isinstance(seal.get("experiments"), dict) else {}
4275
+ row = rows.get(h) if isinstance(rows.get(h), dict) else {}
4276
+ if SHA256_HEX.fullmatch(str(row.get("evaluator") or "")):
4277
+ return row["evaluator"]
4278
+ if not git:
4279
+ return None
4280
+ state_path = os.path.join(base, "state.json")
4281
+ for top, rel in _histories(repo, state_path):
4282
+ for sha, text in _versions(top, rel):
4283
+ try:
4284
+ held = SEAL in json.loads(text)
4285
+ except (ValueError, TypeError):
4286
+ held = False
4287
+ if held:
4288
+ at = _text_at(root, path, sha, _commit_time(repo, state_path, sha))
4289
+ return None if at is None else hashlib.sha256(at.encode("utf-8")).hexdigest()
4290
+ return None
4291
+
4292
+
4293
+ def _check_evaluator(repo, base, state, exp, evaluator, label, errors, git, code=None):
4294
+ """An `evaluator:` file holds the bytes it held at the protocol's first commit -
4295
+ in a history that does not hold that commit (a project evaluator of a line in
4296
+ `.tezgah`'s repository), its latest version not after that commit's time - and
4297
+ its declared `sha256:` (spec 2.4). A concluded line reads the evaluator as it was
4298
+ at the conclusion (`_sealed_evaluator`), so a later project edit is not its own."""
4299
+ import hashlib
4300
+ parts = evaluator.split()
4301
+ path, root = evaluator_file(repo, base, code, parts[0])
4302
+ declared = next((p.split(":", 1)[1] for p in parts[1:] if p.startswith("sha256:")), "")
4303
+ if path is None:
4304
+ errors.append("%s: evaluator %s is not a file in the line, the repository or the "
4305
+ "checkout" % (label, parts[0]))
4306
+ return
4307
+ now = _sealed_evaluator(repo, base, state, exp["h"], root, path, git) or _digest(path)
4308
+ if declared and now != declared.lower():
4309
+ errors.append("%s: evaluator file changed after lock: %s no longer hashes to its "
4310
+ "declared sha256" % (label, parts[0]))
4311
+ return
4312
+ if not git:
4313
+ return
4314
+ proto_path = os.path.join(exp["dir"], "protocol.md")
4315
+ proto = file_versions(repo, proto_path)
4316
+ if not proto:
4317
+ return
4318
+ sha = proto[0][0]
4319
+ locked = _text_at(root, path, sha, _commit_time(repo, proto_path, sha))
4320
+ if locked is None:
4321
+ # committed only after the lock: its first version is the nearest to it
4322
+ versions = file_versions(root, path)
4323
+ locked = versions[0][1] if versions else None
4324
+ if locked is not None and hashlib.sha256(locked.encode("utf-8")).hexdigest() != now:
4325
+ errors.append("%s: evaluator file changed after lock: %s differs from its state "
4326
+ "at the protocol's first commit" % (label, parts[0]))
4327
+
4328
+
4329
+ def _overclaim(cid, claim, cited, warnings):
4330
+ """Universal words over fixture evidence, causal words over an observational or
4331
+ single-group experiment (spec 4.4). Warns in both modes."""
4332
+ text = str(claim.get("statement", ""))
4333
+ rows = [r for exp in cited for r in exp["rows"]]
4334
+ words = []
4335
+ if claim.get("scope") == "fixture" or (rows and all(r.get("scope") == "fixture"
4336
+ for r in rows)):
4337
+ words += UNIVERSAL_WORDS.findall(text)
4338
+ if any(_observational(exp["fields"]) or len(exp["groups"]) < 2 for exp in cited):
4339
+ words += CAUSAL_WORDS.findall(text)
4340
+ if words:
4341
+ warnings.append("claim %s overclaims: %s - the evidence it cites does not reach "
4342
+ "that far; narrow the claim to what was measured"
4343
+ % (cid, ", ".join('"%s"' % w for w in dict.fromkeys(words))))
4344
+
4345
+
4346
+ def _routing_steps(base, state, exps, review):
4347
+ """The 3.3 steps that touch every claim of the line."""
4348
+ cls = state.get("question_class")
4349
+ out = []
4350
+ if not cls:
4351
+ out.append("no-question-class")
4352
+ if review is not None and review.get("question_class") != cls:
4353
+ out.append("class-unreviewed")
4354
+ if cls in CLASS_METHODS and state.get("method") not in CLASS_METHODS[cls]:
4355
+ out.append("method-not-allowed")
4356
+ index = _row_objects(os.path.join(base, "literature", "INDEX.jsonl"))
4357
+ protocols = [exp["fields"] for exp in exps.values()]
4358
+ if cls in ("R7", "R8"):
4359
+ screeners, dropped = {}, set()
4360
+ for row in _row_objects(os.path.join(base, "searches", "screening.jsonl")):
4361
+ rid = str(row.get("id"))
4362
+ if rid not in screeners and row.get("decision") == "dropped":
4363
+ dropped.add(rid)
4364
+ screeners.setdefault(rid, set()).add(
4365
+ re.sub(r"\s+", "", str(row.get("screener") or "")).casefold())
4366
+ dual = {rid for rid, who in screeners.items() if len(who - {""}) >= 2}
4367
+ if cls == "R7":
4368
+ thin = not screeners or len(dual) < len(screeners)
4369
+ else:
4370
+ thin = (not screeners or 5 * len(dual) < len(screeners)
4371
+ or bool(dropped - dual))
4372
+ if thin:
4373
+ out.append("no-dual-screening")
4374
+ stopping = state.get("stopping")
4375
+ if cls == "R7" and not (isinstance(stopping, dict)
4376
+ and stopping.get("rule") == "hypergeometric"):
4377
+ out.append("no-recall-stopping")
4378
+ if cls == "R5" and state.get("method") == "measured" and any(
4379
+ not _first(f, "control") for f in protocols):
4380
+ out.append("no-positive-control")
4381
+ if cls == "R4" and any(not _first(f, "confounders") for f in protocols):
4382
+ out.append("no-confounders")
4383
+ if cls == "R3" and not (state.get("population") and state.get("n")):
4384
+ out.append("no-population")
4385
+ if cls == "R2" and any(r.get("class") == "grey" and not str(r.get("quality", "")).strip()
4386
+ for r in index):
4387
+ out.append("no-grey-quality")
4388
+ if cls == "R9" and any(not str(r.get("amstar", "")).strip() for r in index):
4389
+ out.append("no-amstar")
4390
+ if cls == "R10" and not os.path.isfile(os.path.join(base, "codebook.md")):
4391
+ out.append("no-codebook")
4392
+ approaches = state.get("approaches")
4393
+ if cls == "R12" and sum(1 for a in approaches or [] if isinstance(a, dict)
4394
+ and a.get("biases")) < 2:
4395
+ out.append("fewer-than-two-approaches")
4396
+ return out
4397
+
4398
+
4399
+ def budget(slug, base, state):
4400
+ """(report line, [what is over]) of the tier's budget (spec 3.3)."""
4401
+ caps = BUDGETS.get(state.get("tier"), BUDGETS[DEFAULT_TIER])
4402
+ used = (len(_row_objects(os.path.join(base, "literature", "INDEX.jsonl"))),
4403
+ len(_row_objects(os.path.join(base, "searches", "SEARCHES.jsonl"))),
4404
+ sum(1 for h in _experiment_names(base)
4405
+ for r in _row_objects(os.path.join(base, "experiments", h, "results.jsonl"))
4406
+ if str(r.get("source", "")).startswith("orx:")))
4407
+ names = ("sources", "searches", "runs")
4408
+ line = "budget %s: %s" % (slug, " ".join("%s %d/%d" % (n, u, c)
4409
+ for n, u, c in zip(names, used, caps)))
4410
+ return line, ["%s %d/%d" % (n, u, c) for n, u, c in zip(names, used, caps) if u > c]
4411
+
4412
+
4413
+ def hypothesis_problems(state):
4414
+ """A concluded line ends every hypothesis in a terminal status (spec 2.2)."""
4415
+ out = []
4416
+ for n, entry in enumerate(state.get("hypotheses") or [], 1):
4417
+ status = entry.get("status") if isinstance(entry, dict) else None
4418
+ if status not in TERMINAL_STATUSES:
4419
+ hid = entry.get("id") if isinstance(entry, dict) and entry.get("id") else n
4420
+ out.append("state.json hypothesis %s has no terminal status (%r): a concluded "
4421
+ "line ends each one %s" % (hid, status, ", ".join(TERMINAL_STATUSES)))
4422
+ return out
4423
+
4424
+
4425
+ def overlaps(repo, question, slug=None):
4426
+ """[(line, claim, status, similarity)] of the done lines' claims whose
4427
+ `similarity_matrix` value against `question` is >= 0.5, over the corpus of the
4428
+ question plus every done line's claim statements (spec 4.2)."""
4429
+ done = [(name, row) for name in slugs(repo) if name != slug and sealed(repo, name)
4430
+ for row in _row_objects(os.path.join(line_dir(repo, name), "claims.jsonl"))
4431
+ if str(row.get("statement", "")).strip()]
4432
+ if not done or not str(question or "").strip():
4433
+ return []
4434
+ _backend, matrix = similarity_matrix([question] + [str(r["statement"]) for _, r in done])
4435
+ return [(name, row.get("id"), row.get("status"), matrix[0][n])
4436
+ for n, (name, row) in enumerate(done, 1) if matrix[0][n] >= 0.5]
4437
+
4438
+
4439
+ def _post_hoc(repo, ddir):
4440
+ """The variant ids whose variants.jsonl row was first committed after the first
4441
+ comparison.jsonl cell (spec 4.5)."""
4442
+ paths = [os.path.join(ddir, f) for f in ("variants.jsonl", "comparison.jsonl")]
4443
+ top, _rel = _histories(repo, paths[0])[-1]
4444
+ order, err = _git_out(top, "rev-list", "--reverse", "HEAD")
4445
+ if err:
4446
+ return set()
4447
+ index = {sha: n for n, sha in enumerate(order.split())}
4448
+ firsts = []
4449
+ for path in paths:
4450
+ seen = {}
4451
+ for sha, text in _versions(top, _histories(repo, path)[-1][1]):
4452
+ for line in text.splitlines():
4453
+ if line.strip() and sha in index:
4454
+ seen.setdefault(line.strip(), index[sha])
4455
+ firsts.append(seen)
4456
+ if not firsts[1]:
4457
+ return set()
4458
+ first_cell = min(firsts[1].values())
4459
+ out = set()
4460
+ for line, n in firsts[0].items():
4461
+ if n > first_cell:
4462
+ try:
4463
+ row = json.loads(line)
4464
+ except ValueError:
4465
+ continue
4466
+ if isinstance(row, dict) and row.get("id"):
4467
+ out.add(str(row["id"]))
4468
+ return out
4469
+
4470
+
4471
+ def _check_review8(base, claims, review, errors, warnings, strict):
4472
+ """4.1: families compared normalised, and a review of evidence claims that reads
4473
+ a raw log or code."""
4474
+ if review is None:
4475
+ return
4476
+ family = [re.sub(r"\s+", "", str(review.get(k) or "")).casefold()
4477
+ for k in ("producer_family", "reviewer_family")]
4478
+ if family[0] and family[0] == family[1]:
4479
+ warnings.append("to_human/review.json: same-family review (%s): producer and "
4480
+ "reviewer share a model family, so they share its blind spots"
4481
+ % family[0])
4482
+ if any(c.get("kind") == "evidence" for c in claims):
4483
+ targets = [str(f.get("target", "")).strip() for f in review.get("findings") or []
4484
+ if isinstance(f, dict)]
4485
+ if not any(re.match(r"experiments/[^/]+/raw/", t) or t.endswith(CODE_SUFFIXES)
4486
+ for t in targets):
4487
+ _soft(errors, warnings, strict,
4488
+ "to_human/review.json: review quotes no raw log or code, so no finding "
4489
+ "reads the bytes an evidence claim rests on")
4490
+
4491
+
4492
+ def _core_rules8(repo, slug, base, state, errors, warnings, strict, git, steps, code=None):
4493
+ rows = _row_objects(os.path.join(base, "claims.jsonl"))
4494
+ claims = [c for c in rows if isinstance(c.get("id"), str) and c["id"]]
4495
+ superseded = _superseded(rows)
4496
+ exps = {h: experiment_record(base, h) for h in _experiment_names(base)}
4497
+ report_path = os.path.join(base, "to_human", "report.md")
4498
+ report = _note_text(report_path) if os.path.isfile(report_path) else None
4499
+ review_path = os.path.join(base, "to_human", "review.json")
4500
+ review, exc = _read_json(review_path) if os.path.isfile(review_path) else (None, None)
4501
+ review = review if isinstance(review, dict) and not exc else None
4502
+ lines = steps.setdefault("#report", [])
4503
+ for h in sorted(exps):
4504
+ _check_experiment8(repo, slug, base, state, exps[h], report, errors, warnings,
4505
+ strict, git, lines, code)
4506
+ rejected = _multiplicity(base, exps)
4507
+ every = _routing_steps(base, state, exps, review)
4508
+ cls, measured = state.get("question_class"), state.get("method") == "measured"
4509
+ for claim in claims:
4510
+ cid = claim["id"]
4511
+ cited = [exps[h] for h in _experiments_named(_cited(claim.get("proof")))
4512
+ if h in exps]
4513
+ if cid not in superseded:
4514
+ errors.extend(derived_problems(cid, claim, base))
4515
+ errors.extend(fired_problems(cid, claim, base))
4516
+ found = _experiment_steps(claim, cited, cls, measured, rejected) if cited else []
4517
+ if claim.get("kind") == "evidence" and _untraced(claim, cited):
4518
+ found.append("untraced-number")
4519
+ for name in found + every:
4520
+ _step(steps, cid, name)
4521
+ _overclaim(cid, claim, cited, warnings)
4522
+ failed = steps.setdefault("#stats", {})
4523
+ for h in sorted(exps):
4524
+ if exps[h]["stats_error"]:
4525
+ failed.setdefault("experiment %s" % h, exps[h]["stats_error"])
4526
+ _check_review8(base, claims, review, errors, warnings, strict)
4527
+ if state.get("phase") == "concluded":
4528
+ errors.extend(hypothesis_problems(state))
4529
+ line, over = budget(slug, base, state)
4530
+ lines.append(line)
4531
+ if over:
4532
+ warnings.append("over budget: %s (tier %s)" % (", ".join(over),
4533
+ state.get("tier") or DEFAULT_TIER))
4534
+ if git:
4535
+ for name, ddir in _decision_dirs(base):
4536
+ body = _note_text(os.path.join(ddir, "decision.md"))
4537
+ found = re.search(r"(?im)^\W*chosen\W*:\s*`?([\w.-]+)", body)
4538
+ if found and found.group(1) in _post_hoc(repo, ddir) \
4539
+ and "post-hoc" not in body and "post_hoc" not in body:
4540
+ errors.append("decisions/%s: decision.md chooses %s, a variant first "
4541
+ "committed after the first comparison cell, without saying "
4542
+ "post-hoc" % (name, found.group(1)))
4543
+ related = {(str(r.get("line")), str(r.get("claim")))
4544
+ for r in _row_objects(os.path.join(base, "related.jsonl"))
4545
+ if r.get("relation") in ("builds-on", "dismissed")}
4546
+ for line_name, cid, status, _sim in overlaps(repo, state.get("question"), slug):
4547
+ if status == "refuted" and (line_name, str(cid)) not in related:
4548
+ warnings.append("overlaps refuted claim %s/%s: record in related.jsonl "
4549
+ "whether this line builds on it or dismisses it, and why"
4550
+ % (line_name, cid))
4551
+
4552
+
4553
+ def _check_rules8(repo, slug, base, state, errors, warnings, strict, git, flags=None,
4554
+ code=None):
4555
+ """steps of a rules-8 line: the capture checks, the core ones, then the evidence
4556
+ module, which reads every step found and holds the certainty engine. A module
4557
+ that cannot be imported is an error on a rules-8 line, never a silent pass. A
4558
+ statistics computation's ValueError or ArithmeticError, kept in `#stats` by
4559
+ the module it ran in, is one refusal per experiment (review round two R2-1)."""
4560
+ steps = {"#flags": {"online": bool((flags or {}).get("online")),
4561
+ "orx": bool((flags or {}).get("orx"))}, "#stats": {}}
4562
+ import importlib
4563
+ for name in ("tezgah_research_capture", None, "tezgah_research_evidence"):
4564
+ if name is None:
4565
+ _core_rules8(repo, slug, base, state, errors, warnings, strict, git, steps, code)
4566
+ continue
4567
+ try:
4568
+ module = importlib.import_module(name)
4569
+ except ImportError as exc:
4570
+ errors.append("rules 8: %s cannot be imported (%s), so its checks did not run"
4571
+ % (name, exc))
4572
+ continue
4573
+ module.check(repo, slug, base, state, errors, warnings, strict, git, steps)
4574
+ errors.extend("%s: statistics could not be computed: %s" % (label, error)
4575
+ for label, error in sorted(steps["#stats"].items()))
4576
+ return steps
4577
+
4578
+
4579
+ def check_line(repo, slug, git=True, strict=False, code=None, flags=None, extra=None):
3555
4580
  """(errors, warnings) for one research line. `git=False` skips the checks that
3556
4581
  need git history, for callers that must stay cheap (the session context).
3557
4582
  `strict=True` turns every guarantee the checker cannot decide into an error.
3558
4583
  `code` is the checkout the check runs in when it is not `repo`: the line
3559
4584
  lives in the main checkout's workspace (plan 097) and its citations and
3560
4585
  proofs resolve against the checkout whose branch holds the code; unnamed, a
3561
- bare repository's worktree holding the cwd (`tp.work_tree`, plan 098)."""
4586
+ bare repository's worktree holding the cwd (`tp.work_tree`, plan 098).
4587
+ A rules-8 line (`_evidence`) also runs `_check_rules8` with `flags` (`online`,
4588
+ `orx`); a dict `extra` receives its `report` lines and per-claim `certainty`."""
3562
4589
  code = code or tp.work_tree(repo)
3563
4590
  base = line_dir(repo, slug)
3564
4591
  errors, warnings = [], []
@@ -3586,6 +4613,13 @@ def check_line(repo, slug, git=True, strict=False, code=None):
3586
4613
  if git:
3587
4614
  _check_tracking(repo, base, errors, warnings, strict)
3588
4615
  _check_review_integrity(base, errors, warnings, strict)
4616
+ state = _state(base)
4617
+ if _evidence(state):
4618
+ steps = _check_rules8(repo, slug, base, state, errors, warnings, strict, git, flags,
4619
+ code)
4620
+ if extra is not None:
4621
+ extra["report"] = list(steps.get("#report", []))
4622
+ extra["certainty"] = dict(steps.get("#certainty", {}))
3589
4623
  return errors, warnings
3590
4624
 
3591
4625
 
@@ -3692,8 +4726,10 @@ def _check_derived_phase(base, phase, direction, warnings):
3692
4726
  "stopped describing the line" % (direction, derived, want))
3693
4727
 
3694
4728
 
3695
- def check(repo, slug=None, git=True, strict=False, all_lines=True, code=None):
3696
- """{slug: {"errors": [...], "warnings": [...]}} for one line or every line.
4729
+ def check(repo, slug=None, git=True, strict=False, all_lines=True, code=None, flags=None):
4730
+ """{slug: {"errors": [...], "warnings": [...]}} for one line or every line; a
4731
+ rules-8 line adds "report" (the lines `check` prints, never counted) and
4732
+ "certainty" ({claim id: {level, start, steps}}).
3697
4733
 
3698
4734
  With `all_lines=False` and no slug, a line under `done/` is read by its order
3699
4735
  seal alone (`done_seal`), not re-checked, and appears only when that read
@@ -3706,8 +4742,10 @@ def check(repo, slug=None, git=True, strict=False, all_lines=True, code=None):
3706
4742
  if errors:
3707
4743
  out[name] = {"errors": errors, "warnings": []}
3708
4744
  continue
3709
- errors, warnings = check_line(repo, name, git=git, strict=strict, code=code)
3710
- out[name] = {"errors": errors, "warnings": warnings}
4745
+ extra = {}
4746
+ errors, warnings = check_line(repo, name, git=git, strict=strict, code=code,
4747
+ flags=flags, extra=extra)
4748
+ out[name] = dict({"errors": errors, "warnings": warnings}, **extra)
3711
4749
  return out
3712
4750
 
3713
4751
 
@@ -3789,7 +4827,9 @@ def _last_add(repo, path):
3789
4827
 
3790
4828
  def _seal_rows(repo, base):
3791
4829
  """{experiment: seal row}: the blob hashes of every experiment holding a
3792
- protocol.md, and the add commits of a run one, informational only."""
4830
+ protocol.md, and the add commits of a run one, informational only. A protocol
4831
+ naming an `evaluator:` also seals the evaluator's hash, the version `check`
4832
+ reads for the concluded line (`_sealed_evaluator`, plan 128 K8)."""
3793
4833
  out = {}
3794
4834
  for h, (proto, results) in _seal_files(base).items():
3795
4835
  row = {"protocol": _digest(proto),
@@ -3797,6 +4837,11 @@ def _seal_rows(repo, base):
3797
4837
  if results:
3798
4838
  row["commits"] = {"protocol": _last_add(repo, proto),
3799
4839
  "results": _last_add(repo, results)}
4840
+ words = _first(_fields(_note_text(proto)), "evaluator").split()
4841
+ evaluator = evaluator_file(repo, base, tp.work_tree(repo), words[0])[0] \
4842
+ if words else None
4843
+ if evaluator:
4844
+ row["evaluator"] = _digest(evaluator)
3800
4845
  out[h] = row
3801
4846
  return out
3802
4847
 
@@ -3982,7 +5027,7 @@ def import_line(repo, source, slug=None, allow_open=None, date=""):
3982
5027
  lines, here = _committed_lines(ws, rev), slugs(repo)
3983
5028
  if slug is not None and slug not in lines:
3984
5029
  return [], ("%s is not committed in %s, so it has no history to import; "
3985
- "commit it there (`tezgah-research commit`) - a plain copy is "
5030
+ "commit it there (`~/.config/tezgah/bin/tezgah-research commit`) - a plain copy is "
3986
5031
  "refused because it loses the order proof" % (slug, src))
3987
5032
  wanted = [slug] if slug is not None else sorted(s for s in lines if s not in here)
3988
5033
  clash = [s for s in wanted if s in here]
@@ -4220,8 +5265,20 @@ def conclude_line(repo, slug, date=""):
4220
5265
  "(state.json `success`) before concluding")
4221
5266
  if unjudged:
4222
5267
  problems.append("no verdict for %s: record it with "
4223
- "`tezgah-research verdict <slug> <id> met|not-met|unanswerable "
5268
+ "`~/.config/tezgah/bin/tezgah-research verdict <slug> <id> met|not-met|unanswerable "
4224
5269
  "--evidence <ref>`" % ", ".join(unjudged))
5270
+ if _evidence(state):
5271
+ # the rules `check` refuses a concluded rules-8 line for, asked before the
5272
+ # line is concluded: every hypothesis terminal, every rejected instrument
5273
+ # switched or narrowed (the evidence module's rule)
5274
+ problems.extend(hypothesis_problems(state))
5275
+ try:
5276
+ import tezgah_research_evidence as evidence
5277
+ except ImportError as exc:
5278
+ problems.append("rules 8: tezgah_research_evidence cannot be imported (%s)"
5279
+ % exc)
5280
+ else:
5281
+ problems.extend(evidence.conclude_problems(repo, slug, base, state))
4225
5282
  if problems:
4226
5283
  return problems, None
4227
5284
  state.update(phase="concluded", direction="conclude")
@@ -4678,7 +5735,51 @@ def urlsplit_host(source):
4678
5735
  return host[4:] if host.startswith("www.") else host
4679
5736
 
4680
5737
 
4681
- def source_run(repo, slug, hypothesis, run, orx, command="", scope="", fixture=""):
5738
+ # A run log above 16 MiB is refused rather than committed as a receipt unseen; the
5739
+ # refusal names `--ack`, which files it whole when the owner says so (K6 rater X6).
5740
+ RUN_LOG_BOUND = 16 * 1024 * 1024
5741
+ # What orx 0.2.20's `orx logs` prints instead of the log: the path, the size, a tail.
5742
+ ORX_LOG_SUMMARY = re.compile(r"\s*This is the path to the full log file: (.+?)[ \t]*\n"
5743
+ r"\s*It is ([\d,]+) bytes\.")
5744
+
5745
+
5746
+ def _bound_problem(verb, run, size, bound):
5747
+ return ("orx logs %s %s %d bytes, above the %d-byte bound a receipt is committed "
5748
+ "under - file it with --ack \"<the user's words>\" to keep it whole"
5749
+ % (run, verb, size, bound))
5750
+
5751
+
5752
+ def _full_log(repo, run, out, bound=RUN_LOG_BOUND):
5753
+ """(bytes, problem): the run log `orx logs` printed - its output, or, when that is
5754
+ orx's summary, the file the summary names, held to the size it printed (K6 rater
5755
+ Y2: the summary's 500-character tail is not the log). Either is held to `bound`
5756
+ (`RUN_LOG_BOUND`; None when the owner acknowledged the size, which reads it whole)."""
5757
+ found = ORX_LOG_SUMMARY.match(out)
5758
+ if not found:
5759
+ if out.lstrip().startswith("This is the path to the full log file:"):
5760
+ return None, ("orx logs %s printed the log's path without its size, so the "
5761
+ "file cannot be held to what orx saw" % run)
5762
+ data = out.encode("utf-8")
5763
+ if bound is not None and len(data) > bound:
5764
+ return None, _bound_problem("printed", run, len(data), bound)
5765
+ return data, None
5766
+ path, size = os.path.join(repo, found.group(1)), int(found.group(2).replace(",", ""))
5767
+ if bound is not None and size > bound:
5768
+ return None, _bound_problem("said", run, size, bound)
5769
+ try:
5770
+ with open(path, "rb") as fh:
5771
+ data = fh.read(-1 if bound is None else bound + 1)
5772
+ except OSError as exc:
5773
+ return None, "cannot read the log orx logs %s names: %s" % (run, exc)
5774
+ if len(data) != size:
5775
+ return None, ("orx logs %s said %d bytes and %s holds %d: the run is still "
5776
+ "writing it or it changed since, so it is not the log orx saw - "
5777
+ "file the run once its log is complete" % (run, size, path, len(data)))
5778
+ return data, None
5779
+
5780
+
5781
+ def source_run(repo, slug, hypothesis, run, orx, command="", scope="", fixture="",
5782
+ arm="", item="", values=(), instrument="", ack=""):
4682
5783
  """(path, problems): file one orx run as this experiment's receipt.
4683
5784
 
4684
5785
  `orx logs` is read here rather than by the session, so the log lands in the
@@ -4700,37 +5801,129 @@ def source_run(repo, slug, hypothesis, run, orx, command="", scope="", fixture="
4700
5801
  without it files the row and says which field is still missing rather than
4701
5802
  refusing a row a session may have a reason to file; a value that is not a
4702
5803
  string, or one that is empty, is the mistake the field exists to catch and is
4703
- refused."""
5804
+ refused.
5805
+
5806
+ `values` are (field, number as written) pairs: each lands in the row as a
5807
+ number, and one equal to no number of the stored log is refused - equal as a
5808
+ number, so `1.5` matches a logged `1.50` (review round two K-6). `instrument`,
5809
+ `arm` and `item` land as strings, so the instrument ledger (`gold`, `label`)
5810
+ reads the row (K-7). A run already filed keeps its log; the new one is
5811
+ `raw/<run>.<n>.log` (a capture is written once). A rules-8 line's row also
5812
+ names its `run`, the engine's
5813
+ `status` when the line names an orx project, and the log and row are committed
5814
+ as `capture: source <slug>` (plan 127, spec 1.4).
5815
+
5816
+ `ack` is the owner's words for a log above `RUN_LOG_BOUND`: the log is stored
5817
+ whole and the row records the words (K6 rater X6)."""
4704
5818
  if scope and scope not in SCOPES:
4705
5819
  return None, ["scope %r is not one of %s" % (scope, ", ".join(SCOPES))]
4706
5820
  if fixture and (not isinstance(fixture, str) or not fixture.strip()):
4707
5821
  return None, ["fixture %r does not name what was generated" % (fixture,)]
4708
- newer = newer_rules(_state(line_dir(repo, slug)))
5822
+ base = line_dir(repo, slug)
5823
+ state = _state(base)
5824
+ newer = newer_rules(state)
4709
5825
  if newer:
4710
5826
  return None, [newer]
4711
5827
  experiment = os.path.join(line_dir(repo, slug), "experiments", hypothesis)
4712
5828
  if not os.path.isfile(os.path.join(experiment, "protocol.md")):
4713
5829
  return None, ["experiments/%s holds no protocol.md: write and commit the "
4714
5830
  "plan before filing the run it predicts" % hypothesis]
4715
- log, err = _run([orx, "logs", run], cwd=repo)
5831
+ if _evidence(state) and (_orders_lock(state) or _exploratory(state)):
5832
+ # the capture is committed at once, after HEAD of the history carrying the
5833
+ # line, so an uncommitted lock would follow its results for good: the order
5834
+ # `_check_evaluation_order` reads - an exploratory line's lock is its arena,
5835
+ # ordered by `_check_arena_order` (plan 128 K12) - asked of that HEAD before
5836
+ # anything is written
5837
+ lock_path = os.path.join(base, "state.json")
5838
+ top = _histories(repo, lock_path)[-1][0]
5839
+ arena = _exploratory(state)
5840
+ firsts = _arena_lock(repo, base)[0] if arena else _evaluation_commit(repo, lock_path)
5841
+ if _answers(repo, firsts, {top: "HEAD"}).get(top) is not True:
5842
+ return None, ["%s is not committed: no committed state.json holding %s "
5843
+ "precedes this capture, so its results would come before the "
5844
+ "lock for good - commit the line first (`tezgah-research commit "
5845
+ "%s \"<message>\"`); nothing was written"
5846
+ % ((("the arena", "the arena") if arena else
5847
+ ("the evaluation lock", "the metric and the baseline"))
5848
+ + (slug,))]
5849
+ if _evidence(state):
5850
+ # the same reason as the lock: what the capture commits next would leave the
5851
+ # protocol refused for good - entered after its results, or frozen invalid
5852
+ # (left, refused; fixed, `changed after the run`) - K6 rater X2
5853
+ commit = 'commit the line first (`tezgah-research commit %s "<message>"`); ' \
5854
+ 'nothing was written' % slug
5855
+ record = experiment_record(base, hypothesis)
5856
+ if record["invalid"]:
5857
+ return None, ["experiments/%s/protocol.md carries an invalid record (%s): the "
5858
+ "capture commits its results at once, after which the protocol "
5859
+ "cannot be fixed - fix it, then %s"
5860
+ % (hypothesis, "; ".join(record["invalid"]), commit)]
5861
+ metric = metric_problem(state, record["fields"])
5862
+ if metric:
5863
+ return None, ["experiments/%s/%s: the capture commits its results at once, "
5864
+ "after which the protocol cannot be fixed - name the locked "
5865
+ "metric in its `metric:` line, then %s" % (hypothesis, metric,
5866
+ commit)]
5867
+ if not _git_out(repo, "rev-parse", "--is-inside-work-tree")[1] and not any(
5868
+ _oldest_adds(repo, os.path.join(experiment, "protocol.md"))[0].values()):
5869
+ return None, ["experiments/%s/protocol.md is not committed: the capture commits "
5870
+ "its results at once, so the protocol would enter the history "
5871
+ "after them for good - %s" % (hypothesis, commit)]
5872
+ out, err = _run([orx, "logs", run], cwd=repo)
4716
5873
  if err:
4717
5874
  return None, ["orx logs %s failed: %s" % (run, err)]
4718
- if not log.strip():
5875
+ data, problem = _full_log(repo, run, out, None if ack else RUN_LOG_BOUND)
5876
+ if problem:
5877
+ return None, [problem]
5878
+ if not data.strip():
4719
5879
  return None, ["orx logs %s returned nothing, so there is no receipt to "
4720
5880
  "file" % run]
5881
+ import tezgah_research_capture as cap # deferred: it imports this module
5882
+ logged = cap.log_numbers(data.decode("utf-8", "replace"))
5883
+ for field, written in values:
5884
+ if decimal.Decimal(written) not in logged:
5885
+ return None, ["%s=%s: value not in the run log (orx logs %s holds no number "
5886
+ "equal to %s)" % (field, written, run, written)]
4721
5887
  raw = os.path.join(experiment, "raw")
4722
5888
  os.makedirs(raw, exist_ok=True)
4723
- path = os.path.join(raw, run + ".log")
4724
- with open(path, "w", encoding="utf-8") as fh:
4725
- fh.write(log)
4726
- row = {"source": "orx:%s" % run, "log": "raw/%s.log" % run}
5889
+ name, n = run + ".log", 0
5890
+ while os.path.lexists(os.path.join(raw, name)):
5891
+ n += 1
5892
+ name = "%s.%d.log" % (run, n)
5893
+ path = os.path.join(raw, name)
5894
+ with open(path, "wb") as fh:
5895
+ fh.write(data)
5896
+ row = {"source": "orx:%s" % run, "log": "raw/%s" % name}
5897
+ if _evidence(state):
5898
+ row["run"] = run
5899
+ if instrument:
5900
+ row["instrument"] = instrument
5901
+ if arm:
5902
+ row["arm"] = arm
5903
+ if item:
5904
+ row["item"] = item
5905
+ for field, written in values:
5906
+ row[field] = int(written) if re.fullmatch(r"-?\d+", written) else float(written)
4727
5907
  if command:
4728
5908
  row["command"] = command
5909
+ if ack:
5910
+ row["ack"] = ack
4729
5911
  if scope:
4730
5912
  row["scope"] = scope
4731
5913
  if fixture:
4732
5914
  row["fixture"] = fixture
4733
- problems = _append_row(os.path.join(experiment, "results.jsonl"), row)
5915
+ if _evidence(state):
5916
+ runs, _err = cap.engine_runs(state)
5917
+ status = next((r.get("status") for r in runs or () if cap.run_id(r) == run), None)
5918
+ if isinstance(status, str) and status:
5919
+ row["status"] = status
5920
+ results = os.path.join(experiment, "results.jsonl")
5921
+ problems = _append_row(results, row)
5922
+ if not problems and _evidence(state):
5923
+ err = cap.commit_capture(repo, base, [os.path.relpath(path, base),
5924
+ os.path.relpath(results, base)],
5925
+ "source", slug)
5926
+ problems = [err] if err else []
4734
5927
  return path, problems
4735
5928
 
4736
5929
 
@@ -4899,7 +6092,7 @@ def verdict_problems(state, rows):
4899
6092
 
4900
6093
 
4901
6094
  def init(repo, slug, question="", created="", supersedes=None, ask="", tier=DEFAULT_TIER,
4902
- method="", scope_in=(), scope_out=(), arena=None):
6095
+ method="", scope_in=(), scope_out=(), arena=None, question_class=""):
4903
6096
  """Scaffold a research line. Returns the paths created (never overwrites).
4904
6097
 
4905
6098
  Refuses a slug `slugs()` cannot list - see `valid_slug` - so no caller can
@@ -4914,6 +6107,8 @@ def init(repo, slug, question="", created="", supersedes=None, ask="", tier=DEFA
4914
6107
  os.makedirs(path, exist_ok=True)
4915
6108
  state = dict(STATE_TEMPLATE)
4916
6109
  state["method"] = method
6110
+ if question_class:
6111
+ state["question_class"] = question_class
4917
6112
  state["scope"] = {"in": list(scope_in), "out": list(scope_out)}
4918
6113
  state["question"] = question
4919
6114
  state["created"] = created
@@ -6239,9 +7434,9 @@ def bradley_terry(ids, matches):
6239
7434
 
6240
7435
  def _next_action(slug, iid, stage, history, record, arena):
6241
7436
  """The one action an idea is owed next, as the command that does it."""
6242
- cmd = "tezgah-research stage %s %s" % (slug, iid)
7437
+ cmd = "~/.config/tezgah/bin/tezgah-research stage %s %s" % (slug, iid)
6243
7438
  if stage == "proposed":
6244
- return ("dedup (`tezgah-research dedup %s`), then `%s prior-art --evidence "
7439
+ return ("dedup (`~/.config/tezgah/bin/tezgah-research dedup %s`), then `%s prior-art --evidence "
6245
7440
  "'{\"verdict\": \"novel|incremental|known\", \"nearest\": [3 indexed "
6246
7441
  "notes], \"delta\": \"<one sentence>\"}'`" % (slug, cmd))
6247
7442
  if stage == "duplicate":
@@ -6254,7 +7449,7 @@ def _next_action(slug, iid, stage, history, record, arena):
6254
7449
  if stage == "prior-art":
6255
7450
  if record["judges"] >= 2 and record["orders"] >= 2:
6256
7451
  return "`%s screened --evidence tournament.jsonl`" % cmd
6257
- return ("judge it: `tezgah-research match %s` (2+ judges of a family that "
7452
+ return ("judge it: `~/.config/tezgah/bin/tezgah-research match %s` (2+ judges of a family that "
6258
7453
  "generated neither idea, both orders)" % slug)
6259
7454
  if stage == "screened":
6260
7455
  return ("pilot: commit experiments/<h>/protocol.md naming %s and its kill "
@@ -6318,7 +7513,7 @@ def board(repo, slug):
6318
7513
  live = [e for e in entries if e["stage"] not in ("confirmed", "killed")]
6319
7514
  live.sort(key=lambda e: (-STAGES.index(e["stage"]), -e["score"], _idea_number(e["id"])))
6320
7515
  nxt = ("%s - %s" % (live[0]["id"], live[0]["next"]) if live else
6321
- ("record ideas: `tezgah-research idea %s`" % slug) if not ideas else
7516
+ ("record ideas: `~/.config/tezgah/bin/tezgah-research idea %s`" % slug) if not ideas else
6322
7517
  "nothing left: every idea is confirmed or killed - write the report")
6323
7518
  evaluator = ("none (review only)" if not _text(arena.get("evaluator")) else
6324
7519
  "%s %s" % (arena["evaluator"], "changed" if evaluator_problem(repo, state)
@@ -6444,7 +7639,7 @@ def dedup_text(report, slug):
6444
7639
  rep = c["members"][0]
6445
7640
  out.append("cluster %s: %s" % (rep, ", ".join(
6446
7641
  "%s (%.2f)" % (m, c["similarity"][m]) for m in c["members"][1:])))
6447
- out.extend(" tezgah-research stage %s %s duplicate --evidence '%s'"
7642
+ out.extend(" ~/.config/tezgah/bin/tezgah-research stage %s %s duplicate --evidence '%s'"
6448
7643
  % (slug, m, json.dumps({"of": rep, "similarity": c["similarity"][m],
6449
7644
  "backend": report["backend"]}))
6450
7645
  for m in c["members"][1:])