@r1z4x/tezgah 1.9.2 → 1.13.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.
- package/CHANGELOG.md +415 -1
- package/MANIFEST +404 -144
- package/README.de.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.md +5 -4
- package/README.tr.md +1 -1
- package/bin/codegen +5 -2
- package/bin/tezgah-design +288 -1
- package/bin/tezgah-docs +1 -1
- package/bin/tezgah-doctor +39 -2
- package/bin/tezgah-import-ai-research +26 -12
- package/bin/tezgah-ocr-review +296 -0
- package/bin/tezgah-research +255 -44
- package/bin/tezgah-review-quote +185 -0
- package/bin/tezgah-setup +1365 -55
- package/bin/tezgah-skill +1 -1
- package/bin/tezgah-status +201 -3
- package/bin/tezgah-task +258 -20
- package/commands/adhd.md +2 -2
- package/commands/ponytail.md +4 -4
- package/docs/architecture.md +2 -2
- package/docs/citations-baseline.json +3 -3
- package/docs/clarity-baseline.json +11 -5
- package/docs/contract.md +91 -43
- package/docs/environment.md +5 -2
- package/docs/evidence.md +109 -17
- package/docs/feature-audit.md +2 -2
- package/docs/gate.md +60 -33
- package/docs/glossary.md +6 -3
- package/docs/hosts.md +313 -17
- package/docs/index.json +34 -8
- package/docs/judge.md +4 -2
- package/docs/layers.md +4 -4
- package/docs/models.md +37 -9
- package/docs/operations.md +141 -5
- package/docs/research.md +815 -8
- package/docs/skills.md +137 -13
- package/docs/status-line.md +121 -1
- package/docs/testing.md +3 -1
- package/hooks/hooks.json +2 -2
- package/hooks/lang/en.json +21 -0
- package/hooks/lang/tr.json +22 -1
- package/hooks/projects-stop.py +2 -1
- package/hooks/tezgah_agents.py +65 -25
- package/hooks/tezgah_apps.py +18 -0
- package/hooks/tezgah_attest.py +79 -10
- package/hooks/tezgah_components.py +4 -0
- package/hooks/tezgah_context.py +207 -55
- package/hooks/tezgah_extension.py +337 -0
- package/hooks/tezgah_gate.py +267 -79
- package/hooks/tezgah_integrity.py +864 -143
- package/hooks/tezgah_judge.py +6 -4
- package/hooks/tezgah_models.py +59 -19
- package/hooks/tezgah_orca.py +1 -1
- package/hooks/tezgah_paths.py +145 -8
- package/hooks/tezgah_policy.py +128 -57
- package/hooks/tezgah_rank.py +9 -6
- package/hooks/tezgah_replay.py +12 -6
- package/hooks/tezgah_research.py +1414 -76
- package/hooks/tezgah_research_capture.py +1544 -0
- package/hooks/tezgah_research_evidence.py +842 -0
- package/hooks/tezgah_research_stats.py +258 -0
- package/hooks/tezgah_skill_pick.py +408 -106
- package/hooks/tezgah_stack.py +236 -1
- package/hooks/tezgah_stopspec.py +8 -1
- package/hooks/tezgah_task.py +14 -5
- package/hooks/tezgah_taste_ledger.py +1 -1
- package/hosts/amp/hook.py +217 -0
- package/hosts/amp/tezgah-plugin.ts.in +386 -0
- package/hosts/antigravity/hook.py +255 -0
- package/hosts/codex/hook.py +2 -1
- package/hosts/commandcode/hook.py +239 -0
- package/hosts/copilot/hook.py +284 -0
- package/hosts/cursor/hook.py +31 -3
- package/hosts/droid/hook.py +259 -0
- package/hosts/dsh/hooks.json +1 -1
- package/hosts/gemini/hook.py +213 -0
- package/hosts/goose/hook.py +145 -0
- package/hosts/omp/hook.py +4 -1
- package/hosts/omp/tezgah-hook.ts.in +30 -7
- package/hosts/opencode/plugins/tezgah.js +309 -48
- package/hosts/opencode/tui/cli/tui.tsx +100 -0
- package/hosts/opencode/tui/tezgah-tui.tsx +2 -1
- package/hosts/qwen/hook.py +206 -0
- package/output-styles/tezgah.md +20 -15
- package/package.json +1 -1
- package/skills/ai-research/SKILL.md +21 -14
- package/skills/ai-research/SOURCE +7 -5
- package/skills/ai-research/index/1-frame.md +6 -6
- package/skills/ai-research/index/2-data.md +9 -9
- package/skills/ai-research/index/3-train.md +36 -36
- package/skills/ai-research/index/4-measure.md +13 -13
- package/skills/ai-research/index/5-run.md +11 -11
- package/skills/ai-research/index/6-write.md +23 -23
- package/skills/ai-research/library.json +104 -104
- package/skills/analyze-app/SKILL.md +17 -12
- package/skills/design-contract/SKILL.md +42 -7
- package/skills/design-contract/data-views.md +2 -2
- package/skills/design-library/EVALS.md +1 -1
- package/skills/design-library/INDEX.md +39 -39
- package/skills/design-library/SKILL.md +19 -13
- package/skills/design-library/SOURCE +45 -43
- package/skills/feature-audit/SKILL.md +20 -14
- package/skills/hallmark/LICENSE +21 -0
- package/skills/hallmark/SKILL.md +154 -0
- package/skills/hallmark/SOURCE +207 -0
- package/skills/hallmark/agents/openai.yaml +13 -0
- package/skills/hallmark/references/anatomy.md +71 -0
- package/skills/hallmark/references/brand-truth.md +40 -0
- package/skills/hallmark/references/comp.md +104 -0
- package/skills/hallmark/references/craft.md +116 -0
- package/skills/hallmark/references/dark-mode.md +49 -0
- package/skills/hallmark/references/data-viz.md +48 -0
- package/skills/hallmark/references/design-md.md +53 -0
- package/skills/hallmark/references/direction.md +52 -0
- package/skills/hallmark/references/export-formats.md +80 -0
- package/skills/hallmark/references/flow.md +131 -0
- package/skills/hallmark/references/fonts.md +58 -0
- package/skills/hallmark/references/gates.md +243 -0
- package/skills/hallmark/references/genres/atmospheric.md +24 -0
- package/skills/hallmark/references/genres/editorial.md +24 -0
- package/skills/hallmark/references/genres/modern-minimal.md +24 -0
- package/skills/hallmark/references/genres/playful.md +24 -0
- package/skills/hallmark/references/imagery.md +99 -0
- package/skills/hallmark/references/interaction-and-states.md +89 -0
- package/skills/hallmark/references/layout.md +104 -0
- package/skills/hallmark/references/macrostructures/01-bento-grid.md +21 -0
- package/skills/hallmark/references/macrostructures/02-long-document.md +35 -0
- package/skills/hallmark/references/macrostructures/03-marquee-hero.md +21 -0
- package/skills/hallmark/references/macrostructures/04-stat-led.md +21 -0
- package/skills/hallmark/references/macrostructures/05-workbench.md +21 -0
- package/skills/hallmark/references/macrostructures/06-conversational-faq.md +28 -0
- package/skills/hallmark/references/macrostructures/07-manifesto.md +28 -0
- package/skills/hallmark/references/macrostructures/08-photographic.md +21 -0
- package/skills/hallmark/references/macrostructures/09-quote-led.md +21 -0
- package/skills/hallmark/references/macrostructures/10-specimen.md +21 -0
- package/skills/hallmark/references/macrostructures/11-catalogue.md +21 -0
- package/skills/hallmark/references/macrostructures/12-letter.md +21 -0
- package/skills/hallmark/references/macrostructures/13-index-first.md +21 -0
- package/skills/hallmark/references/macrostructures/14-narrative-workflow.md +21 -0
- package/skills/hallmark/references/macrostructures/15-split-studio.md +21 -0
- package/skills/hallmark/references/macrostructures/16-feature-stack.md +23 -0
- package/skills/hallmark/references/macrostructures/17-type-specimen.md +19 -0
- package/skills/hallmark/references/macrostructures/18-portfolio-grid.md +21 -0
- package/skills/hallmark/references/macrostructures/19-map-diagram.md +21 -0
- package/skills/hallmark/references/macrostructures/20-ecosystem-index.md +19 -0
- package/skills/hallmark/references/macrostructures/21-component-playground.md +21 -0
- package/skills/hallmark/references/macrostructures.md +119 -0
- package/skills/hallmark/references/microinteractions.md +80 -0
- package/skills/hallmark/references/patterns.md +202 -0
- package/skills/hallmark/references/redesign.md +88 -0
- package/skills/hallmark/references/responsive.md +46 -0
- package/skills/hallmark/references/review-finish.md +19 -0
- package/skills/hallmark/references/review-hero.md +20 -0
- package/skills/hallmark/references/scroll-choreography.md +110 -0
- package/skills/hallmark/references/texture.md +90 -0
- package/skills/hallmark/references/themes/README.md +58 -0
- package/skills/hallmark/references/themes/almanac.jpg +0 -0
- package/skills/hallmark/references/themes/almanac.md +103 -0
- package/skills/hallmark/references/themes/arcade.jpg +0 -0
- package/skills/hallmark/references/themes/arcade.md +102 -0
- package/skills/hallmark/references/themes/atelier.jpg +0 -0
- package/skills/hallmark/references/themes/atelier.md +98 -0
- package/skills/hallmark/references/themes/bloom.jpg +0 -0
- package/skills/hallmark/references/themes/bloom.md +102 -0
- package/skills/hallmark/references/themes/blueprint.jpg +0 -0
- package/skills/hallmark/references/themes/blueprint.md +103 -0
- package/skills/hallmark/references/themes/broadside.jpg +0 -0
- package/skills/hallmark/references/themes/broadside.md +108 -0
- package/skills/hallmark/references/themes/brutal.jpg +0 -0
- package/skills/hallmark/references/themes/brutal.md +95 -0
- package/skills/hallmark/references/themes/carnival.jpg +0 -0
- package/skills/hallmark/references/themes/carnival.md +104 -0
- package/skills/hallmark/references/themes/cobalt.jpg +0 -0
- package/skills/hallmark/references/themes/cobalt.md +101 -0
- package/skills/hallmark/references/themes/console.jpg +0 -0
- package/skills/hallmark/references/themes/console.md +99 -0
- package/skills/hallmark/references/themes/coral.jpg +0 -0
- package/skills/hallmark/references/themes/coral.md +102 -0
- package/skills/hallmark/references/themes/darkroom.jpg +0 -0
- package/skills/hallmark/references/themes/darkroom.md +99 -0
- package/skills/hallmark/references/themes/dose.jpg +0 -0
- package/skills/hallmark/references/themes/dose.md +98 -0
- package/skills/hallmark/references/themes/dusk.jpg +0 -0
- package/skills/hallmark/references/themes/dusk.md +105 -0
- package/skills/hallmark/references/themes/editorial.jpg +0 -0
- package/skills/hallmark/references/themes/editorial.md +97 -0
- package/skills/hallmark/references/themes/formwork.jpg +0 -0
- package/skills/hallmark/references/themes/formwork.md +107 -0
- package/skills/hallmark/references/themes/furrow.jpg +0 -0
- package/skills/hallmark/references/themes/furrow.md +98 -0
- package/skills/hallmark/references/themes/garden.jpg +0 -0
- package/skills/hallmark/references/themes/garden.md +97 -0
- package/skills/hallmark/references/themes/grid.jpg +0 -0
- package/skills/hallmark/references/themes/grid.md +101 -0
- package/skills/hallmark/references/themes/halftone.jpg +0 -0
- package/skills/hallmark/references/themes/halftone.md +106 -0
- package/skills/hallmark/references/themes/hedgerow.jpg +0 -0
- package/skills/hallmark/references/themes/hedgerow.md +104 -0
- package/skills/hallmark/references/themes/hum.jpg +0 -0
- package/skills/hallmark/references/themes/hum.md +99 -0
- package/skills/hallmark/references/themes/index.json +2191 -0
- package/skills/hallmark/references/themes/ink.jpg +0 -0
- package/skills/hallmark/references/themes/ink.md +106 -0
- package/skills/hallmark/references/themes/kiln.jpg +0 -0
- package/skills/hallmark/references/themes/kiln.md +108 -0
- package/skills/hallmark/references/themes/loom.jpg +0 -0
- package/skills/hallmark/references/themes/loom.md +106 -0
- package/skills/hallmark/references/themes/lumen.jpg +0 -0
- package/skills/hallmark/references/themes/lumen.md +103 -0
- package/skills/hallmark/references/themes/manifesto.jpg +0 -0
- package/skills/hallmark/references/themes/manifesto.md +101 -0
- package/skills/hallmark/references/themes/midnight.jpg +0 -0
- package/skills/hallmark/references/themes/midnight.md +97 -0
- package/skills/hallmark/references/themes/newsprint.jpg +0 -0
- package/skills/hallmark/references/themes/newsprint.md +102 -0
- package/skills/hallmark/references/themes/overprint.jpg +0 -0
- package/skills/hallmark/references/themes/overprint.md +101 -0
- package/skills/hallmark/references/themes/pasteup.jpg +0 -0
- package/skills/hallmark/references/themes/pasteup.md +100 -0
- package/skills/hallmark/references/themes/piste.jpg +0 -0
- package/skills/hallmark/references/themes/piste.md +99 -0
- package/skills/hallmark/references/themes/ribbon.jpg +0 -0
- package/skills/hallmark/references/themes/ribbon.md +106 -0
- package/skills/hallmark/references/themes/rim.jpg +0 -0
- package/skills/hallmark/references/themes/rim.md +96 -0
- package/skills/hallmark/references/themes/riso.jpg +0 -0
- package/skills/hallmark/references/themes/riso.md +98 -0
- package/skills/hallmark/references/themes/solstice.jpg +0 -0
- package/skills/hallmark/references/themes/solstice.md +104 -0
- package/skills/hallmark/references/themes/specimen.jpg +0 -0
- package/skills/hallmark/references/themes/specimen.md +100 -0
- package/skills/hallmark/references/themes/spore.jpg +0 -0
- package/skills/hallmark/references/themes/spore.md +104 -0
- package/skills/hallmark/references/themes/sport.jpg +0 -0
- package/skills/hallmark/references/themes/sport.md +101 -0
- package/skills/hallmark/references/themes/squeeze.jpg +0 -0
- package/skills/hallmark/references/themes/squeeze.md +99 -0
- package/skills/hallmark/references/themes/sticker.jpg +0 -0
- package/skills/hallmark/references/themes/sticker.md +106 -0
- package/skills/hallmark/references/themes/studio.jpg +0 -0
- package/skills/hallmark/references/themes/studio.md +99 -0
- package/skills/hallmark/references/themes/terminal.jpg +0 -0
- package/skills/hallmark/references/themes/terminal.md +96 -0
- package/skills/hallmark/references/themes/terrazzo.jpg +0 -0
- package/skills/hallmark/references/themes/terrazzo.md +106 -0
- package/skills/hallmark/references/themes/ultraviolet.jpg +0 -0
- package/skills/hallmark/references/themes/ultraviolet.md +106 -0
- package/skills/hallmark/references/themes/vellum.jpg +0 -0
- package/skills/hallmark/references/themes/vellum.md +105 -0
- package/skills/hallmark/references/themes/wayfind.jpg +0 -0
- package/skills/hallmark/references/themes/wayfind.md +105 -0
- package/skills/hallmark/references/themes/werkstatt.jpg +0 -0
- package/skills/hallmark/references/themes/werkstatt.md +105 -0
- package/skills/hallmark/scripts/capture.mjs +125 -0
- package/skills/hallmark/scripts/fixtures/exemplars/atmospheric.html +324 -0
- package/skills/hallmark/scripts/fixtures/exemplars/editorial.html +363 -0
- package/skills/hallmark/scripts/fixtures/exemplars/modern-minimal.html +527 -0
- package/skills/hallmark/scripts/fixtures/exemplars/playful.html +353 -0
- package/skills/hallmark/scripts/fixtures/eyebrow-fail/index.html +118 -0
- package/skills/hallmark/scripts/fixtures/eyebrow-pass/index.html +137 -0
- package/skills/hallmark/scripts/fixtures/kept/after.html +27 -0
- package/skills/hallmark/scripts/fixtures/kept/before.html +26 -0
- package/skills/hallmark/scripts/fixtures/slopped.html +145 -0
- package/skills/hallmark/scripts/fixtures/tailwind-fail/Feature.vue +14 -0
- package/skills/hallmark/scripts/fixtures/tailwind-fail/Hero.tsx +33 -0
- package/skills/hallmark/scripts/fixtures/tailwind-fail/index.html +74 -0
- package/skills/hallmark/scripts/fixtures/tailwind-fail/style.css +6 -0
- package/skills/hallmark/scripts/fixtures/tailwind-pass/Hero.tsx +33 -0
- package/skills/hallmark/scripts/fixtures/tailwind-pass/index.html +57 -0
- package/skills/hallmark/scripts/fixtures/tailwind-pass/style.css +15 -0
- package/skills/hallmark/scripts/fixtures/waiver/index.html +54 -0
- package/skills/hallmark/scripts/imagegen.mjs +85 -0
- package/skills/hallmark/scripts/install-hook.mjs +70 -0
- package/skills/hallmark/scripts/install.mjs +141 -0
- package/skills/hallmark/scripts/lint-hook.mjs +49 -0
- package/skills/hallmark/scripts/photos.json +1 -0
- package/skills/hallmark/scripts/picture.mjs +71 -0
- package/skills/hallmark/scripts/refcheck.mjs +352 -0
- package/skills/hallmark/scripts/sloplint-render.mjs +1252 -0
- package/skills/hallmark/scripts/sloplint.mjs +4422 -0
- package/skills/hallmark/scripts/theme.mjs +650 -0
- package/skills/harness/SKILL.md +17 -8
- package/skills/i-have-adhd/SKILL.md +32 -12
- package/skills/no-ai-slop/SKILL.md +15 -1
- package/skills/plan-add/SKILL.md +38 -27
- package/skills/plan-status/SKILL.md +13 -7
- package/skills/plan-sync/SKILL.md +18 -12
- package/skills/pm-frameworks/SKILL.md +19 -10
- package/skills/pm-frameworks/SOURCE +7 -5
- package/skills/ponytail/SKILL.md +19 -14
- package/skills/product-analysis/SKILL.md +22 -18
- package/skills/product-flow/SKILL.md +14 -9
- package/skills/research/SKILL.md +97 -48
- package/skills/rl-env/SKILL.md +25 -13
- package/skills/rl-env/SOURCE +8 -6
- package/skills/tezgah-contract/SKILL.md +65 -47
- package/statusline.py +20 -9
- package/workflows/graph-review.js +14 -2
- package/bin/tezgah-skill.py +0 -59
- /package/skills/ai-research/0-autoresearch-skill/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/01-model-architecture/litgpt/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/01-model-architecture/mamba/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/01-model-architecture/nanogpt/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/01-model-architecture/rwkv/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/01-model-architecture/torchtitan/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/02-tokenization/huggingface-tokenizers/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/02-tokenization/sentencepiece/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/03-fine-tuning/axolotl/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/03-fine-tuning/llama-factory/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/03-fine-tuning/peft/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/03-fine-tuning/unsloth/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/04-mechanistic-interpretability/nnsight/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/04-mechanistic-interpretability/pyvene/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/04-mechanistic-interpretability/saelens/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/04-mechanistic-interpretability/transformer-lens/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/05-data-processing/nemo-curator/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/05-data-processing/ray-data/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/grpo-rl-training/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/miles/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/openrlhf/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/simpo/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/slime/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/torchforge/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/trl-fine-tuning/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/06-post-training/verl/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/07-safety-alignment/constitutional-ai/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/07-safety-alignment/llamaguard/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/07-safety-alignment/nemo-guardrails/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/07-safety-alignment/prompt-guard/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/08-distributed-training/accelerate/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/08-distributed-training/deepspeed/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/08-distributed-training/megatron-core/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/08-distributed-training/pytorch-fsdp2/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/08-distributed-training/pytorch-lightning/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/08-distributed-training/ray-train/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/09-infrastructure/lambda-labs/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/09-infrastructure/modal/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/09-infrastructure/skypilot/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/10-optimization/awq/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/10-optimization/bitsandbytes/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/10-optimization/flash-attention/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/10-optimization/gguf/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/10-optimization/gptq/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/10-optimization/hqq/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/10-optimization/ml-training-recipes/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/11-evaluation/bigcode-evaluation-harness/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/11-evaluation/lm-evaluation-harness/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/11-evaluation/nemo-evaluator/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/12-inference-serving/llama-cpp/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/12-inference-serving/sglang/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/12-inference-serving/tensorrt-llm/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/12-inference-serving/vllm/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/13-mlops/mlflow/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/13-mlops/swanlab/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/13-mlops/tensorboard/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/13-mlops/weights-and-biases/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/14-agents/a-evolve/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/14-agents/autogpt/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/14-agents/crewai/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/14-agents/langchain/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/14-agents/llamaindex/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/15-rag/chroma/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/15-rag/faiss/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/15-rag/pinecone/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/15-rag/qdrant/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/15-rag/sentence-transformers/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/16-prompt-engineering/dspy/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/16-prompt-engineering/guidance/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/16-prompt-engineering/instructor/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/16-prompt-engineering/outlines/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/17-observability/langsmith/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/17-observability/phoenix/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/audiocraft/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/blip-2/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/clip/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/cosmos-policy/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/llava/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/openpi/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/openvla-oft/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/segment-anything/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/stable-diffusion/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/18-multimodal/whisper/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/19-emerging-techniques/knowledge-distillation/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/19-emerging-techniques/long-context/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/19-emerging-techniques/model-merging/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/19-emerging-techniques/model-pruning/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/19-emerging-techniques/moe-training/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/19-emerging-techniques/speculative-decoding/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/20-ml-paper-writing/academic-plotting/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/20-ml-paper-writing/presenting-conference-talks/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/21-research-ideation/brainstorming-research-ideas/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/21-research-ideation/creative-thinking-for-research/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/22-agent-native-research-artifact/compiler/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/22-agent-native-research-artifact/research-manager/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/ai-research/22-agent-native-research-artifact/rigor-reviewer/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/colour-independence/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/flexible-typography/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/information-density/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/preference-audit/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/responsive-accessibility/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/responsive-review/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/simplified-views/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/specify/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/adaptive-interfaces/skills/user-preference-respect/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/adaptive-personalisation/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/assess-load/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/cognitive-load-assessment/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/contextual-help-design/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/error-prevention-recovery/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/focus-attention-design/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/memory-load-reduction/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/plain-language-design/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/review/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/simplify/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/cognitive-accessibility/skills/wayfinding-navigation/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/design-research/skills/behavioural-analytics/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/design-research/skills/qual-quant-triangulation/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/audit/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/design-flow/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/feedback-and-status/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/gesture-alternatives/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/keyboard-navigation/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/keyboard-review/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/motion-sensitivity/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/multi-modal-input/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/touch-target-design/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/inclusive-interaction/skills/voice-interaction/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/visual-critique/skills/critique-affordance/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/visual-critique/skills/critique-brand-consistency/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/visual-critique/skills/critique-color/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/visual-critique/skills/critique-composition/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/visual-critique/skills/critique-information-density/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/visual-critique/skills/critique-typography/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/design-library/visual-critique/skills/critique-visual-hierarchy/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/pm-frameworks/intended-vs-implemented/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/pm-frameworks/opportunity-solution-tree/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/rl-env/generate-nemo-gym-env/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/rl-env/generate-openenv-env/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/rl-env/generate-ors-env/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/rl-env/generate-verifiers-env/{SKILL.md → ENTRY.md} +0 -0
- /package/skills/rl-env/rl-env-from-description/{SKILL.md → ENTRY.md} +0 -0
package/hooks/tezgah_research.py
CHANGED
|
@@ -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
|
|
@@ -150,7 +151,14 @@ PROTOCOL_RULES = 4
|
|
|
150
151
|
SCOPE_RULES = 5
|
|
151
152
|
HOLDOUT_RULES = 6
|
|
152
153
|
EXPLORE_RULES = 7
|
|
153
|
-
|
|
154
|
+
# Rules 8 (plan 127): evidence captured by the tool and claims narrowed rather
|
|
155
|
+
# than lines refused.
|
|
156
|
+
EVIDENCE_RULES = 8
|
|
157
|
+
# Rules 9 (plan 136): a decision.md writes the line's own pick as `recommended:`;
|
|
158
|
+
# `chosen:` is the owner's, recorded by `choose` with an `owner:` line carrying
|
|
159
|
+
# the owner's words. `init` writes 9.
|
|
160
|
+
OWNER_CHOICE_RULES = 9
|
|
161
|
+
RULES = OWNER_CHOICE_RULES
|
|
154
162
|
# How a rules-5 line answers its question. `qualitative` is a source or
|
|
155
163
|
# reference review and owes no metric; `measured` runs experiments and locks
|
|
156
164
|
# one. The question picks it, not the tooling: a qualitative ask answered with a
|
|
@@ -357,6 +365,27 @@ def slugs(repo):
|
|
|
357
365
|
return sorted(seen)
|
|
358
366
|
|
|
359
367
|
|
|
368
|
+
def stray_dirs(repo):
|
|
369
|
+
"""[(path, error)] for the hidden dirs under research/, its open/ or its done/:
|
|
370
|
+
`slugs` skips them, so a line written into one (a `.staging` beside a refusal)
|
|
371
|
+
is invisible to every check. `status` and the no-slug `check` report each."""
|
|
372
|
+
out = []
|
|
373
|
+
for folder in layout_roots(repo):
|
|
374
|
+
try:
|
|
375
|
+
names = sorted(os.listdir(folder))
|
|
376
|
+
except OSError:
|
|
377
|
+
continue
|
|
378
|
+
for name in names:
|
|
379
|
+
path = os.path.join(folder, name)
|
|
380
|
+
if name.startswith(".") and os.path.isdir(path):
|
|
381
|
+
rel = os.path.relpath(path, repo)
|
|
382
|
+
out.append((rel, "%s is not a research line: a line lives under "
|
|
383
|
+
"research/open or research/done; import it with "
|
|
384
|
+
"`tezgah-research init` then move its files, or "
|
|
385
|
+
"delete it" % rel))
|
|
386
|
+
return out
|
|
387
|
+
|
|
388
|
+
|
|
360
389
|
def line_dir(repo, slug):
|
|
361
390
|
"""The one resolver: the first layout root that holds the line, else the
|
|
362
391
|
`open/` path a new line is created at."""
|
|
@@ -485,6 +514,20 @@ def _ws_rel(repo, path):
|
|
|
485
514
|
return rel if os.path.exists(os.path.join(ws, ".git")) else None
|
|
486
515
|
|
|
487
516
|
|
|
517
|
+
def folded(repo):
|
|
518
|
+
"""True when `<repo>/.tezgah` is a folded workspace: a directory the project
|
|
519
|
+
itself tracks and that holds no repository of its own, so the project's
|
|
520
|
+
history is the one that carries it (`ensure_workspace` answers None for it by
|
|
521
|
+
design). A symlinked `.tezgah` and a tree with no `.git` are not folded: the
|
|
522
|
+
first is refused as repository-provided, the second has no history at all.
|
|
523
|
+
`workspace_tracked` wants positive evidence, so an index that cannot be read
|
|
524
|
+
is not folded either."""
|
|
525
|
+
ws = tp.workspace(repo)
|
|
526
|
+
return (os.path.isdir(ws) and not os.path.islink(ws)
|
|
527
|
+
and not os.path.lexists(os.path.join(ws, ".git"))
|
|
528
|
+
and tp.workspace_tracked(repo))
|
|
529
|
+
|
|
530
|
+
|
|
488
531
|
def _ws_log(repo, path, *args):
|
|
489
532
|
"""(shas, error) of the private repository's `git log <args>` over `path`'s
|
|
490
533
|
own layout names (`_own_names`) along its HEAD, ([], None) when `path` is
|
|
@@ -952,6 +995,13 @@ def _check_state(base, errors, warnings, strict):
|
|
|
952
995
|
_check_derived_phase(base, state.get("phase"), state.get("direction"), warnings)
|
|
953
996
|
|
|
954
997
|
|
|
998
|
+
def _verdict_error(sid, verdict):
|
|
999
|
+
"""The error a verdict outside `VERDICTS` gets; `broken_open_lines` builds the
|
|
1000
|
+
same text for a `pending` one, so an in-flight line is told from a broken one."""
|
|
1001
|
+
return "success criterion %s: verdict %r is not one of %s" % (
|
|
1002
|
+
sid, verdict, ", ".join(VERDICTS))
|
|
1003
|
+
|
|
1004
|
+
|
|
955
1005
|
def _check_success(state, errors, warnings, strict):
|
|
956
1006
|
"""The ask contract, on a line that states an ask (rules 3 onward).
|
|
957
1007
|
|
|
@@ -983,8 +1033,7 @@ def _check_success(state, errors, warnings, strict):
|
|
|
983
1033
|
if not sid or not criterion:
|
|
984
1034
|
errors.append("a success criterion carries no id or no text")
|
|
985
1035
|
if verdict and verdict not in VERDICTS:
|
|
986
|
-
errors.append(
|
|
987
|
-
% (sid, verdict, ", ".join(VERDICTS)))
|
|
1036
|
+
errors.append(_verdict_error(sid, verdict))
|
|
988
1037
|
if verdict in VERDICTS and not evidence:
|
|
989
1038
|
errors.append("success criterion %s: a verdict with no evidence "
|
|
990
1039
|
"pointer is an opinion" % sid)
|
|
@@ -992,7 +1041,7 @@ def _check_success(state, errors, warnings, strict):
|
|
|
992
1041
|
unjudged = [sid for sid, _c, verdict, _e in rows if verdict not in VERDICTS]
|
|
993
1042
|
if unjudged:
|
|
994
1043
|
errors.append("concluded with no verdict for %s: judge each criterion "
|
|
995
|
-
"(
|
|
1044
|
+
"(`~/.config/tezgah/bin/tezgah-research verdict`) before concluding"
|
|
996
1045
|
% ", ".join(unjudged))
|
|
997
1046
|
if unanswered(state) and not (state.get("closed") or {}).get("ack") \
|
|
998
1047
|
and not _user_event(state):
|
|
@@ -1237,7 +1286,7 @@ def _check_findings(base, errors, warnings, strict):
|
|
|
1237
1286
|
missing = [s for s in FINDINGS_SECTIONS if ("## " + s) not in text]
|
|
1238
1287
|
if missing:
|
|
1239
1288
|
errors.append("findings.md does not answer: %s" % ", ".join(missing))
|
|
1240
|
-
_check_patterns(text, errors, warnings, strict)
|
|
1289
|
+
_check_patterns(text, errors, warnings, strict, _evidence(_state(base)))
|
|
1241
1290
|
|
|
1242
1291
|
|
|
1243
1292
|
PATTERNS_BULLET = re.compile(r"^\s*(?:[-*+]|\d+[.)])\s+(.*)$")
|
|
@@ -1256,7 +1305,7 @@ def _section(text, name):
|
|
|
1256
1305
|
return out
|
|
1257
1306
|
|
|
1258
1307
|
|
|
1259
|
-
def _check_patterns(text, errors, warnings, strict):
|
|
1308
|
+
def _check_patterns(text, errors, warnings, strict, wrapped):
|
|
1260
1309
|
"""Every bullet under `## Patterns` names the source it came from.
|
|
1261
1310
|
|
|
1262
1311
|
A pattern is the one finding a reader is meant to carry into other work, so
|
|
@@ -1265,9 +1314,17 @@ def _check_patterns(text, errors, warnings, strict):
|
|
|
1265
1314
|
none is a guess wearing the clothes of a finding, which is exactly the
|
|
1266
1315
|
failure the review exists to catch, and it is reported once per line with the
|
|
1267
1316
|
count so a line with seventeen of them says so in one row rather than
|
|
1268
|
-
seventeen.
|
|
1269
|
-
|
|
1270
|
-
|
|
1317
|
+
seventeen. With `wrapped` (a rules-8 line, `_evidence`) a bullet is its first
|
|
1318
|
+
line plus every following indented, non-blank line before the next bullet
|
|
1319
|
+
(spec 4.7): a source wrapped onto a continuation line is still the bullet's
|
|
1320
|
+
source. An older line reads each bullet's first line, as it was opened under."""
|
|
1321
|
+
bullets = []
|
|
1322
|
+
for line in _section(text, "Patterns"):
|
|
1323
|
+
found = PATTERNS_BULLET.match(line)
|
|
1324
|
+
if found:
|
|
1325
|
+
bullets.append(found.group(1).strip())
|
|
1326
|
+
elif wrapped and bullets and line[:1].isspace() and line.strip():
|
|
1327
|
+
bullets[-1] += " " + line.strip()
|
|
1271
1328
|
unsourced = [b for b in bullets if not PATTERNS_SOURCE.search(b)]
|
|
1272
1329
|
if unsourced:
|
|
1273
1330
|
_soft(errors, warnings, strict,
|
|
@@ -1707,14 +1764,17 @@ def _check_claims(base, errors, warnings, roots=(), strict=False):
|
|
|
1707
1764
|
# held to their proof here. Warning on the historical statement too would
|
|
1708
1765
|
# make a correction that rewords a figure impossible to land (it already
|
|
1709
1766
|
# cannot edit the row), for a sentence no reader is meant to keep.
|
|
1710
|
-
|
|
1767
|
+
# rules 8 replaces this rule by `untraced-number`, a step that never refuses
|
|
1768
|
+
if cid not in superseded and not _evidence(line_state):
|
|
1711
1769
|
_check_claim_numbers(cid, claim, base, repo, errors, warnings, strict)
|
|
1712
1770
|
_check_claim_raters(cid, claim, base, errors, warnings, strict)
|
|
1713
1771
|
if sections and cid not in superseded:
|
|
1714
1772
|
problem = _token_claim_problem(cid, claim, base)
|
|
1715
1773
|
if problem:
|
|
1716
1774
|
errors.append(problem)
|
|
1717
|
-
_check_supersedes(parsed, errors, warnings, strict
|
|
1775
|
+
_check_supersedes(parsed, errors, warnings, strict,
|
|
1776
|
+
_note_text(os.path.join(base, "to_human", "report.md"))
|
|
1777
|
+
if _evidence(line_state) else None)
|
|
1718
1778
|
if not count:
|
|
1719
1779
|
warnings.append("no claims recorded yet")
|
|
1720
1780
|
return count
|
|
@@ -1738,7 +1798,7 @@ def _token_claim_problem(cid, claim, base):
|
|
|
1738
1798
|
return None
|
|
1739
1799
|
|
|
1740
1800
|
|
|
1741
|
-
def _check_supersedes(parsed, errors, warnings, strict):
|
|
1801
|
+
def _check_supersedes(parsed, errors, warnings, strict, report=None):
|
|
1742
1802
|
"""A claim that supersedes another names one this line holds, and says so.
|
|
1743
1803
|
|
|
1744
1804
|
The record is append-only: a claim whose number went stale is corrected by a
|
|
@@ -1760,6 +1820,10 @@ def _check_supersedes(parsed, errors, warnings, strict):
|
|
|
1760
1820
|
errors.append("claim %s supersedes %s, and no claim in this line "
|
|
1761
1821
|
"carries that id" % (cid, ref))
|
|
1762
1822
|
continue
|
|
1823
|
+
# rules 8 (spec 4.6): a report line naming both ids tells the reader
|
|
1824
|
+
if report is not None and any(_mentions(cid, line) and _mentions(ref, line)
|
|
1825
|
+
for line in report.splitlines()):
|
|
1826
|
+
continue
|
|
1763
1827
|
_soft(errors, warnings, strict,
|
|
1764
1828
|
"claim %s supersedes %s: a reader who opens %s alone reads the "
|
|
1765
1829
|
"statement this line has since replaced" % (cid, ref, ref))
|
|
@@ -1787,6 +1851,11 @@ def _supersedes(claim):
|
|
|
1787
1851
|
return ids, None
|
|
1788
1852
|
|
|
1789
1853
|
|
|
1854
|
+
def _superseded(rows):
|
|
1855
|
+
"""The claim ids some row supersedes (`_supersedes`): the historical rows."""
|
|
1856
|
+
return {ref for row in rows for ref in _supersedes(row)[0]}
|
|
1857
|
+
|
|
1858
|
+
|
|
1790
1859
|
def _claim_ids(base, handle=None):
|
|
1791
1860
|
"""The ids `claims.jsonl` already carries, for the rule that a claim may only
|
|
1792
1861
|
supersede one this line holds. A row that does not parse is skipped here: the
|
|
@@ -2027,6 +2096,12 @@ def claim_problems(claim, base, repo, held=None):
|
|
|
2027
2096
|
if scopes and all(s == "fixture" for s in scopes):
|
|
2028
2097
|
problems.append("claim %s declares scope real and every row it rests on "
|
|
2029
2098
|
"records fixture" % cid)
|
|
2099
|
+
# rules 8: the claim refusals `_core_rules8` applies, from the same functions, and
|
|
2100
|
+
# like it never to a claim a row supersedes (the correction is that row)
|
|
2101
|
+
if _evidence(_state(base)) and cid not in _superseded(
|
|
2102
|
+
_row_objects(os.path.join(base, "claims.jsonl"))):
|
|
2103
|
+
problems.extend(derived_problems(cid, claim, base))
|
|
2104
|
+
problems.extend(fired_problems(cid, claim, base))
|
|
2030
2105
|
return problems
|
|
2031
2106
|
|
|
2032
2107
|
|
|
@@ -2329,10 +2404,17 @@ def _check_tracking(repo, base, errors, warnings, strict):
|
|
|
2329
2404
|
if not pattern:
|
|
2330
2405
|
continue
|
|
2331
2406
|
rel = _ws_rel(repo, path)
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2407
|
+
if rel is not None:
|
|
2408
|
+
fix = "`git -C .tezgah add -f %s`" % rel
|
|
2409
|
+
elif folded(repo):
|
|
2410
|
+
# a forced add of a `.tezgah/` path is the control rule's to refuse
|
|
2411
|
+
fix = ("`.tezgah` is tracked by the project - drop that ignore line, "
|
|
2412
|
+
"then `~/.config/tezgah/bin/tezgah-research commit %s \"<message>\"` commits the "
|
|
2413
|
+
"line in the project's history" % os.path.basename(base))
|
|
2414
|
+
else:
|
|
2415
|
+
fix = ("`.tezgah` has no git repository of its own - `~/.config/tezgah/bin/tezgah-research "
|
|
2416
|
+
"commit %s \"<message>\"` creates it and commits the line there"
|
|
2417
|
+
% os.path.basename(base))
|
|
2336
2418
|
_soft(errors, warnings, strict,
|
|
2337
2419
|
"experiment %s: %s is ignored by %s, so no commit can show the "
|
|
2338
2420
|
"protocol predates the results; %s"
|
|
@@ -2532,7 +2614,7 @@ def _check_literature(base, errors, warnings, strict):
|
|
|
2532
2614
|
_soft(errors, warnings, note in legacy or newer,
|
|
2533
2615
|
"literature/%s is not in INDEX.jsonl: a note the index does not "
|
|
2534
2616
|
"name is a source no reader can weigh" % note)
|
|
2535
|
-
_index_fields(rows, errors, warnings, strict)
|
|
2617
|
+
_index_fields(rows, errors, warnings, strict, _evidence(_state(base)))
|
|
2536
2618
|
|
|
2537
2619
|
|
|
2538
2620
|
def _read_index(index, literature, errors, warnings, strict):
|
|
@@ -2572,9 +2654,10 @@ def _read_index(index, literature, errors, warnings, strict):
|
|
|
2572
2654
|
return rows, named
|
|
2573
2655
|
|
|
2574
2656
|
|
|
2575
|
-
def _index_fields(rows, errors, warnings, strict):
|
|
2657
|
+
def _index_fields(rows, errors, warnings, strict, evidence=False):
|
|
2576
2658
|
"""The index entries the rules read: a grey source's quality, a verification
|
|
2577
|
-
against two records, and the three fields a
|
|
2659
|
+
against two records (rules 8 reads its receipts instead), and the three fields a
|
|
2660
|
+
note has to carry."""
|
|
2578
2661
|
total = len(rows)
|
|
2579
2662
|
if not total:
|
|
2580
2663
|
return
|
|
@@ -2585,8 +2668,8 @@ def _index_fields(rows, errors, warnings, strict):
|
|
|
2585
2668
|
"literature/INDEX.jsonl: %d of %d rows are a grey source with no "
|
|
2586
2669
|
"quality note, so nothing says how good the practice evidence is"
|
|
2587
2670
|
% (len(grey), total))
|
|
2588
|
-
verified = [
|
|
2589
|
-
|
|
2671
|
+
verified = [] if evidence else [
|
|
2672
|
+
r for r in rows if not isinstance(r.get("verified"), list) or len(r["verified"]) < 2]
|
|
2590
2673
|
if verified:
|
|
2591
2674
|
_soft(errors, warnings, strict,
|
|
2592
2675
|
"literature/INDEX.jsonl: %d of %d rows are verified by fewer than "
|
|
@@ -2761,6 +2844,11 @@ def _rules(state):
|
|
|
2761
2844
|
return value if isinstance(value, int) and not isinstance(value, bool) else 1
|
|
2762
2845
|
|
|
2763
2846
|
|
|
2847
|
+
def _evidence(state):
|
|
2848
|
+
"""True for a line opened under the evidence rules (`EVIDENCE_RULES`)."""
|
|
2849
|
+
return _rules(state) >= EVIDENCE_RULES
|
|
2850
|
+
|
|
2851
|
+
|
|
2764
2852
|
def _state(base):
|
|
2765
2853
|
"""state.json as an object, or {} when it is missing or unreadable."""
|
|
2766
2854
|
state, exc = _read_json(os.path.join(base, "state.json"))
|
|
@@ -2770,10 +2858,10 @@ def _state(base):
|
|
|
2770
2858
|
def newer_rules(state):
|
|
2771
2859
|
"""The refusal for a line a later tezgah wrote, or None.
|
|
2772
2860
|
|
|
2773
|
-
A `rules` above `RULES` names rules this code does not implement.
|
|
2774
|
-
line by the rules it does know would pass what the later set
|
|
2775
|
-
writer would append rows judged by the older set. So the line
|
|
2776
|
-
name, before any rule reads it."""
|
|
2861
|
+
A `rules` above `RULES` names rules this code does not implement.
|
|
2862
|
+
Reading the line by the rules it does know would pass what the later set
|
|
2863
|
+
refuses, and a writer would append rows judged by the older set. So the line
|
|
2864
|
+
is refused by name, before any rule reads it."""
|
|
2777
2865
|
found = _rules(state)
|
|
2778
2866
|
if found <= RULES:
|
|
2779
2867
|
return None
|
|
@@ -2791,20 +2879,22 @@ def _concluding(base, state):
|
|
|
2791
2879
|
or os.path.isfile(os.path.join(base, "to_human", "report.md")))
|
|
2792
2880
|
|
|
2793
2881
|
|
|
2794
|
-
def file_versions(repo, path):
|
|
2882
|
+
def file_versions(repo, path, every_ref=True):
|
|
2795
2883
|
"""[(sha, text)] of every committed version of `path`, oldest first; [] when git
|
|
2796
2884
|
cannot say. The append-only rule compares versions, which the added/last-touched
|
|
2797
2885
|
pair cannot. The project's history is read first and the private `.tezgah`
|
|
2798
2886
|
repository's after it, like `added_commits`: a line committed in the project
|
|
2799
2887
|
before the move has its older versions there.
|
|
2800
2888
|
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2889
|
+
By default this reads every ref of the private repository (`--all`), because
|
|
2890
|
+
the results rule looks for a rewrite: a row amended away while a tag keeps the
|
|
2891
|
+
old commit is a result rewritten after the run, which HEAD alone no longer
|
|
2892
|
+
shows (an internal research decision); the append-only capture ledgers read it
|
|
2893
|
+
the same way. `every_ref=False` reads HEAD's lineage only, for the order rule of
|
|
2894
|
+
the captures: a backdated side branch read through `--all` supplied the oldest
|
|
2895
|
+
version and satisfied the stopping-rule order."""
|
|
2806
2896
|
return [v for top, rel in _histories(repo, path)
|
|
2807
|
-
for v in _versions(top, rel, ("--all",) if top != repo else ())]
|
|
2897
|
+
for v in _versions(top, rel, ("--all",) if every_ref and top != repo else ())]
|
|
2808
2898
|
|
|
2809
2899
|
|
|
2810
2900
|
def _versions(top, rel, extra=()):
|
|
@@ -2890,19 +2980,22 @@ def _evaluation_commit(repo, path):
|
|
|
2890
2980
|
return {top: _first_lock(top, rel) for top, rel in _histories(repo, path)}
|
|
2891
2981
|
|
|
2892
2982
|
|
|
2983
|
+
def _orders_lock(state):
|
|
2984
|
+
"""True when a line's results are ordered against its evaluation lock: the
|
|
2985
|
+
arena is an exploratory line's lock, and `_check_arena_order` orders every
|
|
2986
|
+
results.jsonl against it instead; a state with no evaluation has none."""
|
|
2987
|
+
return not _exploratory(state) and isinstance(state.get("evaluation"), dict)
|
|
2988
|
+
|
|
2989
|
+
|
|
2893
2990
|
def _check_evaluation_order(repo, base, state, errors, warnings, strict):
|
|
2894
2991
|
"""The evaluation is locked before the first result, and `locked_at` names a
|
|
2895
2992
|
commit the history holds. `_check_evaluation` reads that the fields are filled;
|
|
2896
2993
|
this reads when: a metric and a baseline committed after the results are the
|
|
2897
2994
|
criterion the results were measured against after the fact."""
|
|
2898
|
-
if
|
|
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
|
|
2995
|
+
if not _orders_lock(state):
|
|
2901
2996
|
return
|
|
2902
2997
|
hard = strict or _rules(state) >= STANDARDS_RULES
|
|
2903
|
-
evaluation = state
|
|
2904
|
-
if not isinstance(evaluation, dict):
|
|
2905
|
-
return
|
|
2998
|
+
evaluation = state["evaluation"]
|
|
2906
2999
|
for token in SHA_TOKEN.findall(str(evaluation.get("locked_at", ""))):
|
|
2907
3000
|
if is_ancestor(repo, token, "HEAD") is not True:
|
|
2908
3001
|
_soft(errors, warnings, hard,
|
|
@@ -3181,6 +3274,34 @@ def _mentions(token, text):
|
|
|
3181
3274
|
return re.search(r"(?<![\w.-])%s(?![\w.-])" % re.escape(token), text) is not None
|
|
3182
3275
|
|
|
3183
3276
|
|
|
3277
|
+
# A decision.md's pick: `chosen:` is the owner's, `recommended:` the line's own
|
|
3278
|
+
# (rules 9); every reader takes `chosen:` when present, else `recommended:`.
|
|
3279
|
+
PICK = r"(?im)^\W*%s\W*:\s*`?([\w.-]+)"
|
|
3280
|
+
# The `owner:` line `choose` writes: a date and the owner's words, quoted.
|
|
3281
|
+
OWNER_WORDS = re.compile(r'(?im)^\W*owner\W*:[^\n]*"[^"\n]*\S[^"\n]*"')
|
|
3282
|
+
|
|
3283
|
+
|
|
3284
|
+
def decision_pick(body):
|
|
3285
|
+
"""(variant, "chosen" or "recommended") for a decision.md body, else (None, None)."""
|
|
3286
|
+
for field in ("chosen", "recommended"):
|
|
3287
|
+
found = re.search(PICK % field, body)
|
|
3288
|
+
if found:
|
|
3289
|
+
return found.group(1), field
|
|
3290
|
+
return None, None
|
|
3291
|
+
|
|
3292
|
+
|
|
3293
|
+
def line_pick(body):
|
|
3294
|
+
"""The line's own pick, which the line's rules judge (rejected variants,
|
|
3295
|
+
dominance, post-hoc): `recommended:`, else a `chosen:` with no owner's words.
|
|
3296
|
+
An owner's `chosen:` is the owner's call, so a dominated or post-hoc owner
|
|
3297
|
+
choice is never the line's error."""
|
|
3298
|
+
found = re.search(PICK % "recommended", body)
|
|
3299
|
+
if found:
|
|
3300
|
+
return found.group(1)
|
|
3301
|
+
found = re.search(PICK % "chosen", body)
|
|
3302
|
+
return found.group(1) if found and not OWNER_WORDS.search(body) else None
|
|
3303
|
+
|
|
3304
|
+
|
|
3184
3305
|
def _check_decision(repo, base, name, ddir, need, errors, warnings, strict, git,
|
|
3185
3306
|
concluding):
|
|
3186
3307
|
label = "decisions/%s" % name
|
|
@@ -3264,13 +3385,19 @@ def _check_decision(repo, base, name, ddir, need, errors, warnings, strict, git,
|
|
|
3264
3385
|
"says what ruled it out" % label)
|
|
3265
3386
|
return
|
|
3266
3387
|
body = _note_text(os.path.join(ddir, "decision.md"))
|
|
3267
|
-
|
|
3268
|
-
chosen = found.group(1) if found else None
|
|
3388
|
+
chosen, field = decision_pick(body)
|
|
3269
3389
|
if chosen not in produced:
|
|
3270
3390
|
errors.append("%s: decision.md chosen %r is not a produced variant"
|
|
3271
3391
|
% (label, chosen))
|
|
3392
|
+
if field == "chosen" and _rules(_state(base)) >= OWNER_CHOICE_RULES \
|
|
3393
|
+
and not OWNER_WORDS.search(body):
|
|
3394
|
+
errors.append("%s: decision.md chosen %s names no owner choice: the line "
|
|
3395
|
+
"writes recommended:, and the owner's pick is recorded with "
|
|
3396
|
+
"tezgah-research choose" % (label, chosen))
|
|
3397
|
+
pick = line_pick(body)
|
|
3398
|
+
owned = chosen if field == "chosen" and OWNER_WORDS.search(body) else None
|
|
3272
3399
|
for vid in produced:
|
|
3273
|
-
if vid
|
|
3400
|
+
if vid in (pick, owned):
|
|
3274
3401
|
continue
|
|
3275
3402
|
lines = [line for line in body.splitlines() if _mentions(vid, line)]
|
|
3276
3403
|
if not any(_mentions(cid, line) for line in lines for cid in criteria):
|
|
@@ -3279,12 +3406,12 @@ def _check_decision(repo, base, name, ddir, need, errors, warnings, strict, git,
|
|
|
3279
3406
|
if not re.search(r"(?im)^\W*flip\W*:", body):
|
|
3280
3407
|
errors.append("%s: decision.md names no `flip:` condition - the result that "
|
|
3281
3408
|
"would reverse the choice" % label)
|
|
3282
|
-
if
|
|
3283
|
-
better = dominated_by(
|
|
3409
|
+
if pick in produced:
|
|
3410
|
+
better = dominated_by(pick, produced, criteria, cells)
|
|
3284
3411
|
if better:
|
|
3285
3412
|
errors.append("%s: the chosen variant %s is dominated by %s, which is at "
|
|
3286
3413
|
"least as good on every criterion and better on one"
|
|
3287
|
-
% (label,
|
|
3414
|
+
% (label, pick, better))
|
|
3288
3415
|
|
|
3289
3416
|
|
|
3290
3417
|
def _deliverable_path(state):
|
|
@@ -3533,7 +3660,8 @@ def _check_review_integrity(base, errors, warnings, strict):
|
|
|
3533
3660
|
"to_human/review.json names no %s: a review says who produced the line "
|
|
3534
3661
|
"and who judged it, and those are two readers"
|
|
3535
3662
|
% ("reviewer" if not reviewer else "producer"))
|
|
3536
|
-
elif reviewer == producer
|
|
3663
|
+
elif reviewer == producer or (_evidence(state) and re.sub(r"\s+", "", reviewer).casefold()
|
|
3664
|
+
== re.sub(r"\s+", "", producer).casefold()):
|
|
3537
3665
|
errors.append("to_human/review.json reviewer %r is the producer: a line graded "
|
|
3538
3666
|
"by the session that wrote it is not reviewed" % reviewer)
|
|
3539
3667
|
if review.get("findings") == []:
|
|
@@ -3551,14 +3679,977 @@ def _check_review_integrity(base, errors, warnings, strict):
|
|
|
3551
3679
|
% (" and ".join(high), len(order), order[0][:80]))
|
|
3552
3680
|
|
|
3553
3681
|
|
|
3554
|
-
|
|
3682
|
+
# --- rules 8 (plan 127): steps, routing and the execution trail ----------------
|
|
3683
|
+
# A line opened under `EVIDENCE_RULES` is read for the steps that narrow each
|
|
3684
|
+
# claim's certainty (spec section 2) beside its refusals. `_check_rules8` threads
|
|
3685
|
+
# one `steps` dict - claim id -> step names in the order found - through the
|
|
3686
|
+
# capture module, the checks below and the evidence module, which turns the steps
|
|
3687
|
+
# into levels. Keys starting with `#` are the channel, never claim ids: `#flags`
|
|
3688
|
+
# (what `check` was asked), `#report` (the report lines `check` prints and never
|
|
3689
|
+
# counts), `#certainty` (each claim's level, for `check --json`) and `#stats`
|
|
3690
|
+
# ("experiment <h>" -> the statistics error `_check_rules8` refuses).
|
|
3691
|
+
|
|
3692
|
+
QUESTION_CLASSES = tuple("R%d" % n for n in range(13))
|
|
3693
|
+
CLASS_METHODS = dict(
|
|
3694
|
+
[(c, ("qualitative",)) for c in ("R1", "R2", "R7", "R8", "R9", "R10")]
|
|
3695
|
+
+ [(c, ("qualitative", "measured")) for c in ("R3", "R12")]
|
|
3696
|
+
+ [(c, ("measured",)) for c in ("R4", "R5", "R6")] + [("R11", ("exploratory",))])
|
|
3697
|
+
SEARCH_CLASSES = ("R1", "R2", "R7", "R8", "R9", "R10")
|
|
3698
|
+
# sources / searches / runs per tier: starting values, to re-measure (spec 3.3)
|
|
3699
|
+
BUDGETS = {"quick": (10, 10, 10), "study": (30, 40, 40), "program": (100, 150, 200)}
|
|
3700
|
+
LITERATURE_STEPS = ("header-mismatch", "unindexed-source", "online-drift",
|
|
3701
|
+
"abstract-only", "source-not-captured", "one-provider")
|
|
3702
|
+
SEARCH_STEPS = ("one-connector", "no-counter-search", "no-stopping-rule",
|
|
3703
|
+
"stopping-not-met", "known-item-missed", "screened-outside-search")
|
|
3704
|
+
EXPERIMENT_STEPS = ("no-replication", "no-power-plan", "below-planned-n", "no-interval",
|
|
3705
|
+
"interval-includes-zero", "below-achieved-mde",
|
|
3706
|
+
"no-multiplicity-correction", "fails-multiplicity", "no-sesoi",
|
|
3707
|
+
"tost-not-passed", "instrument-unvalidated", "instrument-rejected",
|
|
3708
|
+
"judged-row-incomplete", "falsifier-not-evaluable")
|
|
3709
|
+
CLASS_STEPS = {"R2": ("no-grey-quality",), "R3": ("no-population",),
|
|
3710
|
+
"R4": ("no-confounders",),
|
|
3711
|
+
"R5": ("fewer-than-two-rivals", "no-positive-control"),
|
|
3712
|
+
"R6": ("fewer-than-two-rivals",),
|
|
3713
|
+
"R7": ("no-dual-screening", "no-recall-stopping"),
|
|
3714
|
+
"R8": ("no-dual-screening",), "R9": ("no-amstar",), "R10": ("no-codebook",),
|
|
3715
|
+
"R12": ("fewer-than-two-approaches",)}
|
|
3716
|
+
TERMINAL_STATUSES = ("supported", "refuted", "weakened", "revised", "untested",
|
|
3717
|
+
"inconclusive")
|
|
3718
|
+
DEAD_ROWS = ("aborted", "failed")
|
|
3719
|
+
# The comparator words match ASCII-case-insensitively (`(?a:...)`): a Unicode
|
|
3720
|
+
# IGNORECASE lets U+017F match `s`, a spelling FALSIFIER_OPS holds no key for
|
|
3721
|
+
# (review round two R2-2).
|
|
3722
|
+
FALSIFIER_COMPARATOR = re.compile(
|
|
3723
|
+
r"(?i)(?:<=|>=|≤|≥|<|>|=|\b(?a:at most|at least|above|below|less than|more than"
|
|
3724
|
+
r"|fewer than|greater than|exceeds?|under|over))\s*-?\d")
|
|
3725
|
+
FALSIFIER_TEST = re.compile(
|
|
3726
|
+
r"(?i)\b(?:t-test|welch|wilcoxon|mann-whitney|chi-square|fisher|binomial|tost)\b")
|
|
3727
|
+
FALSIFIER_ALPHA = re.compile(r"(?i)\balpha\s*=?\s*0?\.\d+")
|
|
3728
|
+
# The structured form (spec 2.2) takes every comparator the grammar above names; each
|
|
3729
|
+
# maps to the operator it means. Anything else stays unevaluated (review D3-2).
|
|
3730
|
+
FALSIFIER_OPS = {"<=": "<=", ">=": ">=", "≤": "<=", "≥": ">=", "<": "<", ">": ">", "=": "=",
|
|
3731
|
+
"at most": "<=", "at least": ">=", "above": ">", "below": "<",
|
|
3732
|
+
"less than": "<", "more than": ">", "fewer than": "<", "greater than": ">",
|
|
3733
|
+
"exceeds": ">", "exceed": ">", "under": "<", "over": ">"}
|
|
3734
|
+
FALSIFIER_FORM = re.compile(
|
|
3735
|
+
r"(?i)^([\w.-]+)(?:\s*(<=|>=|≤|≥|<|>|=)|\s+(?a:(at\s+most|at\s+least|above|below"
|
|
3736
|
+
r"|less\s+than|more\s+than|fewer\s+than|greater\s+than|exceeds?|under|over)))"
|
|
3737
|
+
r"\s*(-?\d+(?:\.\d+)?)(?:\s+on\s+(\S+))?\s*$")
|
|
3738
|
+
# A protocol number (spec 2.3): `0.05`, `.05`, `5%` (0.05), `1e-1`.
|
|
3739
|
+
PROTOCOL_NUMBER = re.compile(r"([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?)(\s*%)?")
|
|
3740
|
+
# Each checked protocol number's open range (lower, upper; None = unbounded).
|
|
3741
|
+
PROTOCOL_RANGES = {"alpha": (0.0, 1.0), "power": (0.0, 1.0), "sesoi": (0.0, None),
|
|
3742
|
+
"mde": (0.0, None)}
|
|
3743
|
+
COMPARE_FORM = re.compile(
|
|
3744
|
+
r"(?i)^(\S+)\s+vs\s+(\S+)\s+on\s+([\w.-]+)\s*,\s*(lower|higher)\s+is\s+better")
|
|
3745
|
+
CLAIM_NUMBER = re.compile(r"(?<![\w.])-?\d+(?:\.\d+)?(?![\w.])")
|
|
3746
|
+
DERIVED_REF = re.compile(r"^([\w.-]+)/(#\d+|[\w.-]+):([\w.-]+)$")
|
|
3747
|
+
UNIVERSAL_WORDS = re.compile(r"(?i)\b(every|all|always|never|any)\b")
|
|
3748
|
+
CAUSAL_WORDS = re.compile(r"(?i)\b(causes|caused|cause|leads to|due to|because of)\b")
|
|
3749
|
+
CODE_SUFFIXES = (".py", ".sh", ".js", ".ts", ".rs", ".go")
|
|
3750
|
+
|
|
3751
|
+
|
|
3752
|
+
def obligations(cls, method=""):
|
|
3753
|
+
"""The step names of spec section 2 a line of class `cls` can trigger."""
|
|
3754
|
+
out = ["no-question-class", "class-unreviewed", "method-not-allowed",
|
|
3755
|
+
"untraced-number", "uncalibrated-verifier"] + list(LITERATURE_STEPS)
|
|
3756
|
+
if cls in SEARCH_CLASSES or method == "qualitative":
|
|
3757
|
+
out += SEARCH_STEPS
|
|
3758
|
+
if method == "measured" or "measured" in CLASS_METHODS.get(cls, ()):
|
|
3759
|
+
out += EXPERIMENT_STEPS
|
|
3760
|
+
return out + list(CLASS_STEPS.get(cls, ()))
|
|
3761
|
+
|
|
3762
|
+
|
|
3763
|
+
def _step(steps, cid, name):
|
|
3764
|
+
"""Name `name` once for claim `cid`, in the order found."""
|
|
3765
|
+
found = steps.setdefault(cid, [])
|
|
3766
|
+
if name not in found:
|
|
3767
|
+
found.append(name)
|
|
3768
|
+
|
|
3769
|
+
|
|
3770
|
+
def _fields(text):
|
|
3771
|
+
"""{key: [values]} of every column-0 `key: value` line of a protocol."""
|
|
3772
|
+
out = {}
|
|
3773
|
+
for line in text.lstrip("\ufeff").splitlines():
|
|
3774
|
+
found = PROTOCOL_LABEL.match(line)
|
|
3775
|
+
if found:
|
|
3776
|
+
out.setdefault(found.group(1).lower(), []).append(found.group(2).strip())
|
|
3777
|
+
return out
|
|
3778
|
+
|
|
3779
|
+
|
|
3780
|
+
def _first(fields, key):
|
|
3781
|
+
return (fields.get(key) or [""])[0]
|
|
3782
|
+
|
|
3783
|
+
|
|
3784
|
+
def _number(value):
|
|
3785
|
+
"""The first number of a protocol value (`PROTOCOL_NUMBER`), a percent read as its
|
|
3786
|
+
fraction; None without one."""
|
|
3787
|
+
found = PROTOCOL_NUMBER.search(value or "")
|
|
3788
|
+
if not found:
|
|
3789
|
+
return None
|
|
3790
|
+
number = float(found.group(1))
|
|
3791
|
+
return number / 100 if found.group(2) else number
|
|
3792
|
+
|
|
3793
|
+
|
|
3794
|
+
def protocol_number(key, value):
|
|
3795
|
+
"""(number, problem) of the protocol line `key: value`, `key` one of PROTOCOL_RANGES:
|
|
3796
|
+
the one reader `check`, `claim`, the session context and the instruments share.
|
|
3797
|
+
(None, None) when the line is absent or empty; a value with no number, or one
|
|
3798
|
+
outside its range (alpha, power in (0, 1); sesoi, mde above 0), is an invalid
|
|
3799
|
+
record: (None, the refusal naming the key and the value). So is an alpha so close
|
|
3800
|
+
to 0 that alpha/2 or 1 - alpha/2, the quantiles the intervals read, is 0 or 1 as a
|
|
3801
|
+
float (`1e-20`, review round two R2-1)."""
|
|
3802
|
+
value = (value or "").strip()
|
|
3803
|
+
if not value:
|
|
3804
|
+
return None, None
|
|
3805
|
+
number = _number(value)
|
|
3806
|
+
lo, hi = PROTOCOL_RANGES[key]
|
|
3807
|
+
if number is not None and math.isfinite(number) and number > lo \
|
|
3808
|
+
and (hi is None or number < hi):
|
|
3809
|
+
if key == "alpha" and not (number / 2 > 0 and 1 - number / 2 < 1):
|
|
3810
|
+
return None, "protocol.md alpha: %s leaves a quantile at 0 or 1" % value
|
|
3811
|
+
return number, None
|
|
3812
|
+
return None, "protocol.md %s: %s is not a number %s" % (
|
|
3813
|
+
key, value, "above 0" if hi is None else "in (0, 1)")
|
|
3814
|
+
|
|
3815
|
+
|
|
3816
|
+
def _numeric(value):
|
|
3817
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
3818
|
+
|
|
3819
|
+
|
|
3820
|
+
def _live(rows):
|
|
3821
|
+
"""Rows that count. A refile is a correction: the latest row filed for the same
|
|
3822
|
+
`run`, `arm`, `item` and `instrument` stands for it, so one run measured over
|
|
3823
|
+
several arms or items keeps a row for each; a row with no `run` counts on its
|
|
3824
|
+
own. An `aborted` or `failed` run stays a row and counts nowhere."""
|
|
3825
|
+
key = lambda r: tuple(str(r.get(k)) for k in ("run", "arm", "item", "instrument")) # noqa: E731
|
|
3826
|
+
last = {key(r): n for n, r in enumerate(rows) if r.get("run") is not None}
|
|
3827
|
+
return [r for n, r in enumerate(rows)
|
|
3828
|
+
if (r.get("run") is None or last[key(r)] == n)
|
|
3829
|
+
and r.get("status") not in DEAD_ROWS]
|
|
3830
|
+
|
|
3831
|
+
|
|
3832
|
+
def _groups(rows):
|
|
3833
|
+
"""{group: rows}: by `arm`, else `phase`, else one group `all`."""
|
|
3834
|
+
live = _live(rows)
|
|
3835
|
+
key = next((k for k in ("arm", "phase") if any(k in r for r in live)), None)
|
|
3836
|
+
out = {}
|
|
3837
|
+
for row in live:
|
|
3838
|
+
out.setdefault(str(row[key]) if key in row else "all", []).append(row)
|
|
3839
|
+
return out
|
|
3840
|
+
|
|
3841
|
+
|
|
3842
|
+
def _paired(rows_a, rows_b):
|
|
3843
|
+
"""The two groups aligned by `item` when both carry the same item set, else None."""
|
|
3844
|
+
items_a, items_b = [r.get("item") for r in rows_a], [r.get("item") for r in rows_b]
|
|
3845
|
+
if None in items_a or None in items_b or len(set(map(str, items_a))) != len(items_a):
|
|
3846
|
+
return None
|
|
3847
|
+
if sorted(map(str, items_a)) != sorted(map(str, items_b)):
|
|
3848
|
+
return None
|
|
3849
|
+
order = {str(i): n for n, i in enumerate(items_a)}
|
|
3850
|
+
return rows_a, sorted(rows_b, key=lambda r: order[str(r.get("item"))])
|
|
3851
|
+
|
|
3852
|
+
|
|
3853
|
+
def _effect(fields, groups, alpha, power):
|
|
3854
|
+
"""The `compare:` line's effect (spec 2.3), or None without one."""
|
|
3855
|
+
import tezgah_research_stats as stats
|
|
3856
|
+
found = COMPARE_FORM.match(_first(fields, "compare"))
|
|
3857
|
+
if not found:
|
|
3858
|
+
return None
|
|
3859
|
+
t, c, field, better = found.groups()
|
|
3860
|
+
rows_t, rows_c = groups.get(t, []), groups.get(c, [])
|
|
3861
|
+
out = {"t": t, "c": c, "field": field, "n": (len(rows_t), len(rows_c)),
|
|
3862
|
+
"effect": None, "ci": None, "mde": None, "p": None, "a": [], "b": [],
|
|
3863
|
+
"paired": False}
|
|
3864
|
+
rows_t = [r for r in rows_t if _numeric(r.get(field))]
|
|
3865
|
+
rows_c = [r for r in rows_c if _numeric(r.get(field))]
|
|
3866
|
+
if better.lower() == "lower":
|
|
3867
|
+
rows_t, rows_c = rows_c, rows_t
|
|
3868
|
+
aligned = _paired(rows_t, rows_c) if len(rows_t) >= 2 else None
|
|
3869
|
+
if aligned:
|
|
3870
|
+
rows_t, rows_c = aligned
|
|
3871
|
+
a, b = [float(r[field]) for r in rows_t], [float(r[field]) for r in rows_c]
|
|
3872
|
+
out.update(a=a, b=b, paired=bool(aligned))
|
|
3873
|
+
if a and b:
|
|
3874
|
+
out["effect"] = sum(a) / len(a) - sum(b) / len(b)
|
|
3875
|
+
if len(a) >= 2 and len(b) >= 2:
|
|
3876
|
+
diff = stats.mean_diff(a, b, alpha, paired=bool(aligned))
|
|
3877
|
+
out.update(effect=diff["effect"], ci=(diff["lo"], diff["hi"]), p=diff["p"],
|
|
3878
|
+
mde=stats.achieved_mde(diff["se"], alpha, power))
|
|
3879
|
+
return out
|
|
3880
|
+
|
|
3881
|
+
|
|
3882
|
+
def _stats_failed(exp, exc):
|
|
3883
|
+
"""Keep an experiment's first statistics error (ValueError, ArithmeticError);
|
|
3884
|
+
`_check_rules8` turns it into the experiment's one refusal (review round two R2-1)."""
|
|
3885
|
+
if not exp.get("stats_error"):
|
|
3886
|
+
exp["stats_error"] = str(exc) or type(exc).__name__
|
|
3887
|
+
|
|
3888
|
+
|
|
3889
|
+
def experiment_record(base, h):
|
|
3890
|
+
"""One experiment as the rules-8 checks read it: protocol fields, rows, groups,
|
|
3891
|
+
the plan's numbers and the `compare:` effect. `invalid` holds the refusal of each
|
|
3892
|
+
protocol number outside its range; an invalid alpha or power is None and leaves
|
|
3893
|
+
the effect uncomputed, so no statistic runs on it. A statistics error leaves the
|
|
3894
|
+
effect None and lands in `stats_error`."""
|
|
3895
|
+
d = os.path.join(base, "experiments", h)
|
|
3896
|
+
fields = _fields(_note_text(os.path.join(d, "protocol.md")))
|
|
3897
|
+
rows = _row_objects(os.path.join(d, "results.jsonl"))
|
|
3898
|
+
groups = _groups(rows)
|
|
3899
|
+
plan = {k: protocol_number(k, _first(fields, k)) for k in PROTOCOL_RANGES}
|
|
3900
|
+
alpha, power = (None if plan[k][1] else plan[k][0] or default
|
|
3901
|
+
for k, default in (("alpha", 0.05), ("power", 0.80)))
|
|
3902
|
+
exp = {"h": h, "dir": d, "fields": fields, "rows": rows, "groups": groups,
|
|
3903
|
+
"alpha": alpha, "power": power, "sesoi": plan["sesoi"][0],
|
|
3904
|
+
"invalid": [problem for _, problem in plan.values() if problem],
|
|
3905
|
+
"effect": None, "stats_error": None}
|
|
3906
|
+
if alpha is not None and power is not None:
|
|
3907
|
+
try:
|
|
3908
|
+
exp["effect"] = _effect(fields, groups, alpha, power)
|
|
3909
|
+
except (ValueError, ArithmeticError) as exc:
|
|
3910
|
+
_stats_failed(exp, exc)
|
|
3911
|
+
return exp
|
|
3912
|
+
|
|
3913
|
+
|
|
3914
|
+
def falsifier_unscoped(text, groups):
|
|
3915
|
+
"""True when a structured falsifier names no `on <group>` while the rows hold
|
|
3916
|
+
more than one group: its mean would pool the control with the arm it judges."""
|
|
3917
|
+
found = FALSIFIER_FORM.match(text)
|
|
3918
|
+
return bool(found and not found.group(5) and len(groups) > 1)
|
|
3919
|
+
|
|
3920
|
+
|
|
3921
|
+
def falsifier_fired(exp):
|
|
3922
|
+
"""(falsifier, mean) when the structured `falsifier:` holds over its rows, else
|
|
3923
|
+
None - also when the group holds no row carrying the field, and when it names no
|
|
3924
|
+
group over several (`falsifier_unscoped`)."""
|
|
3925
|
+
text = _first(exp["fields"], "falsifier")
|
|
3926
|
+
found = FALSIFIER_FORM.match(text)
|
|
3927
|
+
if not found or falsifier_unscoped(text, exp["groups"]):
|
|
3928
|
+
return None
|
|
3929
|
+
field, symbol, words, number, group = found.groups()
|
|
3930
|
+
op = FALSIFIER_OPS.get(" ".join((symbol or words).casefold().split()))
|
|
3931
|
+
if op is None:
|
|
3932
|
+
return None
|
|
3933
|
+
rows = exp["groups"].get(group, []) if group else _live(exp["rows"])
|
|
3934
|
+
values = [float(r[field]) for r in rows if _numeric(r.get(field))]
|
|
3935
|
+
if not values:
|
|
3936
|
+
return None
|
|
3937
|
+
mean, x = sum(values) / len(values), float(number)
|
|
3938
|
+
holds = {"<": mean < x, "<=": mean <= x, ">": mean > x, ">=": mean >= x,
|
|
3939
|
+
"=": mean == x}[op]
|
|
3940
|
+
return (text, mean) if holds else None
|
|
3941
|
+
|
|
3942
|
+
|
|
3943
|
+
def _claim_experiments(claim, base):
|
|
3944
|
+
return [h for h in _experiments_named(_cited(claim.get("proof")))
|
|
3945
|
+
if os.path.isdir(os.path.join(base, "experiments", h))]
|
|
3946
|
+
|
|
3947
|
+
|
|
3948
|
+
def fired_problems(cid, claim, base):
|
|
3949
|
+
"""The refusal for a `supported` claim whose cited experiment's falsifier fired."""
|
|
3950
|
+
if claim.get("status") != "supported":
|
|
3951
|
+
return []
|
|
3952
|
+
out = []
|
|
3953
|
+
for h in _claim_experiments(claim, base):
|
|
3954
|
+
fired = falsifier_fired(experiment_record(base, h))
|
|
3955
|
+
if fired:
|
|
3956
|
+
out.append("claim %s is supported while its falsifier fired: experiments/%s "
|
|
3957
|
+
"`falsifier: %s` holds at a mean of %s" % (cid, h, fired[0],
|
|
3958
|
+
"%g" % fired[1]))
|
|
3959
|
+
return out
|
|
3960
|
+
|
|
3961
|
+
|
|
3962
|
+
def _ref_value(base, ref):
|
|
3963
|
+
"""The number `<h>/<run>:<field>` or `<h>/#<row>:<field>` names, as the Decimal of
|
|
3964
|
+
the value written in the row (`repr` gives back a JSON float's shortest digits), or
|
|
3965
|
+
None."""
|
|
3966
|
+
found = DERIVED_REF.match(str(ref).strip())
|
|
3967
|
+
if not found:
|
|
3968
|
+
return None
|
|
3969
|
+
h, which, field = found.groups()
|
|
3970
|
+
rows = _row_objects(os.path.join(base, "experiments", h, "results.jsonl"))
|
|
3971
|
+
if which.startswith("#"):
|
|
3972
|
+
k = int(which[1:])
|
|
3973
|
+
row = rows[k - 1] if 1 <= k <= len(rows) else None
|
|
3974
|
+
else:
|
|
3975
|
+
row = next((r for r in reversed(rows) if str(r.get("run")) == which), None)
|
|
3976
|
+
value = row.get(field) if row else None
|
|
3977
|
+
if not _numeric(value) or (isinstance(value, float) and not math.isfinite(value)):
|
|
3978
|
+
return None
|
|
3979
|
+
return decimal.Decimal(repr(value))
|
|
3980
|
+
|
|
3981
|
+
|
|
3982
|
+
def _recompute(spec, base):
|
|
3983
|
+
if not isinstance(spec, dict) or not isinstance(spec.get("of"), list):
|
|
3984
|
+
return None
|
|
3985
|
+
op, refs = spec.get("op"), spec["of"]
|
|
3986
|
+
if op == "count":
|
|
3987
|
+
return decimal.Decimal(len(refs))
|
|
3988
|
+
values = [_ref_value(base, ref) for ref in refs]
|
|
3989
|
+
if not values or None in values:
|
|
3990
|
+
return None
|
|
3991
|
+
if op in ("diff", "ratio"):
|
|
3992
|
+
if len(values) != 2 or (op == "ratio" and values[1] == 0):
|
|
3993
|
+
return None
|
|
3994
|
+
return values[0] - values[1] if op == "diff" else values[0] / values[1]
|
|
3995
|
+
return {"mean": lambda v: sum(v) / len(v), "sum": sum, "min": min,
|
|
3996
|
+
"max": max}.get(op, lambda v: None)(values)
|
|
3997
|
+
|
|
3998
|
+
|
|
3999
|
+
def _rounds_to(value, written):
|
|
4000
|
+
"""True when `value` (a Decimal) rounded half-up or half-even at the precision of the
|
|
4001
|
+
number as written equals it."""
|
|
4002
|
+
try:
|
|
4003
|
+
target = decimal.Decimal(written)
|
|
4004
|
+
return any(value.quantize(target, rounding=mode) == target
|
|
4005
|
+
for mode in (decimal.ROUND_HALF_UP, decimal.ROUND_HALF_EVEN))
|
|
4006
|
+
except decimal.DecimalException:
|
|
4007
|
+
return False
|
|
4008
|
+
|
|
4009
|
+
|
|
4010
|
+
def derived_problems(cid, claim, base):
|
|
4011
|
+
"""The refusals for `derived_from` entries whose recomputed value differs from
|
|
4012
|
+
the number as written (spec 2.4); writer and checker read this one rule. The
|
|
4013
|
+
arithmetic is decimal over the rows' written values, so 2.675 is 2.675, not the
|
|
4014
|
+
binary float below it."""
|
|
4015
|
+
derived = claim.get("derived_from")
|
|
4016
|
+
if derived is None:
|
|
4017
|
+
return []
|
|
4018
|
+
if not isinstance(derived, dict):
|
|
4019
|
+
return ["claim %s derived_from is not an object of number -> {op, of}" % cid]
|
|
4020
|
+
out = []
|
|
4021
|
+
with decimal.localcontext() as ctx:
|
|
4022
|
+
ctx.prec = 50
|
|
4023
|
+
for written, spec in derived.items():
|
|
4024
|
+
value = _recompute(spec, base)
|
|
4025
|
+
if value is None or not _rounds_to(value, written):
|
|
4026
|
+
out.append("claim %s derived_from %s does not recompute: %s gives %s"
|
|
4027
|
+
% (cid, written, json.dumps(spec),
|
|
4028
|
+
"nothing" if value is None else "%g" % value))
|
|
4029
|
+
return out
|
|
4030
|
+
|
|
4031
|
+
|
|
4032
|
+
def _log_tokens(exp, row, cache):
|
|
4033
|
+
rel = row.get("log")
|
|
4034
|
+
if not isinstance(rel, str) or not rel:
|
|
4035
|
+
return set()
|
|
4036
|
+
for root in (exp["dir"], os.path.dirname(os.path.dirname(exp["dir"]))):
|
|
4037
|
+
path = os.path.join(root, rel)
|
|
4038
|
+
if os.path.isfile(path):
|
|
4039
|
+
if path not in cache:
|
|
4040
|
+
cache[path] = set(CLAIM_NUMBER.findall(_note_text(path)))
|
|
4041
|
+
return cache[path]
|
|
4042
|
+
return set()
|
|
4043
|
+
|
|
4044
|
+
|
|
4045
|
+
def _untraced(claim, cited):
|
|
4046
|
+
"""The numbers of an evidence claim no cited row holds with its log, and no
|
|
4047
|
+
`derived_from` entry names (spec 2.4)."""
|
|
4048
|
+
derived = claim.get("derived_from")
|
|
4049
|
+
derived = derived if isinstance(derived, dict) else {}
|
|
4050
|
+
cache, out = {}, []
|
|
4051
|
+
for token in CLAIM_NUMBER.findall(str(claim.get("statement", ""))):
|
|
4052
|
+
if token in derived:
|
|
4053
|
+
continue
|
|
4054
|
+
value = float(token)
|
|
4055
|
+
if not any(token in _log_tokens(exp, row, cache)
|
|
4056
|
+
and any(_numeric(v) and float(v) == value for v in row.values())
|
|
4057
|
+
for exp in cited for row in _live(exp["rows"])):
|
|
4058
|
+
out.append(token)
|
|
4059
|
+
return out
|
|
4060
|
+
|
|
4061
|
+
|
|
4062
|
+
def _rivals(fields):
|
|
4063
|
+
return [v for v in fields.get("rival", [])
|
|
4064
|
+
if re.match(r"\S.*\sexcludes:\s*\S", v)]
|
|
4065
|
+
|
|
4066
|
+
|
|
4067
|
+
def _observational(fields):
|
|
4068
|
+
design = _first(fields, "design").lower()
|
|
4069
|
+
return design == "observational" or (not design and not _first(fields, "change"))
|
|
4070
|
+
|
|
4071
|
+
|
|
4072
|
+
def _experiment_steps(claim, cited, cls, measured, rejected):
|
|
4073
|
+
"""The section-2 steps an experiment claim takes from the experiments it cites."""
|
|
4074
|
+
import tezgah_research_stats as stats
|
|
4075
|
+
out = []
|
|
4076
|
+
no_difference = claim.get("effect") == "no-difference"
|
|
4077
|
+
for exp in cited:
|
|
4078
|
+
fields, groups, eff = exp["fields"], exp["groups"], exp["effect"]
|
|
4079
|
+
if cls in ("R5", "R6") and len(_rivals(fields)) < 2:
|
|
4080
|
+
out.append("fewer-than-two-rivals")
|
|
4081
|
+
if not groups or any(len(rows) < 2 for rows in groups.values()):
|
|
4082
|
+
out.append("no-replication")
|
|
4083
|
+
if measured and not all(_first(fields, k) for k in ("alpha", "power", "mde", "n")):
|
|
4084
|
+
out.append("no-power-plan")
|
|
4085
|
+
falsifier = _first(fields, "falsifier")
|
|
4086
|
+
if measured and falsifier and (not falsifier_testable(falsifier)
|
|
4087
|
+
or falsifier_unscoped(falsifier, groups)):
|
|
4088
|
+
out.append("falsifier-not-evaluable")
|
|
4089
|
+
if eff:
|
|
4090
|
+
planned = _number(_first(fields, "n"))
|
|
4091
|
+
if planned is not None and min(eff["n"]) < planned:
|
|
4092
|
+
out.append("below-planned-n")
|
|
4093
|
+
if eff["ci"] is None:
|
|
4094
|
+
out.append("no-interval")
|
|
4095
|
+
elif not no_difference:
|
|
4096
|
+
if eff["ci"][0] <= 0:
|
|
4097
|
+
out.append("interval-includes-zero")
|
|
4098
|
+
if eff["effect"] < eff["mde"]:
|
|
4099
|
+
out.append("below-achieved-mde")
|
|
4100
|
+
correction = _first(fields, "correction")
|
|
4101
|
+
if len(groups) > 2 and not correction:
|
|
4102
|
+
out.append("no-multiplicity-correction")
|
|
4103
|
+
if correction and exp["h"] not in rejected:
|
|
4104
|
+
out.append("fails-multiplicity")
|
|
4105
|
+
if no_difference:
|
|
4106
|
+
sesoi = exp["sesoi"]
|
|
4107
|
+
if sesoi is None:
|
|
4108
|
+
out.append("no-sesoi")
|
|
4109
|
+
else:
|
|
4110
|
+
try:
|
|
4111
|
+
passed = bool(eff and eff["ci"] and stats.tost(
|
|
4112
|
+
eff["a"], eff["b"], sesoi, exp["alpha"], eff["paired"]))
|
|
4113
|
+
except (ValueError, ArithmeticError) as exc:
|
|
4114
|
+
_stats_failed(exp, exc)
|
|
4115
|
+
passed = False
|
|
4116
|
+
if not passed:
|
|
4117
|
+
out.append("tost-not-passed")
|
|
4118
|
+
return out
|
|
4119
|
+
|
|
4120
|
+
|
|
4121
|
+
def _multiplicity(base, exps):
|
|
4122
|
+
"""The experiments their declared `correction:` rejects, corrected together."""
|
|
4123
|
+
import tezgah_research_stats as stats
|
|
4124
|
+
tests = {"bh": stats.bh, "holm": stats.holm, "bonferroni": stats.bonferroni}
|
|
4125
|
+
by = {}
|
|
4126
|
+
for h, exp in sorted(exps.items()):
|
|
4127
|
+
method = _first(exp["fields"], "correction").strip().lower()
|
|
4128
|
+
if method and exp["effect"]:
|
|
4129
|
+
by.setdefault(method, []).append(h)
|
|
4130
|
+
rejected = set()
|
|
4131
|
+
for method, hs in by.items():
|
|
4132
|
+
alpha = min(exps[h]["alpha"] for h in hs)
|
|
4133
|
+
try:
|
|
4134
|
+
if method == "e-bh":
|
|
4135
|
+
values = [_number(_first(_fields(_note_text(os.path.join(
|
|
4136
|
+
exps[h]["dir"], "analysis.md"))), "e-value")) or 0.0 for h in hs]
|
|
4137
|
+
flags = stats.ebh(values, alpha)
|
|
4138
|
+
elif method in tests:
|
|
4139
|
+
p = [exps[h]["effect"]["p"] for h in hs]
|
|
4140
|
+
flags = tests[method]([1.0 if v is None else v for v in p], alpha)
|
|
4141
|
+
else:
|
|
4142
|
+
continue
|
|
4143
|
+
except (ValueError, ArithmeticError) as exc:
|
|
4144
|
+
for h in hs:
|
|
4145
|
+
_stats_failed(exps[h], exc)
|
|
4146
|
+
continue
|
|
4147
|
+
rejected.update(h for h, flag in zip(hs, flags) if flag)
|
|
4148
|
+
return rejected
|
|
4149
|
+
|
|
4150
|
+
|
|
4151
|
+
def _format_ci(lo, hi):
|
|
4152
|
+
return "[%.4f, %.4f]" % (lo, hi)
|
|
4153
|
+
|
|
4154
|
+
|
|
4155
|
+
def falsifier_testable(text):
|
|
4156
|
+
"""True when a falsifier holds a comparator and a number, or a named test with its
|
|
4157
|
+
alpha (spec 2.2)."""
|
|
4158
|
+
return bool(FALSIFIER_COMPARATOR.search(text)
|
|
4159
|
+
or (FALSIFIER_TEST.search(text) and FALSIFIER_ALPHA.search(text)))
|
|
4160
|
+
|
|
4161
|
+
|
|
4162
|
+
# A `metric:` list splits at the commas that separate names: never inside parentheses
|
|
4163
|
+
# (`(ms, median of 5 runs)`), never between digit groups (`10,000-piece`), and never
|
|
4164
|
+
# inside the locked name, which is one name (plan 128 K1, K3).
|
|
4165
|
+
METRIC_COMMA = re.compile(r",(?![^()]*\))(?!(?<=\d,)\d)")
|
|
4166
|
+
# A word of a metric name, for the reworded comparison: hyphen and underscore split,
|
|
4167
|
+
# digits kept (`p95`), and the joining words a reworded name may drop or add
|
|
4168
|
+
# (`of`, `the`, `per`) ignored.
|
|
4169
|
+
METRIC_WORD = re.compile(r"[0-9a-z]+")
|
|
4170
|
+
METRIC_JOIN = {"a", "an", "the", "of", "for", "in", "on", "to", "per", "by", "with"}
|
|
4171
|
+
|
|
4172
|
+
|
|
4173
|
+
def _metric_key(name):
|
|
4174
|
+
"""A metric name as the rules compare it: casefolded, whitespace collapsed, a
|
|
4175
|
+
trailing parenthetical (`(ms)`) dropped."""
|
|
4176
|
+
return " ".join(re.sub(r"\s*\([^)]*\)\s*$", "", str(name or "")).casefold().split())
|
|
4177
|
+
|
|
4178
|
+
|
|
4179
|
+
def _metric_words(key):
|
|
4180
|
+
return {w for w in METRIC_WORD.findall(key) if w not in METRIC_JOIN}
|
|
4181
|
+
|
|
4182
|
+
|
|
4183
|
+
def _same_metric(a, b):
|
|
4184
|
+
"""True when one metric key is the other or a word-boundary prefix of it, either
|
|
4185
|
+
way (plan 128 K2), or a rewording of it: every word of `b` sits in `a`, in any
|
|
4186
|
+
order and with the joining words ignored (`read-path p95 latency` is
|
|
4187
|
+
`p95 latency of the read path`, K6 raters X6 and Y6)."""
|
|
4188
|
+
if a == b or a.startswith(b + " ") or b.startswith(a + " "):
|
|
4189
|
+
return True
|
|
4190
|
+
words = _metric_words(b)
|
|
4191
|
+
return bool(words) and words <= _metric_words(a)
|
|
4192
|
+
|
|
4193
|
+
|
|
4194
|
+
def _registered(state, fields):
|
|
4195
|
+
"""(names, keys, locked key): the metrics a protocol's `metric:` lines register,
|
|
4196
|
+
split at METRIC_COMMA with the locked name kept whole, and the line's lock."""
|
|
4197
|
+
evaluation = state.get("evaluation") if isinstance(state.get("evaluation"), dict) else {}
|
|
4198
|
+
locked = _metric_key(evaluation.get("metric"))
|
|
4199
|
+
whole = re.compile(r"\s*,\s*".join(r"\s+".join(map(re.escape, part.split()))
|
|
4200
|
+
for part in locked.split(",")), re.I) \
|
|
4201
|
+
if "," in locked else None
|
|
4202
|
+
names = []
|
|
4203
|
+
for value in fields.get("metric", []):
|
|
4204
|
+
if whole:
|
|
4205
|
+
value = whole.sub(lambda m: m.group(0).replace(",", "\0"), value)
|
|
4206
|
+
names += [m.strip().replace("\0", ",") for m in METRIC_COMMA.split(value)
|
|
4207
|
+
if m.strip()]
|
|
4208
|
+
return names, [_metric_key(m) for m in names], locked
|
|
4209
|
+
|
|
4210
|
+
|
|
4211
|
+
def metric_problem(state, fields):
|
|
4212
|
+
"""The locked-metric refusal of a protocol's `metric:` lines, or None (spec 2.2):
|
|
4213
|
+
one of them has to be the locked metric (`_same_metric`). `source` asks it
|
|
4214
|
+
before a rules-8 capture freezes the protocol (plan 128 K4)."""
|
|
4215
|
+
names, keys, locked = _registered(state, fields)
|
|
4216
|
+
if locked and keys and not any(_same_metric(k, locked) for k in keys):
|
|
4217
|
+
return ("protocol.md registers a metric other than the locked one (%s, locked: %s)"
|
|
4218
|
+
% (", ".join(names), locked))
|
|
4219
|
+
return None
|
|
4220
|
+
|
|
4221
|
+
|
|
4222
|
+
def _check_experiment8(repo, slug, base, state, exp, report, errors, warnings, strict,
|
|
4223
|
+
git, lines, code=None):
|
|
4224
|
+
"""One experiment's rules-8 refusals (2.2, 2.4) and report lines (2.3)."""
|
|
4225
|
+
import tezgah_research_stats as stats
|
|
4226
|
+
h, fields = exp["h"], exp["fields"]
|
|
4227
|
+
label = "experiment %s" % h
|
|
4228
|
+
errors.extend("%s: %s" % (label, problem) for problem in exp["invalid"])
|
|
4229
|
+
problem = metric_problem(state, fields)
|
|
4230
|
+
if problem:
|
|
4231
|
+
errors.append("%s: %s" % (label, problem))
|
|
4232
|
+
if report is not None:
|
|
4233
|
+
metrics, norm, locked = _registered(state, fields)
|
|
4234
|
+
excused = {_metric_key(v.split(":", 1)[0])
|
|
4235
|
+
for v in fields.get("not_reported", []) if ":" in v}
|
|
4236
|
+
text = " ".join(report.casefold().split())
|
|
4237
|
+
# protocol.md is frozen once the run is filed (`changed after the run`), so
|
|
4238
|
+
# `not_reported:` is an exit only before it (plan 128 K5)
|
|
4239
|
+
exit_ = ("the run is filed, so protocol.md is frozen: name it in to_human/report.md"
|
|
4240
|
+
if exp["rows"] else
|
|
4241
|
+
"name it in to_human/report.md, or before the run mark it "
|
|
4242
|
+
"`not_reported: <metric>: <reason>` in protocol.md")
|
|
4243
|
+
for metric, key in zip(metrics, norm):
|
|
4244
|
+
# a registered metric that is the locked one (the rule above's
|
|
4245
|
+
# equivalence) is reported where the report names the locked metric
|
|
4246
|
+
refines = locked and _same_metric(key, locked)
|
|
4247
|
+
if (key not in text and not (refines and locked in text)
|
|
4248
|
+
and key not in excused):
|
|
4249
|
+
errors.append("%s: registered metric %r is neither reported nor marked "
|
|
4250
|
+
"not_reported - %s" % (label, metric, exit_))
|
|
4251
|
+
evaluator = _first(fields, "evaluator")
|
|
4252
|
+
if evaluator:
|
|
4253
|
+
_check_evaluator(repo, base, state, exp, evaluator, label, errors, git, code)
|
|
4254
|
+
select = re.match(r"(?i)^(min|max)\s+([\w.-]+)", _first(fields, "select"))
|
|
4255
|
+
if select:
|
|
4256
|
+
field = select.group(2)
|
|
4257
|
+
values = {g: [float(r[field]) for r in rows if _numeric(r.get(field))]
|
|
4258
|
+
for g, rows in exp["groups"].items()}
|
|
4259
|
+
means = {g: sum(v) / len(v) for g, v in values.items() if v}
|
|
4260
|
+
if means:
|
|
4261
|
+
pick = (min if select.group(1).lower() == "min" else max)(
|
|
4262
|
+
sorted(means), key=means.get)
|
|
4263
|
+
found = re.search(r"(?im)^\W*selected\W*:\s*`?([\w.-]+)",
|
|
4264
|
+
_note_text(os.path.join(exp["dir"], "analysis.md")))
|
|
4265
|
+
if not found:
|
|
4266
|
+
_soft(errors, warnings, strict,
|
|
4267
|
+
"%s: analysis states no selected group, and `select:` picks %s"
|
|
4268
|
+
% (label, pick))
|
|
4269
|
+
elif found.group(1) != pick:
|
|
4270
|
+
errors.append("%s: select rule picks %s, and analysis.md selected: %s"
|
|
4271
|
+
% (label, pick, found.group(1)))
|
|
4272
|
+
for group, rows in exp["groups"].items():
|
|
4273
|
+
# binary: every outcome 0 or 1, a JSON boolean counting as 1/0 (True == 1)
|
|
4274
|
+
if rows and exp["alpha"] is not None and all(r.get("outcome") in (0, 1)
|
|
4275
|
+
for r in rows):
|
|
4276
|
+
k, n = sum(int(r["outcome"]) for r in rows), len(rows)
|
|
4277
|
+
try:
|
|
4278
|
+
wilson = _format_ci(*stats.wilson(k, n, exp["alpha"]))
|
|
4279
|
+
cp = _format_ci(*stats.clopper_pearson(k, n, exp["alpha"]))
|
|
4280
|
+
except (ValueError, ArithmeticError) as exc:
|
|
4281
|
+
_stats_failed(exp, exc)
|
|
4282
|
+
continue
|
|
4283
|
+
lines.append("stats %s: %s %s n=%d k=%d wilson=%s cp=%s"
|
|
4284
|
+
% (slug, h, group, n, k, wilson, cp))
|
|
4285
|
+
eff = exp["effect"]
|
|
4286
|
+
if eff:
|
|
4287
|
+
lines.append("effect %s: %s %s vs %s on %s effect=%s ci=%s mde=%s"
|
|
4288
|
+
% (slug, h, eff["t"], eff["c"], eff["field"],
|
|
4289
|
+
"none" if eff["effect"] is None else "%.4f" % eff["effect"],
|
|
4290
|
+
_format_ci(*eff["ci"]) if eff["ci"] else "none",
|
|
4291
|
+
"none" if eff["mde"] is None else "%.4f" % eff["mde"]))
|
|
4292
|
+
|
|
4293
|
+
|
|
4294
|
+
def evaluator_file(repo, base, code, rel):
|
|
4295
|
+
"""(path, root): an `evaluator:` path looked up in the line, the repository and
|
|
4296
|
+
the checkout `check` runs from (`code`, plan 128 K9), with the repository whose
|
|
4297
|
+
history holds it; (None, None) when none holds the file."""
|
|
4298
|
+
for root, at in ((repo, base), (repo, repo), (code, code)):
|
|
4299
|
+
if at and os.path.isfile(os.path.join(at, rel)):
|
|
4300
|
+
return os.path.join(at, rel), root
|
|
4301
|
+
return None, None
|
|
4302
|
+
|
|
4303
|
+
|
|
4304
|
+
def _commit_time(repo, path, sha):
|
|
4305
|
+
"""The commit time of `sha` in the first history of `path` that holds it, or ""."""
|
|
4306
|
+
stamp = ""
|
|
4307
|
+
for top, _rel in _histories(repo, path):
|
|
4308
|
+
out, err = _git_out(top, "show", "-s", "--format=%cI", sha)
|
|
4309
|
+
stamp = stamp or ("" if err else out.strip())
|
|
4310
|
+
return stamp
|
|
4311
|
+
|
|
4312
|
+
|
|
4313
|
+
def _text_at(root, path, sha, stamp):
|
|
4314
|
+
"""`path`'s committed text at commit `sha` - in a history that does not hold that
|
|
4315
|
+
commit (a project file read against a line in `.tezgah`'s repository), its latest
|
|
4316
|
+
version not after `stamp` - or None."""
|
|
4317
|
+
found = None
|
|
4318
|
+
for top, rel in _histories(root, path):
|
|
4319
|
+
rev = sha if _holds(top, sha) else None
|
|
4320
|
+
if rev is None and stamp:
|
|
4321
|
+
out, err = _git_out(top, "log", "-1", "--format=%H", "--until=" + stamp,
|
|
4322
|
+
"--", rel)
|
|
4323
|
+
rev = None if err else out.strip()
|
|
4324
|
+
if rev:
|
|
4325
|
+
out, err = _git_out(top, "show", "%s:%s" % (rev, rel))
|
|
4326
|
+
found = found if err else out
|
|
4327
|
+
return found
|
|
4328
|
+
|
|
4329
|
+
|
|
4330
|
+
def _sealed_evaluator(repo, base, state, h, root, path, git):
|
|
4331
|
+
"""The evaluator's sha256 at the line's conclusion, or None for a line not
|
|
4332
|
+
concluded (plan 128 K8): the hash `conclude` sealed, and for a seal older than
|
|
4333
|
+
that field the evaluator at the first committed state.json holding the seal."""
|
|
4334
|
+
import hashlib
|
|
4335
|
+
seal = state.get(SEAL)
|
|
4336
|
+
if not isinstance(seal, dict):
|
|
4337
|
+
return None
|
|
4338
|
+
rows = seal.get("experiments") if isinstance(seal.get("experiments"), dict) else {}
|
|
4339
|
+
row = rows.get(h) if isinstance(rows.get(h), dict) else {}
|
|
4340
|
+
if SHA256_HEX.fullmatch(str(row.get("evaluator") or "")):
|
|
4341
|
+
return row["evaluator"]
|
|
4342
|
+
if not git:
|
|
4343
|
+
return None
|
|
4344
|
+
state_path = os.path.join(base, "state.json")
|
|
4345
|
+
for top, rel in _histories(repo, state_path):
|
|
4346
|
+
for sha, text in _versions(top, rel):
|
|
4347
|
+
try:
|
|
4348
|
+
held = SEAL in json.loads(text)
|
|
4349
|
+
except (ValueError, TypeError):
|
|
4350
|
+
held = False
|
|
4351
|
+
if held:
|
|
4352
|
+
at = _text_at(root, path, sha, _commit_time(repo, state_path, sha))
|
|
4353
|
+
return None if at is None else hashlib.sha256(at.encode("utf-8")).hexdigest()
|
|
4354
|
+
return None
|
|
4355
|
+
|
|
4356
|
+
|
|
4357
|
+
def _check_evaluator(repo, base, state, exp, evaluator, label, errors, git, code=None):
|
|
4358
|
+
"""An `evaluator:` file holds the bytes it held at the protocol's first commit -
|
|
4359
|
+
in a history that does not hold that commit (a project evaluator of a line in
|
|
4360
|
+
`.tezgah`'s repository), its latest version not after that commit's time - and
|
|
4361
|
+
its declared `sha256:` (spec 2.4). A concluded line reads the evaluator as it was
|
|
4362
|
+
at the conclusion (`_sealed_evaluator`), so a later project edit is not its own."""
|
|
4363
|
+
import hashlib
|
|
4364
|
+
parts = evaluator.split()
|
|
4365
|
+
path, root = evaluator_file(repo, base, code, parts[0])
|
|
4366
|
+
declared = next((p.split(":", 1)[1] for p in parts[1:] if p.startswith("sha256:")), "")
|
|
4367
|
+
if path is None:
|
|
4368
|
+
errors.append("%s: evaluator %s is not a file in the line, the repository or the "
|
|
4369
|
+
"checkout" % (label, parts[0]))
|
|
4370
|
+
return
|
|
4371
|
+
now = _sealed_evaluator(repo, base, state, exp["h"], root, path, git) or _digest(path)
|
|
4372
|
+
if declared and now != declared.lower():
|
|
4373
|
+
errors.append("%s: evaluator file changed after lock: %s no longer hashes to its "
|
|
4374
|
+
"declared sha256" % (label, parts[0]))
|
|
4375
|
+
return
|
|
4376
|
+
if not git:
|
|
4377
|
+
return
|
|
4378
|
+
proto_path = os.path.join(exp["dir"], "protocol.md")
|
|
4379
|
+
proto = file_versions(repo, proto_path)
|
|
4380
|
+
if not proto:
|
|
4381
|
+
return
|
|
4382
|
+
sha = proto[0][0]
|
|
4383
|
+
locked = _text_at(root, path, sha, _commit_time(repo, proto_path, sha))
|
|
4384
|
+
if locked is None:
|
|
4385
|
+
# committed only after the lock: its first version is the nearest to it
|
|
4386
|
+
versions = file_versions(root, path)
|
|
4387
|
+
locked = versions[0][1] if versions else None
|
|
4388
|
+
if locked is not None and hashlib.sha256(locked.encode("utf-8")).hexdigest() != now:
|
|
4389
|
+
errors.append("%s: evaluator file changed after lock: %s differs from its state "
|
|
4390
|
+
"at the protocol's first commit" % (label, parts[0]))
|
|
4391
|
+
|
|
4392
|
+
|
|
4393
|
+
def _overclaim(cid, claim, cited, warnings):
|
|
4394
|
+
"""Universal words over fixture evidence, causal words over an observational or
|
|
4395
|
+
single-group experiment (spec 4.4). Warns in both modes."""
|
|
4396
|
+
text = str(claim.get("statement", ""))
|
|
4397
|
+
rows = [r for exp in cited for r in exp["rows"]]
|
|
4398
|
+
words = []
|
|
4399
|
+
if claim.get("scope") == "fixture" or (rows and all(r.get("scope") == "fixture"
|
|
4400
|
+
for r in rows)):
|
|
4401
|
+
words += UNIVERSAL_WORDS.findall(text)
|
|
4402
|
+
if any(_observational(exp["fields"]) or len(exp["groups"]) < 2 for exp in cited):
|
|
4403
|
+
words += CAUSAL_WORDS.findall(text)
|
|
4404
|
+
if words:
|
|
4405
|
+
warnings.append("claim %s overclaims: %s - the evidence it cites does not reach "
|
|
4406
|
+
"that far; narrow the claim to what was measured"
|
|
4407
|
+
% (cid, ", ".join('"%s"' % w for w in dict.fromkeys(words))))
|
|
4408
|
+
|
|
4409
|
+
|
|
4410
|
+
def _routing_steps(base, state, exps, review):
|
|
4411
|
+
"""The 3.3 steps that touch every claim of the line."""
|
|
4412
|
+
cls = state.get("question_class")
|
|
4413
|
+
out = []
|
|
4414
|
+
if not cls:
|
|
4415
|
+
out.append("no-question-class")
|
|
4416
|
+
if review is not None and review.get("question_class") != cls:
|
|
4417
|
+
out.append("class-unreviewed")
|
|
4418
|
+
if cls in CLASS_METHODS and state.get("method") not in CLASS_METHODS[cls]:
|
|
4419
|
+
out.append("method-not-allowed")
|
|
4420
|
+
index = _row_objects(os.path.join(base, "literature", "INDEX.jsonl"))
|
|
4421
|
+
protocols = [exp["fields"] for exp in exps.values()]
|
|
4422
|
+
if cls in ("R7", "R8"):
|
|
4423
|
+
screeners, dropped = {}, set()
|
|
4424
|
+
for row in _row_objects(os.path.join(base, "searches", "screening.jsonl")):
|
|
4425
|
+
rid = str(row.get("id"))
|
|
4426
|
+
if rid not in screeners and row.get("decision") == "dropped":
|
|
4427
|
+
dropped.add(rid)
|
|
4428
|
+
screeners.setdefault(rid, set()).add(
|
|
4429
|
+
re.sub(r"\s+", "", str(row.get("screener") or "")).casefold())
|
|
4430
|
+
dual = {rid for rid, who in screeners.items() if len(who - {""}) >= 2}
|
|
4431
|
+
if cls == "R7":
|
|
4432
|
+
thin = not screeners or len(dual) < len(screeners)
|
|
4433
|
+
else:
|
|
4434
|
+
thin = (not screeners or 5 * len(dual) < len(screeners)
|
|
4435
|
+
or bool(dropped - dual))
|
|
4436
|
+
if thin:
|
|
4437
|
+
out.append("no-dual-screening")
|
|
4438
|
+
stopping = state.get("stopping")
|
|
4439
|
+
if cls == "R7" and not (isinstance(stopping, dict)
|
|
4440
|
+
and stopping.get("rule") == "hypergeometric"):
|
|
4441
|
+
out.append("no-recall-stopping")
|
|
4442
|
+
if cls == "R5" and state.get("method") == "measured" and any(
|
|
4443
|
+
not _first(f, "control") for f in protocols):
|
|
4444
|
+
out.append("no-positive-control")
|
|
4445
|
+
if cls == "R4" and any(not _first(f, "confounders") for f in protocols):
|
|
4446
|
+
out.append("no-confounders")
|
|
4447
|
+
if cls == "R3" and not (state.get("population") and state.get("n")):
|
|
4448
|
+
out.append("no-population")
|
|
4449
|
+
if cls == "R2" and any(r.get("class") == "grey" and not str(r.get("quality", "")).strip()
|
|
4450
|
+
for r in index):
|
|
4451
|
+
out.append("no-grey-quality")
|
|
4452
|
+
if cls == "R9" and any(not str(r.get("amstar", "")).strip() for r in index):
|
|
4453
|
+
out.append("no-amstar")
|
|
4454
|
+
if cls == "R10" and not os.path.isfile(os.path.join(base, "codebook.md")):
|
|
4455
|
+
out.append("no-codebook")
|
|
4456
|
+
approaches = state.get("approaches")
|
|
4457
|
+
if cls == "R12" and sum(1 for a in approaches or [] if isinstance(a, dict)
|
|
4458
|
+
and a.get("biases")) < 2:
|
|
4459
|
+
out.append("fewer-than-two-approaches")
|
|
4460
|
+
return out
|
|
4461
|
+
|
|
4462
|
+
|
|
4463
|
+
def budget(slug, base, state):
|
|
4464
|
+
"""(report line, [what is over]) of the tier's budget (spec 3.3)."""
|
|
4465
|
+
caps = BUDGETS.get(state.get("tier"), BUDGETS[DEFAULT_TIER])
|
|
4466
|
+
used = (len(_row_objects(os.path.join(base, "literature", "INDEX.jsonl"))),
|
|
4467
|
+
len(_row_objects(os.path.join(base, "searches", "SEARCHES.jsonl"))),
|
|
4468
|
+
sum(1 for h in _experiment_names(base)
|
|
4469
|
+
for r in _row_objects(os.path.join(base, "experiments", h, "results.jsonl"))
|
|
4470
|
+
if str(r.get("source", "")).startswith("orx:")))
|
|
4471
|
+
names = ("sources", "searches", "runs")
|
|
4472
|
+
line = "budget %s: %s" % (slug, " ".join("%s %d/%d" % (n, u, c)
|
|
4473
|
+
for n, u, c in zip(names, used, caps)))
|
|
4474
|
+
return line, ["%s %d/%d" % (n, u, c) for n, u, c in zip(names, used, caps) if u > c]
|
|
4475
|
+
|
|
4476
|
+
|
|
4477
|
+
def hypothesis_problems(state):
|
|
4478
|
+
"""A concluded line ends every hypothesis in a terminal status (spec 2.2)."""
|
|
4479
|
+
out = []
|
|
4480
|
+
for n, entry in enumerate(state.get("hypotheses") or [], 1):
|
|
4481
|
+
status = entry.get("status") if isinstance(entry, dict) else None
|
|
4482
|
+
if status not in TERMINAL_STATUSES:
|
|
4483
|
+
hid = entry.get("id") if isinstance(entry, dict) and entry.get("id") else n
|
|
4484
|
+
out.append("state.json hypothesis %s has no terminal status (%r): a concluded "
|
|
4485
|
+
"line ends each one %s" % (hid, status, ", ".join(TERMINAL_STATUSES)))
|
|
4486
|
+
return out
|
|
4487
|
+
|
|
4488
|
+
|
|
4489
|
+
def overlaps(repo, question, slug=None):
|
|
4490
|
+
"""[(line, claim, status, similarity)] of the done lines' claims whose
|
|
4491
|
+
`similarity_matrix` value against `question` is >= 0.5, over the corpus of the
|
|
4492
|
+
question plus every done line's claim statements (spec 4.2)."""
|
|
4493
|
+
done = [(name, row) for name in slugs(repo) if name != slug and sealed(repo, name)
|
|
4494
|
+
for row in _row_objects(os.path.join(line_dir(repo, name), "claims.jsonl"))
|
|
4495
|
+
if str(row.get("statement", "")).strip()]
|
|
4496
|
+
if not done or not str(question or "").strip():
|
|
4497
|
+
return []
|
|
4498
|
+
_backend, matrix = similarity_matrix([question] + [str(r["statement"]) for _, r in done])
|
|
4499
|
+
return [(name, row.get("id"), row.get("status"), matrix[0][n])
|
|
4500
|
+
for n, (name, row) in enumerate(done, 1) if matrix[0][n] >= 0.5]
|
|
4501
|
+
|
|
4502
|
+
|
|
4503
|
+
def _post_hoc(repo, ddir):
|
|
4504
|
+
"""The variant ids whose variants.jsonl row was first committed after the first
|
|
4505
|
+
comparison.jsonl cell (spec 4.5)."""
|
|
4506
|
+
paths = [os.path.join(ddir, f) for f in ("variants.jsonl", "comparison.jsonl")]
|
|
4507
|
+
top, _rel = _histories(repo, paths[0])[-1]
|
|
4508
|
+
order, err = _git_out(top, "rev-list", "--reverse", "HEAD")
|
|
4509
|
+
if err:
|
|
4510
|
+
return set()
|
|
4511
|
+
index = {sha: n for n, sha in enumerate(order.split())}
|
|
4512
|
+
firsts = []
|
|
4513
|
+
for path in paths:
|
|
4514
|
+
seen = {}
|
|
4515
|
+
for sha, text in _versions(top, _histories(repo, path)[-1][1]):
|
|
4516
|
+
for line in text.splitlines():
|
|
4517
|
+
if line.strip() and sha in index:
|
|
4518
|
+
seen.setdefault(line.strip(), index[sha])
|
|
4519
|
+
firsts.append(seen)
|
|
4520
|
+
if not firsts[1]:
|
|
4521
|
+
return set()
|
|
4522
|
+
first_cell = min(firsts[1].values())
|
|
4523
|
+
out = set()
|
|
4524
|
+
for line, n in firsts[0].items():
|
|
4525
|
+
if n > first_cell:
|
|
4526
|
+
try:
|
|
4527
|
+
row = json.loads(line)
|
|
4528
|
+
except ValueError:
|
|
4529
|
+
continue
|
|
4530
|
+
if isinstance(row, dict) and row.get("id"):
|
|
4531
|
+
out.add(str(row["id"]))
|
|
4532
|
+
return out
|
|
4533
|
+
|
|
4534
|
+
|
|
4535
|
+
def _check_review8(base, claims, review, errors, warnings, strict):
|
|
4536
|
+
"""4.1: families compared normalised, and a review of evidence claims that reads
|
|
4537
|
+
a raw log or code."""
|
|
4538
|
+
if review is None:
|
|
4539
|
+
return
|
|
4540
|
+
family = [re.sub(r"\s+", "", str(review.get(k) or "")).casefold()
|
|
4541
|
+
for k in ("producer_family", "reviewer_family")]
|
|
4542
|
+
if family[0] and family[0] == family[1]:
|
|
4543
|
+
warnings.append("to_human/review.json: same-family review (%s): producer and "
|
|
4544
|
+
"reviewer share a model family, so they share its blind spots"
|
|
4545
|
+
% family[0])
|
|
4546
|
+
if any(c.get("kind") == "evidence" for c in claims):
|
|
4547
|
+
targets = [str(f.get("target", "")).strip() for f in review.get("findings") or []
|
|
4548
|
+
if isinstance(f, dict)]
|
|
4549
|
+
if not any(re.match(r"experiments/[^/]+/raw/", t) or t.endswith(CODE_SUFFIXES)
|
|
4550
|
+
for t in targets):
|
|
4551
|
+
_soft(errors, warnings, strict,
|
|
4552
|
+
"to_human/review.json: review quotes no raw log or code, so no finding "
|
|
4553
|
+
"reads the bytes an evidence claim rests on")
|
|
4554
|
+
|
|
4555
|
+
|
|
4556
|
+
def _core_rules8(repo, slug, base, state, errors, warnings, strict, git, steps, code=None):
|
|
4557
|
+
rows = _row_objects(os.path.join(base, "claims.jsonl"))
|
|
4558
|
+
claims = [c for c in rows if isinstance(c.get("id"), str) and c["id"]]
|
|
4559
|
+
superseded = _superseded(rows)
|
|
4560
|
+
exps = {h: experiment_record(base, h) for h in _experiment_names(base)}
|
|
4561
|
+
report_path = os.path.join(base, "to_human", "report.md")
|
|
4562
|
+
report = _note_text(report_path) if os.path.isfile(report_path) else None
|
|
4563
|
+
review_path = os.path.join(base, "to_human", "review.json")
|
|
4564
|
+
review, exc = _read_json(review_path) if os.path.isfile(review_path) else (None, None)
|
|
4565
|
+
review = review if isinstance(review, dict) and not exc else None
|
|
4566
|
+
lines = steps.setdefault("#report", [])
|
|
4567
|
+
for h in sorted(exps):
|
|
4568
|
+
_check_experiment8(repo, slug, base, state, exps[h], report, errors, warnings,
|
|
4569
|
+
strict, git, lines, code)
|
|
4570
|
+
rejected = _multiplicity(base, exps)
|
|
4571
|
+
every = _routing_steps(base, state, exps, review)
|
|
4572
|
+
cls, measured = state.get("question_class"), state.get("method") == "measured"
|
|
4573
|
+
for claim in claims:
|
|
4574
|
+
cid = claim["id"]
|
|
4575
|
+
cited = [exps[h] for h in _experiments_named(_cited(claim.get("proof")))
|
|
4576
|
+
if h in exps]
|
|
4577
|
+
if cid not in superseded:
|
|
4578
|
+
errors.extend(derived_problems(cid, claim, base))
|
|
4579
|
+
errors.extend(fired_problems(cid, claim, base))
|
|
4580
|
+
found = _experiment_steps(claim, cited, cls, measured, rejected) if cited else []
|
|
4581
|
+
if claim.get("kind") == "evidence" and _untraced(claim, cited):
|
|
4582
|
+
found.append("untraced-number")
|
|
4583
|
+
for name in found + every:
|
|
4584
|
+
_step(steps, cid, name)
|
|
4585
|
+
_overclaim(cid, claim, cited, warnings)
|
|
4586
|
+
failed = steps.setdefault("#stats", {})
|
|
4587
|
+
for h in sorted(exps):
|
|
4588
|
+
if exps[h]["stats_error"]:
|
|
4589
|
+
failed.setdefault("experiment %s" % h, exps[h]["stats_error"])
|
|
4590
|
+
_check_review8(base, claims, review, errors, warnings, strict)
|
|
4591
|
+
if state.get("phase") == "concluded":
|
|
4592
|
+
errors.extend(hypothesis_problems(state))
|
|
4593
|
+
line, over = budget(slug, base, state)
|
|
4594
|
+
lines.append(line)
|
|
4595
|
+
if over:
|
|
4596
|
+
warnings.append("over budget: %s (tier %s)" % (", ".join(over),
|
|
4597
|
+
state.get("tier") or DEFAULT_TIER))
|
|
4598
|
+
if git:
|
|
4599
|
+
for name, ddir in _decision_dirs(base):
|
|
4600
|
+
body = _note_text(os.path.join(ddir, "decision.md"))
|
|
4601
|
+
picked = line_pick(body)
|
|
4602
|
+
if picked and picked in _post_hoc(repo, ddir) \
|
|
4603
|
+
and "post-hoc" not in body and "post_hoc" not in body:
|
|
4604
|
+
errors.append("decisions/%s: decision.md chooses %s, a variant first "
|
|
4605
|
+
"committed after the first comparison cell, without saying "
|
|
4606
|
+
"post-hoc" % (name, picked))
|
|
4607
|
+
related = {(str(r.get("line")), str(r.get("claim")))
|
|
4608
|
+
for r in _row_objects(os.path.join(base, "related.jsonl"))
|
|
4609
|
+
if r.get("relation") in ("builds-on", "dismissed")}
|
|
4610
|
+
for line_name, cid, status, _sim in overlaps(repo, state.get("question"), slug):
|
|
4611
|
+
if status == "refuted" and (line_name, str(cid)) not in related:
|
|
4612
|
+
warnings.append("overlaps refuted claim %s/%s: record in related.jsonl "
|
|
4613
|
+
"whether this line builds on it or dismisses it, and why"
|
|
4614
|
+
% (line_name, cid))
|
|
4615
|
+
|
|
4616
|
+
|
|
4617
|
+
def _check_rules8(repo, slug, base, state, errors, warnings, strict, git, flags=None,
|
|
4618
|
+
code=None):
|
|
4619
|
+
"""steps of a rules-8 line: the capture checks, the core ones, then the evidence
|
|
4620
|
+
module, which reads every step found and holds the certainty engine. A module
|
|
4621
|
+
that cannot be imported is an error on a rules-8 line, never a silent pass. A
|
|
4622
|
+
statistics computation's ValueError or ArithmeticError, kept in `#stats` by
|
|
4623
|
+
the module it ran in, is one refusal per experiment (review round two R2-1)."""
|
|
4624
|
+
steps = {"#flags": {"online": bool((flags or {}).get("online")),
|
|
4625
|
+
"orx": bool((flags or {}).get("orx"))}, "#stats": {}}
|
|
4626
|
+
import importlib
|
|
4627
|
+
for name in ("tezgah_research_capture", None, "tezgah_research_evidence"):
|
|
4628
|
+
if name is None:
|
|
4629
|
+
_core_rules8(repo, slug, base, state, errors, warnings, strict, git, steps, code)
|
|
4630
|
+
continue
|
|
4631
|
+
try:
|
|
4632
|
+
module = importlib.import_module(name)
|
|
4633
|
+
except ImportError as exc:
|
|
4634
|
+
errors.append("rules 8: %s cannot be imported (%s), so its checks did not run"
|
|
4635
|
+
% (name, exc))
|
|
4636
|
+
continue
|
|
4637
|
+
module.check(repo, slug, base, state, errors, warnings, strict, git, steps)
|
|
4638
|
+
errors.extend("%s: statistics could not be computed: %s" % (label, error)
|
|
4639
|
+
for label, error in sorted(steps["#stats"].items()))
|
|
4640
|
+
return steps
|
|
4641
|
+
|
|
4642
|
+
|
|
4643
|
+
def check_line(repo, slug, git=True, strict=False, code=None, flags=None, extra=None):
|
|
3555
4644
|
"""(errors, warnings) for one research line. `git=False` skips the checks that
|
|
3556
4645
|
need git history, for callers that must stay cheap (the session context).
|
|
3557
4646
|
`strict=True` turns every guarantee the checker cannot decide into an error.
|
|
3558
4647
|
`code` is the checkout the check runs in when it is not `repo`: the line
|
|
3559
4648
|
lives in the main checkout's workspace (plan 097) and its citations and
|
|
3560
4649
|
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).
|
|
4650
|
+
bare repository's worktree holding the cwd (`tp.work_tree`, plan 098).
|
|
4651
|
+
A rules-8 line (`_evidence`) also runs `_check_rules8` with `flags` (`online`,
|
|
4652
|
+
`orx`); a dict `extra` receives its `report` lines and per-claim `certainty`."""
|
|
3562
4653
|
code = code or tp.work_tree(repo)
|
|
3563
4654
|
base = line_dir(repo, slug)
|
|
3564
4655
|
errors, warnings = [], []
|
|
@@ -3586,6 +4677,13 @@ def check_line(repo, slug, git=True, strict=False, code=None):
|
|
|
3586
4677
|
if git:
|
|
3587
4678
|
_check_tracking(repo, base, errors, warnings, strict)
|
|
3588
4679
|
_check_review_integrity(base, errors, warnings, strict)
|
|
4680
|
+
state = _state(base)
|
|
4681
|
+
if _evidence(state):
|
|
4682
|
+
steps = _check_rules8(repo, slug, base, state, errors, warnings, strict, git, flags,
|
|
4683
|
+
code)
|
|
4684
|
+
if extra is not None:
|
|
4685
|
+
extra["report"] = list(steps.get("#report", []))
|
|
4686
|
+
extra["certainty"] = dict(steps.get("#certainty", {}))
|
|
3589
4687
|
return errors, warnings
|
|
3590
4688
|
|
|
3591
4689
|
|
|
@@ -3692,22 +4790,29 @@ def _check_derived_phase(base, phase, direction, warnings):
|
|
|
3692
4790
|
"stopped describing the line" % (direction, derived, want))
|
|
3693
4791
|
|
|
3694
4792
|
|
|
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
|
|
4793
|
+
def check(repo, slug=None, git=True, strict=False, all_lines=True, code=None, flags=None):
|
|
4794
|
+
"""{slug: {"errors": [...], "warnings": [...]}} for one line or every line; a
|
|
4795
|
+
rules-8 line adds "report" (the lines `check` prints, never counted) and
|
|
4796
|
+
"certainty" ({claim id: {level, start, steps}}).
|
|
3697
4797
|
|
|
3698
4798
|
With `all_lines=False` and no slug, a line under `done/` is read by its order
|
|
3699
4799
|
seal alone (`done_seal`), not re-checked, and appears only when that read
|
|
3700
4800
|
fails: the default no-slug `check` and the session note report the open
|
|
3701
|
-
lines, and `check --all-lines` re-runs the closed ones (ADR 015).
|
|
3702
|
-
|
|
4801
|
+
lines, and `check --all-lines` re-runs the closed ones (ADR 015). With no
|
|
4802
|
+
slug, each hidden dir under research/ (`stray_dirs`) is a row of its own,
|
|
4803
|
+
keyed by its path."""
|
|
4804
|
+
out = {} if slug else {path: {"errors": [error], "warnings": []}
|
|
4805
|
+
for path, error in stray_dirs(repo)}
|
|
3703
4806
|
for name in ([slug] if slug else slugs(repo)):
|
|
3704
4807
|
if not slug and not all_lines and sealed(repo, name):
|
|
3705
4808
|
errors = done_seal(repo, name)[0]
|
|
3706
4809
|
if errors:
|
|
3707
4810
|
out[name] = {"errors": errors, "warnings": []}
|
|
3708
4811
|
continue
|
|
3709
|
-
|
|
3710
|
-
|
|
4812
|
+
extra = {}
|
|
4813
|
+
errors, warnings = check_line(repo, name, git=git, strict=strict, code=code,
|
|
4814
|
+
flags=flags, extra=extra)
|
|
4815
|
+
out[name] = dict({"errors": errors, "warnings": warnings}, **extra)
|
|
3711
4816
|
return out
|
|
3712
4817
|
|
|
3713
4818
|
|
|
@@ -3789,7 +4894,9 @@ def _last_add(repo, path):
|
|
|
3789
4894
|
|
|
3790
4895
|
def _seal_rows(repo, base):
|
|
3791
4896
|
"""{experiment: seal row}: the blob hashes of every experiment holding a
|
|
3792
|
-
protocol.md, and the add commits of a run one, informational only.
|
|
4897
|
+
protocol.md, and the add commits of a run one, informational only. A protocol
|
|
4898
|
+
naming an `evaluator:` also seals the evaluator's hash, the version `check`
|
|
4899
|
+
reads for the concluded line (`_sealed_evaluator`, plan 128 K8)."""
|
|
3793
4900
|
out = {}
|
|
3794
4901
|
for h, (proto, results) in _seal_files(base).items():
|
|
3795
4902
|
row = {"protocol": _digest(proto),
|
|
@@ -3797,6 +4904,11 @@ def _seal_rows(repo, base):
|
|
|
3797
4904
|
if results:
|
|
3798
4905
|
row["commits"] = {"protocol": _last_add(repo, proto),
|
|
3799
4906
|
"results": _last_add(repo, results)}
|
|
4907
|
+
words = _first(_fields(_note_text(proto)), "evaluator").split()
|
|
4908
|
+
evaluator = evaluator_file(repo, base, tp.work_tree(repo), words[0])[0] \
|
|
4909
|
+
if words else None
|
|
4910
|
+
if evaluator:
|
|
4911
|
+
row["evaluator"] = _digest(evaluator)
|
|
3800
4912
|
out[h] = row
|
|
3801
4913
|
return out
|
|
3802
4914
|
|
|
@@ -3982,7 +5094,7 @@ def import_line(repo, source, slug=None, allow_open=None, date=""):
|
|
|
3982
5094
|
lines, here = _committed_lines(ws, rev), slugs(repo)
|
|
3983
5095
|
if slug is not None and slug not in lines:
|
|
3984
5096
|
return [], ("%s is not committed in %s, so it has no history to import; "
|
|
3985
|
-
"commit it there (
|
|
5097
|
+
"commit it there (`~/.config/tezgah/bin/tezgah-research commit`) - a plain copy is "
|
|
3986
5098
|
"refused because it loses the order proof" % (slug, src))
|
|
3987
5099
|
wanted = [slug] if slug is not None else sorted(s for s in lines if s not in here)
|
|
3988
5100
|
clash = [s for s in wanted if s in here]
|
|
@@ -3999,14 +5111,14 @@ def import_line(repo, source, slug=None, allow_open=None, date=""):
|
|
|
3999
5111
|
unfinished = open_lines(repo) if arriving else []
|
|
4000
5112
|
if (unfinished or len(arriving) > 1) and allow_open is None:
|
|
4001
5113
|
return [], ("%s would open beside unfinished line(s) %s; say why with "
|
|
4002
|
-
"--allow-open \"<reason>\"" % (
|
|
5114
|
+
"--allow-open \"<reason>\"; %s" % (
|
|
4003
5115
|
", ".join(arriving),
|
|
4004
5116
|
"; ".join("%s (%s)" % (n, "; ".join(r)) for n, r in unfinished)
|
|
4005
|
-
or ", ".join(arriving)))
|
|
5117
|
+
or ", ".join(arriving), OWNER_ASK))
|
|
4006
5118
|
broken = broken_open_lines(repo) if unfinished else []
|
|
4007
5119
|
if broken:
|
|
4008
5120
|
return [], ("--allow-open does not open a line beside an open line check "
|
|
4009
|
-
"refuses: %s" % ", ".join(n for n, _e in broken))
|
|
5121
|
+
"refuses: %s; %s" % (", ".join(n for n, _e in broken), OWNER_ASK))
|
|
4010
5122
|
if fresh:
|
|
4011
5123
|
done = tp.ws_git(repo, "commit", "-q", "--allow-empty", "-m",
|
|
4012
5124
|
"research: workspace root")
|
|
@@ -4134,10 +5246,16 @@ def _variants_owed(base, state):
|
|
|
4134
5246
|
def broken_open_lines(repo):
|
|
4135
5247
|
"""[(slug, errors)] for the open lines `check` refuses: what `init
|
|
4136
5248
|
--allow-open` will not open a new line beside, because a hatch past broken
|
|
4137
|
-
lines is how they stayed broken.
|
|
5249
|
+
lines is how they stayed broken. A criterion whose verdict is still
|
|
5250
|
+
`pending` is a line in flight, not a broken one, so that error alone does
|
|
5251
|
+
not block: another session may be running the line."""
|
|
4138
5252
|
out = []
|
|
4139
5253
|
for slug, _reasons in open_lines(repo):
|
|
4140
5254
|
errors, _ = check_line(repo, slug)
|
|
5255
|
+
pending = {_verdict_error(sid, verdict) for sid, _c, verdict, _e
|
|
5256
|
+
in success_rows(line_state(repo, slug).get("success"))
|
|
5257
|
+
if verdict == "pending"}
|
|
5258
|
+
errors = [e for e in errors if e not in pending]
|
|
4141
5259
|
if errors:
|
|
4142
5260
|
out.append((slug, errors))
|
|
4143
5261
|
return out
|
|
@@ -4220,8 +5338,20 @@ def conclude_line(repo, slug, date=""):
|
|
|
4220
5338
|
"(state.json `success`) before concluding")
|
|
4221
5339
|
if unjudged:
|
|
4222
5340
|
problems.append("no verdict for %s: record it with "
|
|
4223
|
-
"
|
|
5341
|
+
"`~/.config/tezgah/bin/tezgah-research verdict <slug> <id> met|not-met|unanswerable "
|
|
4224
5342
|
"--evidence <ref>`" % ", ".join(unjudged))
|
|
5343
|
+
if _evidence(state):
|
|
5344
|
+
# the rules `check` refuses a concluded rules-8 line for, asked before the
|
|
5345
|
+
# line is concluded: every hypothesis terminal, every rejected instrument
|
|
5346
|
+
# switched or narrowed (the evidence module's rule)
|
|
5347
|
+
problems.extend(hypothesis_problems(state))
|
|
5348
|
+
try:
|
|
5349
|
+
import tezgah_research_evidence as evidence
|
|
5350
|
+
except ImportError as exc:
|
|
5351
|
+
problems.append("rules 8: tezgah_research_evidence cannot be imported (%s)"
|
|
5352
|
+
% exc)
|
|
5353
|
+
else:
|
|
5354
|
+
problems.extend(evidence.conclude_problems(repo, slug, base, state))
|
|
4225
5355
|
if problems:
|
|
4226
5356
|
return problems, None
|
|
4227
5357
|
state.update(phase="concluded", direction="conclude")
|
|
@@ -4238,6 +5368,75 @@ def conclude_line(repo, slug, date=""):
|
|
|
4238
5368
|
return ([problem] if problem else []), state
|
|
4239
5369
|
|
|
4240
5370
|
|
|
5371
|
+
# The line every refusal over open lines carries (`init`, `import`): deciding
|
|
5372
|
+
# about another session's work (its line, plan or branch) is the owner's.
|
|
5373
|
+
OWNER_ASK = ("a line another session is running is not this session's to fix or "
|
|
5374
|
+
"close: stop and ask the owner (a decision only the owner makes), never "
|
|
5375
|
+
"a way around a tezgah refusal (another folder, a hidden dir)")
|
|
5376
|
+
|
|
5377
|
+
|
|
5378
|
+
def choose_line(repo, slug, decision, variant, ack, date=""):
|
|
5379
|
+
"""The problem that stopped the owner's choice being written, or None.
|
|
5380
|
+
|
|
5381
|
+
A line recommends; the owner chooses (rules 9). `chosen: <variant>` and
|
|
5382
|
+
`owner: <date> "<words>"` replace any earlier pair in the decision's
|
|
5383
|
+
decision.md; the line's own pick stays as written, and a pick a rules-8 line
|
|
5384
|
+
wrote as `chosen:` is renamed `recommended:`. log.md gets one line. A done
|
|
5385
|
+
line takes it too: its order seal hashes the experiments alone, so the
|
|
5386
|
+
decision note stays writable."""
|
|
5387
|
+
words = " ".join(str(ack or "").replace('"', "'").split())
|
|
5388
|
+
if not words:
|
|
5389
|
+
return ("choose needs --ack \"<the owner's words>\": choosing a variant or "
|
|
5390
|
+
"option a research, analysis or review recommends is the owner's call")
|
|
5391
|
+
base = line_dir(repo, slug)
|
|
5392
|
+
newer = newer_rules(_state(base))
|
|
5393
|
+
if newer:
|
|
5394
|
+
return newer
|
|
5395
|
+
ddir = dict(_decision_dirs(base)).get(decision)
|
|
5396
|
+
path = os.path.join(ddir or "", "decision.md")
|
|
5397
|
+
if not ddir or not os.path.isfile(path):
|
|
5398
|
+
return "decisions/%s has no decision.md in %s" % (decision, slug)
|
|
5399
|
+
_criteria, variants, _problems = load_decision(ddir)
|
|
5400
|
+
produced = sorted(v for v, row in variants.items() if row.get("status") == "produced")
|
|
5401
|
+
if variant not in produced:
|
|
5402
|
+
return "variant %s is not produced in decisions/%s (produced: %s)" % (
|
|
5403
|
+
variant, decision, ", ".join(produced) or "none")
|
|
5404
|
+
body = _note_text(path)
|
|
5405
|
+
replace = OWNER_WORDS.search(body) or re.search(PICK % "recommended", body)
|
|
5406
|
+
kept = []
|
|
5407
|
+
for line in body.splitlines():
|
|
5408
|
+
if re.match(r"(?i)\W*owner\W*:", line):
|
|
5409
|
+
continue
|
|
5410
|
+
if re.match(r"(?i)\W*chosen\W*:", line):
|
|
5411
|
+
if replace:
|
|
5412
|
+
continue
|
|
5413
|
+
# the line wrote its own pick as `chosen:` (rules 8): it stays the
|
|
5414
|
+
# line's pick, for the line's rules to judge
|
|
5415
|
+
line = re.sub(r"(?i)chosen", "recommended", line, count=1)
|
|
5416
|
+
kept.append(line)
|
|
5417
|
+
try:
|
|
5418
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
5419
|
+
fh.write("\n".join(kept + ["chosen: %s" % variant,
|
|
5420
|
+
'owner: %s "%s"' % (date, words)]) + "\n")
|
|
5421
|
+
with open(os.path.join(base, "log.md"), "a", encoding="utf-8") as fh:
|
|
5422
|
+
fh.write('- %s owner chose %s in decisions/%s: "%s"\n'
|
|
5423
|
+
% (date, variant, decision, words))
|
|
5424
|
+
except OSError as exc:
|
|
5425
|
+
return str(exc)
|
|
5426
|
+
return None
|
|
5427
|
+
|
|
5428
|
+
|
|
5429
|
+
def owner_waits(repo, slug):
|
|
5430
|
+
"""[(decision, variant)] for each decision.md that recommends a variant and
|
|
5431
|
+
records no owner choice: what `conclude` tells the session to report."""
|
|
5432
|
+
out = []
|
|
5433
|
+
for name, ddir in _decision_dirs(line_dir(repo, slug)):
|
|
5434
|
+
picked, field = decision_pick(_note_text(os.path.join(ddir, "decision.md")))
|
|
5435
|
+
if field == "recommended":
|
|
5436
|
+
out.append((name, picked))
|
|
5437
|
+
return out
|
|
5438
|
+
|
|
5439
|
+
|
|
4241
5440
|
def open_lines(repo):
|
|
4242
5441
|
"""[(slug, [reason, ...])] for every line that is not finished, for `init`.
|
|
4243
5442
|
|
|
@@ -4296,6 +5495,7 @@ def summary(repo):
|
|
|
4296
5495
|
if fixtures:
|
|
4297
5496
|
note += ", %d fixture-scoped claim(s): %s" % (len(fixtures), ", ".join(fixtures))
|
|
4298
5497
|
out.append("%s: %s" % (slug, note))
|
|
5498
|
+
out.extend("FAIL %s" % error for _path, error in stray_dirs(repo))
|
|
4299
5499
|
unfinished = open_note(repo)
|
|
4300
5500
|
if unfinished:
|
|
4301
5501
|
out.append(unfinished)
|
|
@@ -4678,7 +5878,51 @@ def urlsplit_host(source):
|
|
|
4678
5878
|
return host[4:] if host.startswith("www.") else host
|
|
4679
5879
|
|
|
4680
5880
|
|
|
4681
|
-
|
|
5881
|
+
# A run log above 16 MiB is refused rather than committed as a receipt unseen; the
|
|
5882
|
+
# refusal names `--ack`, which files it whole when the owner says so (K6 rater X6).
|
|
5883
|
+
RUN_LOG_BOUND = 16 * 1024 * 1024
|
|
5884
|
+
# What orx 0.2.20's `orx logs` prints instead of the log: the path, the size, a tail.
|
|
5885
|
+
ORX_LOG_SUMMARY = re.compile(r"\s*This is the path to the full log file: (.+?)[ \t]*\n"
|
|
5886
|
+
r"\s*It is ([\d,]+) bytes\.")
|
|
5887
|
+
|
|
5888
|
+
|
|
5889
|
+
def _bound_problem(verb, run, size, bound):
|
|
5890
|
+
return ("orx logs %s %s %d bytes, above the %d-byte bound a receipt is committed "
|
|
5891
|
+
"under - file it with --ack \"<the user's words>\" to keep it whole"
|
|
5892
|
+
% (run, verb, size, bound))
|
|
5893
|
+
|
|
5894
|
+
|
|
5895
|
+
def _full_log(repo, run, out, bound=RUN_LOG_BOUND):
|
|
5896
|
+
"""(bytes, problem): the run log `orx logs` printed - its output, or, when that is
|
|
5897
|
+
orx's summary, the file the summary names, held to the size it printed (K6 rater
|
|
5898
|
+
Y2: the summary's 500-character tail is not the log). Either is held to `bound`
|
|
5899
|
+
(`RUN_LOG_BOUND`; None when the owner acknowledged the size, which reads it whole)."""
|
|
5900
|
+
found = ORX_LOG_SUMMARY.match(out)
|
|
5901
|
+
if not found:
|
|
5902
|
+
if out.lstrip().startswith("This is the path to the full log file:"):
|
|
5903
|
+
return None, ("orx logs %s printed the log's path without its size, so the "
|
|
5904
|
+
"file cannot be held to what orx saw" % run)
|
|
5905
|
+
data = out.encode("utf-8")
|
|
5906
|
+
if bound is not None and len(data) > bound:
|
|
5907
|
+
return None, _bound_problem("printed", run, len(data), bound)
|
|
5908
|
+
return data, None
|
|
5909
|
+
path, size = os.path.join(repo, found.group(1)), int(found.group(2).replace(",", ""))
|
|
5910
|
+
if bound is not None and size > bound:
|
|
5911
|
+
return None, _bound_problem("said", run, size, bound)
|
|
5912
|
+
try:
|
|
5913
|
+
with open(path, "rb") as fh:
|
|
5914
|
+
data = fh.read(-1 if bound is None else bound + 1)
|
|
5915
|
+
except OSError as exc:
|
|
5916
|
+
return None, "cannot read the log orx logs %s names: %s" % (run, exc)
|
|
5917
|
+
if len(data) != size:
|
|
5918
|
+
return None, ("orx logs %s said %d bytes and %s holds %d: the run is still "
|
|
5919
|
+
"writing it or it changed since, so it is not the log orx saw - "
|
|
5920
|
+
"file the run once its log is complete" % (run, size, path, len(data)))
|
|
5921
|
+
return data, None
|
|
5922
|
+
|
|
5923
|
+
|
|
5924
|
+
def source_run(repo, slug, hypothesis, run, orx, command="", scope="", fixture="",
|
|
5925
|
+
arm="", item="", values=(), instrument="", ack=""):
|
|
4682
5926
|
"""(path, problems): file one orx run as this experiment's receipt.
|
|
4683
5927
|
|
|
4684
5928
|
`orx logs` is read here rather than by the session, so the log lands in the
|
|
@@ -4700,37 +5944,129 @@ def source_run(repo, slug, hypothesis, run, orx, command="", scope="", fixture="
|
|
|
4700
5944
|
without it files the row and says which field is still missing rather than
|
|
4701
5945
|
refusing a row a session may have a reason to file; a value that is not a
|
|
4702
5946
|
string, or one that is empty, is the mistake the field exists to catch and is
|
|
4703
|
-
refused.
|
|
5947
|
+
refused.
|
|
5948
|
+
|
|
5949
|
+
`values` are (field, number as written) pairs: each lands in the row as a
|
|
5950
|
+
number, and one equal to no number of the stored log is refused - equal as a
|
|
5951
|
+
number, so `1.5` matches a logged `1.50` (review round two K-6). `instrument`,
|
|
5952
|
+
`arm` and `item` land as strings, so the instrument ledger (`gold`, `label`)
|
|
5953
|
+
reads the row (K-7). A run already filed keeps its log; the new one is
|
|
5954
|
+
`raw/<run>.<n>.log` (a capture is written once). A rules-8 line's row also
|
|
5955
|
+
names its `run`, the engine's
|
|
5956
|
+
`status` when the line names an orx project, and the log and row are committed
|
|
5957
|
+
as `capture: source <slug>` (plan 127, spec 1.4).
|
|
5958
|
+
|
|
5959
|
+
`ack` is the owner's words for a log above `RUN_LOG_BOUND`: the log is stored
|
|
5960
|
+
whole and the row records the words (K6 rater X6)."""
|
|
4704
5961
|
if scope and scope not in SCOPES:
|
|
4705
5962
|
return None, ["scope %r is not one of %s" % (scope, ", ".join(SCOPES))]
|
|
4706
5963
|
if fixture and (not isinstance(fixture, str) or not fixture.strip()):
|
|
4707
5964
|
return None, ["fixture %r does not name what was generated" % (fixture,)]
|
|
4708
|
-
|
|
5965
|
+
base = line_dir(repo, slug)
|
|
5966
|
+
state = _state(base)
|
|
5967
|
+
newer = newer_rules(state)
|
|
4709
5968
|
if newer:
|
|
4710
5969
|
return None, [newer]
|
|
4711
5970
|
experiment = os.path.join(line_dir(repo, slug), "experiments", hypothesis)
|
|
4712
5971
|
if not os.path.isfile(os.path.join(experiment, "protocol.md")):
|
|
4713
5972
|
return None, ["experiments/%s holds no protocol.md: write and commit the "
|
|
4714
5973
|
"plan before filing the run it predicts" % hypothesis]
|
|
4715
|
-
|
|
5974
|
+
if _evidence(state) and (_orders_lock(state) or _exploratory(state)):
|
|
5975
|
+
# the capture is committed at once, after HEAD of the history carrying the
|
|
5976
|
+
# line, so an uncommitted lock would follow its results for good: the order
|
|
5977
|
+
# `_check_evaluation_order` reads - an exploratory line's lock is its arena,
|
|
5978
|
+
# ordered by `_check_arena_order` (plan 128 K12) - asked of that HEAD before
|
|
5979
|
+
# anything is written
|
|
5980
|
+
lock_path = os.path.join(base, "state.json")
|
|
5981
|
+
top = _histories(repo, lock_path)[-1][0]
|
|
5982
|
+
arena = _exploratory(state)
|
|
5983
|
+
firsts = _arena_lock(repo, base)[0] if arena else _evaluation_commit(repo, lock_path)
|
|
5984
|
+
if _answers(repo, firsts, {top: "HEAD"}).get(top) is not True:
|
|
5985
|
+
return None, ["%s is not committed: no committed state.json holding %s "
|
|
5986
|
+
"precedes this capture, so its results would come before the "
|
|
5987
|
+
"lock for good - commit the line first (`tezgah-research commit "
|
|
5988
|
+
"%s \"<message>\"`); nothing was written"
|
|
5989
|
+
% ((("the arena", "the arena") if arena else
|
|
5990
|
+
("the evaluation lock", "the metric and the baseline"))
|
|
5991
|
+
+ (slug,))]
|
|
5992
|
+
if _evidence(state):
|
|
5993
|
+
# the same reason as the lock: what the capture commits next would leave the
|
|
5994
|
+
# protocol refused for good - entered after its results, or frozen invalid
|
|
5995
|
+
# (left, refused; fixed, `changed after the run`) - K6 rater X2
|
|
5996
|
+
commit = 'commit the line first (`tezgah-research commit %s "<message>"`); ' \
|
|
5997
|
+
'nothing was written' % slug
|
|
5998
|
+
record = experiment_record(base, hypothesis)
|
|
5999
|
+
if record["invalid"]:
|
|
6000
|
+
return None, ["experiments/%s/protocol.md carries an invalid record (%s): the "
|
|
6001
|
+
"capture commits its results at once, after which the protocol "
|
|
6002
|
+
"cannot be fixed - fix it, then %s"
|
|
6003
|
+
% (hypothesis, "; ".join(record["invalid"]), commit)]
|
|
6004
|
+
metric = metric_problem(state, record["fields"])
|
|
6005
|
+
if metric:
|
|
6006
|
+
return None, ["experiments/%s/%s: the capture commits its results at once, "
|
|
6007
|
+
"after which the protocol cannot be fixed - name the locked "
|
|
6008
|
+
"metric in its `metric:` line, then %s" % (hypothesis, metric,
|
|
6009
|
+
commit)]
|
|
6010
|
+
if not _git_out(repo, "rev-parse", "--is-inside-work-tree")[1] and not any(
|
|
6011
|
+
_oldest_adds(repo, os.path.join(experiment, "protocol.md"))[0].values()):
|
|
6012
|
+
return None, ["experiments/%s/protocol.md is not committed: the capture commits "
|
|
6013
|
+
"its results at once, so the protocol would enter the history "
|
|
6014
|
+
"after them for good - %s" % (hypothesis, commit)]
|
|
6015
|
+
out, err = _run([orx, "logs", run], cwd=repo)
|
|
4716
6016
|
if err:
|
|
4717
6017
|
return None, ["orx logs %s failed: %s" % (run, err)]
|
|
4718
|
-
if
|
|
6018
|
+
data, problem = _full_log(repo, run, out, None if ack else RUN_LOG_BOUND)
|
|
6019
|
+
if problem:
|
|
6020
|
+
return None, [problem]
|
|
6021
|
+
if not data.strip():
|
|
4719
6022
|
return None, ["orx logs %s returned nothing, so there is no receipt to "
|
|
4720
6023
|
"file" % run]
|
|
6024
|
+
import tezgah_research_capture as cap # deferred: it imports this module
|
|
6025
|
+
logged = cap.log_numbers(data.decode("utf-8", "replace"))
|
|
6026
|
+
for field, written in values:
|
|
6027
|
+
if decimal.Decimal(written) not in logged:
|
|
6028
|
+
return None, ["%s=%s: value not in the run log (orx logs %s holds no number "
|
|
6029
|
+
"equal to %s)" % (field, written, run, written)]
|
|
4721
6030
|
raw = os.path.join(experiment, "raw")
|
|
4722
6031
|
os.makedirs(raw, exist_ok=True)
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
6032
|
+
name, n = run + ".log", 0
|
|
6033
|
+
while os.path.lexists(os.path.join(raw, name)):
|
|
6034
|
+
n += 1
|
|
6035
|
+
name = "%s.%d.log" % (run, n)
|
|
6036
|
+
path = os.path.join(raw, name)
|
|
6037
|
+
with open(path, "wb") as fh:
|
|
6038
|
+
fh.write(data)
|
|
6039
|
+
row = {"source": "orx:%s" % run, "log": "raw/%s" % name}
|
|
6040
|
+
if _evidence(state):
|
|
6041
|
+
row["run"] = run
|
|
6042
|
+
if instrument:
|
|
6043
|
+
row["instrument"] = instrument
|
|
6044
|
+
if arm:
|
|
6045
|
+
row["arm"] = arm
|
|
6046
|
+
if item:
|
|
6047
|
+
row["item"] = item
|
|
6048
|
+
for field, written in values:
|
|
6049
|
+
row[field] = int(written) if re.fullmatch(r"-?\d+", written) else float(written)
|
|
4727
6050
|
if command:
|
|
4728
6051
|
row["command"] = command
|
|
6052
|
+
if ack:
|
|
6053
|
+
row["ack"] = ack
|
|
4729
6054
|
if scope:
|
|
4730
6055
|
row["scope"] = scope
|
|
4731
6056
|
if fixture:
|
|
4732
6057
|
row["fixture"] = fixture
|
|
4733
|
-
|
|
6058
|
+
if _evidence(state):
|
|
6059
|
+
runs, _err = cap.engine_runs(state)
|
|
6060
|
+
status = next((r.get("status") for r in runs or () if cap.run_id(r) == run), None)
|
|
6061
|
+
if isinstance(status, str) and status:
|
|
6062
|
+
row["status"] = status
|
|
6063
|
+
results = os.path.join(experiment, "results.jsonl")
|
|
6064
|
+
problems = _append_row(results, row)
|
|
6065
|
+
if not problems and _evidence(state):
|
|
6066
|
+
err = cap.commit_capture(repo, base, [os.path.relpath(path, base),
|
|
6067
|
+
os.path.relpath(results, base)],
|
|
6068
|
+
"source", slug)
|
|
6069
|
+
problems = [err] if err else []
|
|
4734
6070
|
return path, problems
|
|
4735
6071
|
|
|
4736
6072
|
|
|
@@ -4899,7 +6235,7 @@ def verdict_problems(state, rows):
|
|
|
4899
6235
|
|
|
4900
6236
|
|
|
4901
6237
|
def init(repo, slug, question="", created="", supersedes=None, ask="", tier=DEFAULT_TIER,
|
|
4902
|
-
method="", scope_in=(), scope_out=(), arena=None):
|
|
6238
|
+
method="", scope_in=(), scope_out=(), arena=None, question_class=""):
|
|
4903
6239
|
"""Scaffold a research line. Returns the paths created (never overwrites).
|
|
4904
6240
|
|
|
4905
6241
|
Refuses a slug `slugs()` cannot list - see `valid_slug` - so no caller can
|
|
@@ -4914,6 +6250,8 @@ def init(repo, slug, question="", created="", supersedes=None, ask="", tier=DEFA
|
|
|
4914
6250
|
os.makedirs(path, exist_ok=True)
|
|
4915
6251
|
state = dict(STATE_TEMPLATE)
|
|
4916
6252
|
state["method"] = method
|
|
6253
|
+
if question_class:
|
|
6254
|
+
state["question_class"] = question_class
|
|
4917
6255
|
state["scope"] = {"in": list(scope_in), "out": list(scope_out)}
|
|
4918
6256
|
state["question"] = question
|
|
4919
6257
|
state["created"] = created
|
|
@@ -6239,9 +7577,9 @@ def bradley_terry(ids, matches):
|
|
|
6239
7577
|
|
|
6240
7578
|
def _next_action(slug, iid, stage, history, record, arena):
|
|
6241
7579
|
"""The one action an idea is owed next, as the command that does it."""
|
|
6242
|
-
cmd = "tezgah-research stage %s %s" % (slug, iid)
|
|
7580
|
+
cmd = "~/.config/tezgah/bin/tezgah-research stage %s %s" % (slug, iid)
|
|
6243
7581
|
if stage == "proposed":
|
|
6244
|
-
return ("dedup (
|
|
7582
|
+
return ("dedup (`~/.config/tezgah/bin/tezgah-research dedup %s`), then `%s prior-art --evidence "
|
|
6245
7583
|
"'{\"verdict\": \"novel|incremental|known\", \"nearest\": [3 indexed "
|
|
6246
7584
|
"notes], \"delta\": \"<one sentence>\"}'`" % (slug, cmd))
|
|
6247
7585
|
if stage == "duplicate":
|
|
@@ -6254,7 +7592,7 @@ def _next_action(slug, iid, stage, history, record, arena):
|
|
|
6254
7592
|
if stage == "prior-art":
|
|
6255
7593
|
if record["judges"] >= 2 and record["orders"] >= 2:
|
|
6256
7594
|
return "`%s screened --evidence tournament.jsonl`" % cmd
|
|
6257
|
-
return ("judge it:
|
|
7595
|
+
return ("judge it: `~/.config/tezgah/bin/tezgah-research match %s` (2+ judges of a family that "
|
|
6258
7596
|
"generated neither idea, both orders)" % slug)
|
|
6259
7597
|
if stage == "screened":
|
|
6260
7598
|
return ("pilot: commit experiments/<h>/protocol.md naming %s and its kill "
|
|
@@ -6318,7 +7656,7 @@ def board(repo, slug):
|
|
|
6318
7656
|
live = [e for e in entries if e["stage"] not in ("confirmed", "killed")]
|
|
6319
7657
|
live.sort(key=lambda e: (-STAGES.index(e["stage"]), -e["score"], _idea_number(e["id"])))
|
|
6320
7658
|
nxt = ("%s - %s" % (live[0]["id"], live[0]["next"]) if live else
|
|
6321
|
-
("record ideas:
|
|
7659
|
+
("record ideas: `~/.config/tezgah/bin/tezgah-research idea %s`" % slug) if not ideas else
|
|
6322
7660
|
"nothing left: every idea is confirmed or killed - write the report")
|
|
6323
7661
|
evaluator = ("none (review only)" if not _text(arena.get("evaluator")) else
|
|
6324
7662
|
"%s %s" % (arena["evaluator"], "changed" if evaluator_problem(repo, state)
|
|
@@ -6444,7 +7782,7 @@ def dedup_text(report, slug):
|
|
|
6444
7782
|
rep = c["members"][0]
|
|
6445
7783
|
out.append("cluster %s: %s" % (rep, ", ".join(
|
|
6446
7784
|
"%s (%.2f)" % (m, c["similarity"][m]) for m in c["members"][1:])))
|
|
6447
|
-
out.extend(" tezgah-research stage %s %s duplicate --evidence '%s'"
|
|
7785
|
+
out.extend(" ~/.config/tezgah/bin/tezgah-research stage %s %s duplicate --evidence '%s'"
|
|
6448
7786
|
% (slug, m, json.dumps({"of": rep, "similarity": c["similarity"][m],
|
|
6449
7787
|
"backend": report["backend"]}))
|
|
6450
7788
|
for m in c["members"][1:])
|