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