diff --git a/.verify-floor.json b/.verify-floor.json index 60062db..48941ac 100644 --- a/.verify-floor.json +++ b/.verify-floor.json @@ -4,6 +4,6 @@ "skipped_max": 26, "selftest_skipped_max": 7, "gate_skipped_max": 2, - "note": "Recorded by verify.py --update-floor, except the *_max ceilings, which are edited BY HAND and never re-measured. `collected` catches tests that stopped being collected; `passed` is compared against passed+skipped, so a check may move between passing and consciously-skipped but the two together may never shrink. The *_max ceilings bound the skipped side: 24/7/2 is exactly what a machine with none of this instance's local prerequisites skips (a GitHub runner: no agent CLIs, no ~/.codex/skills, no /Applications/ChatGPT.app, no populated capability ledger), measured 2026-08-21. On the owner's machine all prerequisites exist and nothing skips at all. Raising a ceiling is a deliberate act: it means agreeing that one more thing is allowed to go unchecked, so say which and why in the commit. LOWERED 26 -> 24 on 2026-08-22, reverting the raise made earlier the same day. The two kill-switch exemption tests no longer need to skip on a bare runner: their declarations moved out of the running instance's ledger and into capabilities.KNOWN_DECLARATIONS, so they assert code-derived truth and run everywhere. Moving a test back below the ceiling is the preferred way to lower it -- fix what made it machine-dependent, rather than agreeing to check less. FLOOR 345 -> 353 on 2026-08-22: 345 was measured on a branch cut before #13 (research panels/rounds/domain studies) merged, so the recorded floor sat 8 tests BELOW what main actually collects. A floor below reality is the permissive direction -- those 8 could have silently stopped being collected and still cleared the check, which is exactly the hole this file exists to close. Measure the floor on the merge result, not on the branch. Raised again on 2026-08-22 by the producer-identity-scope branch, which adds tests on top of the 353 recorded by #15; re-measured after rebasing rather than assumed. NOTE: `verify.py --update-floor` REPLACES this note with a generic one, so it must be restored by hand after every use \u2014 the ceiling rationale is the only record of which prerequisite justifies each skip. FLOOR 365 -> 366 on 2026-08-22 (heartbeat-ordering work, PR #18): exactly one new test, test_capabilities.test_no_tick_producer_runs_above_the_heartbeat_export. No ceiling moved and nothing new is skipped -- it reads source files rather than a populated ledger, so it runs on any machine. The branch recorded 354 because it was cut before #16 merged; re-measured on the MERGE RESULT per the rule above, which is exactly the mistake that put the floor 8 below reality last time. FLOOR 366 -> 368 on 2026-08-23: main collected 368 while this file recorded 366, drift left by #34 (evidence-acquisition landed, +1) and #37 (tick capability evidence, +1) whose authors each measured against a branch cut before the other merged. A floor BELOW reality is the permissive direction this file exists to close -- those two could have silently stopped being collected and still cleared the check. Measured on the merge result per the rule above: 368 passed, 0 failed, 0 skipped, 83/83 selftests, 43/43 can-fire, 5/5 gates. CEILING 24 -> 26 and FLOOR 368 -> 387 on 2026-08-23 (profiles/provenance branch, PR #42). This file CONFLICTED with #50, which raised the floor 366 -> 368 on main while this branch raised it to 387; resolved as the UNION rather than by taking a side -- #50's rationale is retained above and the count was RE-MEASURED on the new merge result instead of keeping either number. 368 (main) + 19 (this branch's net new tests) = 387; #50 corrected recorded drift rather than adding coverage, which is why 387 is unchanged from the pre-conflict measurement. Measured in a runner sandbox reproducing CI exactly (361 passed, 26 skipped, 387 collected) AND on the owner's machine (387 passed, 0 skipped, 5/5 gates). The two new skips are drift detectors against a REAL installed agent runtime, so neither can be moved below the ceiling -- the preferred way to lower one: (1) agy advertised-models cache absent, since comparing declared model ids against the catalogue agy actually advertises needs that catalogue, and a fixture would exercise the comparison while detecting no real drift; (2) vibe config absent (~/.vibe/config.toml), since active_model cannot be read to check for drift when there is no config to read. Both name their missing prerequisite, so a green run still states what it did not check. A third candidate skip was REFUSED: dispatcher's per-run agy-log assertion failed on a bare runner because adapters.advertised_models shells out to `agy models` when its disk cache is cold, and that probe landed inside a monkeypatched subprocess.run and overwrote the captured command. That is a stub leak, so it was fixed by ISOLATING the double rather than by skipping -- which makes CI run MORE. FLOOR 387 -> 391 on 2026-08-23 (improvement-log accessor, PR #59): exactly four new tests, all in test_improvement_log.py -- three read tracked files in the tree (the pointer's size and content, and that CLAUDE.md 0 step 3 and 5 name the accessor rather than a bare path) and one runs the accessor as a subprocess against a path that cannot exist. None reads a populated ledger, an agent CLI or ~/.codex, so all four RUN on a bare runner and NO ceiling moved: nothing new is skipped. Measured on the MERGE RESULT after rebasing onto origin/main af6654d, which collected 387 -- not on the branch base, per the rule above. FLOOR 391 -> 402 on 2026-08-23 (Gate python-ci configuration, the PR that adds the missing .github/workflows/autofix-versions.env): exactly 11 new tests, all in test_ci_gate_config.py, which read committed files only -- the pin file, ruff.toml, mypy.ini, pr-00-gate.yml's toggle annotations and docs/CI_LINT_BASELINE.md. NO ceiling moved. On any CHECKOUT -- CI, the owner's tree, a second instance -- all 11 run: they need no installed linter and no populated ledger. In the EXEC-MIRROR layout all 11 skip with one named reason, because orch-sync-mirror.sh copies root-level *.py only, so .github/workflows, docs/ and scripts/ are genuinely absent there (env_prereq.repo_files_absent). That lands at 11/26 on a machine that otherwise skips nothing, and CI stays at 26/26, so no ceiling needed raising. The skip gate is the presence of those DIRECTORIES, never of the pin file itself -- gating on the file would have made the test that checks for it unable to fail. Measured on the merge result, twice: the branch was rebuilt on origin/main after #42 and #59 merged, and re-measured after #61 merged and was merged in -- 393 passed + 9 skipped = 402 collected both times, so #61 added no collected tests and this floor is not sitting below reality. #61 itself left main's floor at 391, which is exactly main-without-these-11, so there is no inherited drift to correct. RULE CHANGE 2026-08-23: `collected` is now an EQUALITY, not a minimum. Every floor entry above this one records the number being found BELOW reality and hand-raised after the fact -- 21 low at the worst, then 8, then 1, then 2 -- because nothing ever required a test-adding PR to touch this file, so the permissive direction was silent by construction and the rule 'measure on the merge result' had to be restated three times with nothing enforcing it. verify.py now FAILS when collected exceeds the floor, printing the two integers to write. That also makes the concurrency case self-enforcing: once every test-adding branch must edit these same two lines, two concurrent branches CONFLICT IN GIT, so the second cannot merge without rebasing onto the first and re-measuring on the actual merge result. Demonstrated repeatedly on the change itself: six merges landed on main in the two hours it took to write, moving this file 368 -> 387 -> 391 -> 402, and every one would have left the floor below reality under the old one-directional rule. `passed` deliberately stays a MINIMUM on passed+skipped: only collection is machine-invariant (a skipped test is still collected), measured across machines at 391 collected on both, with pass/skip splits of 365/26 on CI against 391/0 locally. The *_max ceilings are untouched by this change and nothing new is skipped. `--update-floor` also stops REPLACING this note -- it appends -- so the warning above about restoring it by hand no longer applies; and drift does NOT block --update-floor, since a gate that forbade its own only remedy would be a deadlock (the first draft was exactly that). FLOOR 402 -> 407 on 2026-08-23 (findability admission requirement). (findability admission requirement). (findability admission requirement). (findability admission requirement). Exactly five new pytest tests, all in test_capability_admission.py: test_findability_distinguishes_its_three_sub_causes, test_findability_blocks_new_capabilities_and_reports_older_ones_as_debt, test_unreadable_reach_is_not_evaluated_and_never_a_failure, test_findability_exemption_is_declared_in_code_not_in_a_live_ledger, test_consult_sites_are_falsifiable_claims_about_real_callers. NO CEILING MOVED and nothing new skips: all five build synthetic ledgers in a tempdir or read committed tables, so none needs a populated capability ledger, an agent CLI or ~/.claude/skills. The one machine-dependent thing they touch -- an external consult site declared in capability_advisor.CONSULT_SITES whose skill prompt is not on this machine -- is reported as UNVERIFIED rather than skipped, because absence of the caller is not refutation of the claim; the in-tree site (tick) is asserted verified on every machine so the check can never degrade into 'everything unverified, nothing tested'. Measured on the merge result per the rule above: this file CONFLICTED three times while the branch was open, as main went 387 -> 391 -> 402 (#61, #64, #65, #60). Each time it was resolved as the UNION rather than by taking a side, and the count was RE-MEASURED on the new merge result rather than either number being carried forward: 402 (main at bd6da2e) + 5 (this branch's new tests) = 407. That is the rule this file already states -- measure the floor on the merge result, not on the branch -- and it mattered here, because #60 both deleted test_ci_gate_config.py and added more than it removed, so guessing in either direction would have been wrong. -> re-measured on 2026-08-23 (PR #62, the four deferred #42 review findings): three new tests, all machine-independent (each builds its own tmp_path Brain and manifests), so NO CEILING MOVED and nothing new is skipped. Fourth conflict for this branch, and the first one under the EQUALITY -- which is the point: the equality's own rationale says git conflict detection is what enforces 'measure on the merge result', and that is exactly what happened here. Under the old minimum the three earlier conflicts could each have been resolved by keeping the larger number; under the equality the count MUST be measured, and it was. RESOLVED AGAINST #68 (findability admission requirement) on 2026-08-23: taken as the UNION per the rule this file states -- #68's five-test entry is retained above and this branch's three-test entry beside it -- and the count RE-MEASURED on the merge result rather than keeping either side's number. main fc1fd42 collects 407; this branch adds 3; 410 measured with `pytest --collect-only -q` on the merge result, not assumed. Ceilings untouched at 26/7/2 and nothing new is skipped. Also resolved in the same merge: langsmith-fleet-worker-attempt.json, a CI-emitted `langsmith-fleet/v1` worker-attempt record whose two sides differed only in `emitted_at` and `pr_number` (62 here, 68 on main). Main's NEWER record was kept rather than this branch's older one -- discarding a newer provenance observation to win a merge would corrupt exactly the causal-provenance evidence CLAUDE.md 2 protects, and this branch's own run re-emits its record anyway. FLOOR 410 -> 411 on 2026-08-23 (CodeRabbit follow-up on PR #42, thread 3837879039; re-measured again after #56 made `collected` an EQUALITY, which makes an assumed number a hard RED rather than a quiet pass -- main stayed at 402 across #56, and the merge result measures 403, so #56 added no collected tests and this is main's 402 plus this branch's one): exactly one new test, test_feedback_model_provenance.test_late_sweep_completes_terminal_attempts_never_one_in_flight, which pins that ledger_reconcile.resolve_unresolved_worker_attempts completes only TERMINAL unresolved worker attempts and never one still in flight. No ceiling moved and nothing new is skipped -- the test builds its own tmp ledger and codex rollout fixture and monkeypatches adapters.CODEX_SESSIONS, so it needs no agent CLI and no populated capability ledger and runs on a bare runner. RESOLVED AGAINST #59 (improvement-log accessor), which raised the floor 387 -> 391 on main while this branch raised it to 388: taken as the UNION -- #59's rationale is retained above and the count was RE-MEASURED on the new merge result rather than keeping either number, which is the rule this file states and the mistake that once put the floor 8 below reality. 391 (main, incl. #59's four tests) + 1 (this branch's one new test) = 392 measured, not assumed: 392 passed, 0 failed, 0 skipped, 83/83 selftests, 43/43 can-fire, 5/5 gates. Three sibling follow-up branches are in flight against this same main (CI/ruff config, arm-attribution + durability, adapters label->ID); if this file conflicts with one of them, resolve as the UNION and RE-MEASURE on the new merge result rather than taking either number -- that is what #42 and #50 did, and taking a side is what put the floor 8 below reality earlier. RESOLVED AGAINST #68 (findability admission requirement) on 2026-08-23: taken as the UNION per the rule this file states -- #68's five-test entry is retained above and this branch's one-test entry beside it -- and the count RE-MEASURED on the merge result. main fc1fd42 collects 407; this branch adds 1; 408 measured with `pytest --collect-only -q` on the merge result, not assumed. Ceilings untouched at 26/7/2 and nothing new is skipped -- the one new test builds its own tmp ledger and codex rollout fixture, so it runs on a bare runner. Also resolved in the same merge: langsmith-fleet-worker-attempt.json, a CI-emitted `langsmith-fleet/v1` worker-attempt record differing only in `emitted_at` and `pr_number`; main's NEWER record was kept, since discarding a newer provenance observation to win a merge would corrupt the causal-provenance evidence CLAUDE.md 2 protects. FLOOR 411 -> 415 on 2026-08-23 (PR #70 diagnostics salvage): four new collected tests in test_capability_set_coverage.py from the PR #43 salvage plus CodeRabbit follow-ups on PR #51/#70 \u2014 union/missing-candidate fetch command, truncation after six modules, AST-scoped gate-call audit, and entrypoint-diagnosis coverage. NO CEILING MOVED and nothing new is skipped; all inject synthetic ledgers or read committed source. Measured on the merge result at 91d37fa: 389 passed + 26 skipped = 415 collected on CI, not assumed. FLOOR 415 -> 416 on 2026-08-23 (the dangling-citation follow-up, PR #74): exactly ONE new test, test_ci_gate_config.test_every_cited_repo_path_resolves, which reads the two committed config files this repo OWNS (the pin file and ruff.toml) and asserts every repo-relative path they cite exists. It exists because the pin file shipped citing docs/ci/LINT_BASELINE.md when the real path was docs/CI_LINT_BASELINE.md: the sibling checks read that file's CONTENTS thoroughly and its PROSE not at all, and the prose is the only pointer telling a reader where to re-measure before bumping a pin. Scoped to the two owned files deliberately -- scanning pr-00-gate.yml yields six findings that are all correct as written (guarded by hashFiles or a .agents check, or upstream paths), and a test that cries wolf gets waived. NO ceiling moved. RE-MEASURED SIX TIMES as the base moved under this ONE-LINE change: bd6da2e 402 -> ddb0928 402 -> fc1fd42 407 -> 0d661e3 407 -> 0593eeb 411 -> 6fed4ad 415, each +1 with this test, and the branch was rebuilt on each rather than the number carried forward. THIS BRANCH IS THE WORKED EXAMPLE of the equality's concurrency cost, so record it rather than rediscover it: main moved EIGHT times in the ~2.5 hours a one-line comment fix was open (#56, #68, #73, #69, #62, #70 and two direct commits), the floor line conflicted THREE separate times, and two merges overlapped the change directly -- #73 landed a byte-identical copy of the backplane-conformance.yml guard this branch also carried (dropped as redundant), and #69 edited this very test file in a neighbouring region. The equality is still the right call and should stay: every entry above this one records the floor being found BELOW reality, which is the permissive direction. But no amount of author care wins this race, because the correct value is only knowable on the merge result. The durable fix is CI running `verify.py --update-floor` on the merge commit, which keeps the equality and removes the race; until then a test-adding PR must be merged promptly after going green, because it re-conflicts on roughly every subsequent merge. FLOOR 416 -> 427 on 2026-08-23 (PR #72 hygiene untrack, rebased after #71 merged): exactly 11 new tests from test_repo_artifact_hygiene.py with root-anchored gitignore patterns. NO ceiling moved. Measured on merge result after #71 landed on main: 416 (main) + 11 = 427 collected via pytest --collect-only -q, not assumed. #71's simpler untrack landed first; this branch carries the full hygiene test suite and corrected root-anchored patterns. FLOOR 427 -> 428 on 2026-08-23 (PR salvaging #34/#42 remnants): exactly one new test, test_feedback_model_provenance.test_gemini_provenance_reads_the_per_run_log_before_the_conversation_store, recovered from #42's post-merge commit 4e0d6ae along with the adapters catalog work it exercises. No ceiling moved and nothing new is skipped -- it seeds adapters._ADVERTISED_MEMO instead of letting the catalog probe shell out, so it runs on any machine and adds no prerequisite. `passed` is 428 rather than the 426 verify.py suggested on this machine: two test_capabilities liveness tests (test_gate_blocks_execution_is_opt_in_and_narrow, test_evidence_gate_kind_is_not_blanket_observer) currently fail HERE on pristine main as well, because the hourly fleet tick mutated the machine-local ledger and range-lane-rollout now classifies matched_not_invoked rather than deliberately_gated. That is ledger STATE, not this branch and not the code -- CI bootstraps an empty ledger and counts 428/428. Recording 426 would have baked a local environment failure into the floor as though it were the expected result. FLOOR 428 -> 441 on 2026-08-23 (coverage measures what actually runs): exactly 12 new tests, all in test_verify_coverage_mode.py. They read committed files and verify.py's own source, and monkeypatch verify.COVERAGE in-process -- no populated ledger, no agent CLI, no ~/.codex, and no coverage RUN -- so all 12 execute on any machine and NO ceiling moved. The change itself is a measurement fix, not a gate: `verify.py --coverage` wraps each child in `coverage run --parallel-mode` and combines, because the per-module --selftest is a SUBPROCESS and a pytest-only coverage run cannot see it. That blind spot was most of the codebase -- 78 modules have no test_*.py at all, ~85,500 lines, 79.6% of non-test root Python -- so the reported 48.45% was measuring the gap in the instrument, not a gap in the tests. Combined: 76.1% (45,049 statements, 10,774 missed). Twelve of the twelve modules the old report named as worst were selftest-only; outcomes.py reported 9.0% and measures 61.9%, watch.py 9.6% -> 86.9%. Coverage is OFF by default and deliberately never touches the exit code -- one of the 12 tests pins that, because enforcing a threshold here would reward pytest wrappers around already-tested modules: metric up, assurance flat. Measured on the MERGE RESULT: branched from origin/main bcc68cd (floor 427), then REBASED onto f5f1c39 when it landed underneath and re-measured on the new merge result rather than carrying the old number: 428 + 13 = 441. +1 on 2026-08-23 (440 -> 441 after the rebase), same branch: test_the_cli_help_actually_renders. It exists because this branch BROKE `verify.py --help` and its own twelve tests did not notice. argparse interpolates help strings with `% params`, so the literal `~80%` in the --coverage help was read as an `%o` octal conversion and --help died with 'badly formed help string'. All twelve original tests passed: every one inspected source text or monkeypatched a flag, and not one RENDERED the help -- a construction-time test suite that never exercised the constructed thing, which is this repo's founding defect one layer up. CI's verify.py gate caught it, which is the check of last resort working as intended. The new test runs `verify.py --help` as a subprocess and asserts it exits 0, so it runs anywhere and NO ceiling moved. Rendering rather than grepping for `%` is deliberate: a grep would flag the legitimate `%(default)s`. FLOOR 441 -> 442 on 2026-08-23 (matched_not_invoked yields to observers and declared gates): exactly one new test, test_capabilities.test_matched_not_invoked_yields_to_observers_and_declared_gates. It is SYNTHETIC on purpose and that is the point of it: the two tests that caught this bug in the wild read the LIVE ledger, so they skipped with a named reason on the empty ledger ci.yml bootstraps -- the defect was red on every populated machine and green on CI for as long as it existed. A synthetic row asks the same question everywhere, so this one RUNS on a bare runner and NO ceiling moved. The fix itself moves matched_not_invoked below `observing` and below the DECLARED deliberately_gated check in classify_liveness: it was the first check, which made it the fourth instance of the unescapable label the comments in that function exist to fix. Audited before committing -- 12 of 43 live rows reclassify (ten observers to observing, two declared gates to deliberately_gated) and ZERO move for any other reason, so nothing is reclassified by inference and the weaker gate_reason-only branch is untouched. Measured on origin/main 0d9c3a7, whose recorded floor is 441, so 441 + 1 = 442. FLOOR RECORDED by verify.py --update-floor on 2026-08-23: collected=442, passed=442. Ceilings preserved, never re-measured \u2014 they are edited by hand. FLOOR RECORDED by verify.py --update-floor on 2026-08-24: collected=442, passed=442. Ceilings preserved, never re-measured \u2014 they are edited by hand. FLOOR RECORDED by verify.py --update-floor on 2026-08-24: collected=442, passed=442. Ceilings preserved, never re-measured \u2014 they are edited by hand. MYPY_EXEMPT_MAX introduced at 64 on 2026-08-23, the same change that flipped `typecheck` ON in pr-00-gate.yml. It was OFF because 608 whole-tree errors were drainable 0 per PR -- a gate whose clear path is blocked by the thing it measures. Two real changes opened it: the src/ move scoped the Gate's `target=src` to the 99 modules (608 -> 467), and pyproject.toml's [[tool.mypy.overrides]] exempts the modules that still have findings BY NAME so the 35 already-clean ones are checked today. 12 var-annotated findings were then drained (467 -> 455, 66 -> 64 modules) to prove the drain works rather than promise it. This ceiling may only ever be LOWERED, by typing a module and deleting its line -- raising it means agreeing one more module goes unchecked, so say which and why. NOT a suppression: no error code is disabled anywhere and the 455 stay visible via `python3 scripts/ci_lint_baseline.py`. Edited BY HAND like the other *_max values; --update-floor never re-measures it. MYPY_EXEMPT_MAX LOWERED 64 -> 43 on 2026-08-24, the ratchet's first real drain: 21 modules typed clean and removed from the exempt list, 455 -> 430 findings. Targeted the modules with 1-3 errors on purpose -- fixing 50 errors spread across the big modules would move this number by ZERO, and this number is what the gate reads. Lowering it is the drain; it may never be raised without naming the module and why.", - "mypy_exempt_max": 43 + "note": "Recorded by verify.py --update-floor, except the *_max ceilings, which are edited BY HAND and never re-measured. `collected` catches tests that stopped being collected; `passed` is compared against passed+skipped, so a check may move between passing and consciously-skipped but the two together may never shrink. The *_max ceilings bound the skipped side: 24/7/2 is exactly what a machine with none of this instance's local prerequisites skips (a GitHub runner: no agent CLIs, no ~/.codex/skills, no /Applications/ChatGPT.app, no populated capability ledger), measured 2026-08-21. On the owner's machine all prerequisites exist and nothing skips at all. Raising a ceiling is a deliberate act: it means agreeing that one more thing is allowed to go unchecked, so say which and why in the commit. LOWERED 26 -> 24 on 2026-08-22, reverting the raise made earlier the same day. The two kill-switch exemption tests no longer need to skip on a bare runner: their declarations moved out of the running instance's ledger and into capabilities.KNOWN_DECLARATIONS, so they assert code-derived truth and run everywhere. Moving a test back below the ceiling is the preferred way to lower it -- fix what made it machine-dependent, rather than agreeing to check less. FLOOR 345 -> 353 on 2026-08-22: 345 was measured on a branch cut before #13 (research panels/rounds/domain studies) merged, so the recorded floor sat 8 tests BELOW what main actually collects. A floor below reality is the permissive direction -- those 8 could have silently stopped being collected and still cleared the check, which is exactly the hole this file exists to close. Measure the floor on the merge result, not on the branch. Raised again on 2026-08-22 by the producer-identity-scope branch, which adds tests on top of the 353 recorded by #15; re-measured after rebasing rather than assumed. NOTE: `verify.py --update-floor` REPLACES this note with a generic one, so it must be restored by hand after every use \u2014 the ceiling rationale is the only record of which prerequisite justifies each skip. FLOOR 365 -> 366 on 2026-08-22 (heartbeat-ordering work, PR #18): exactly one new test, test_capabilities.test_no_tick_producer_runs_above_the_heartbeat_export. No ceiling moved and nothing new is skipped -- it reads source files rather than a populated ledger, so it runs on any machine. The branch recorded 354 because it was cut before #16 merged; re-measured on the MERGE RESULT per the rule above, which is exactly the mistake that put the floor 8 below reality last time. FLOOR 366 -> 368 on 2026-08-23: main collected 368 while this file recorded 366, drift left by #34 (evidence-acquisition landed, +1) and #37 (tick capability evidence, +1) whose authors each measured against a branch cut before the other merged. A floor BELOW reality is the permissive direction this file exists to close -- those two could have silently stopped being collected and still cleared the check. Measured on the merge result per the rule above: 368 passed, 0 failed, 0 skipped, 83/83 selftests, 43/43 can-fire, 5/5 gates. CEILING 24 -> 26 and FLOOR 368 -> 387 on 2026-08-23 (profiles/provenance branch, PR #42). This file CONFLICTED with #50, which raised the floor 366 -> 368 on main while this branch raised it to 387; resolved as the UNION rather than by taking a side -- #50's rationale is retained above and the count was RE-MEASURED on the new merge result instead of keeping either number. 368 (main) + 19 (this branch's net new tests) = 387; #50 corrected recorded drift rather than adding coverage, which is why 387 is unchanged from the pre-conflict measurement. Measured in a runner sandbox reproducing CI exactly (361 passed, 26 skipped, 387 collected) AND on the owner's machine (387 passed, 0 skipped, 5/5 gates). The two new skips are drift detectors against a REAL installed agent runtime, so neither can be moved below the ceiling -- the preferred way to lower one: (1) agy advertised-models cache absent, since comparing declared model ids against the catalogue agy actually advertises needs that catalogue, and a fixture would exercise the comparison while detecting no real drift; (2) vibe config absent (~/.vibe/config.toml), since active_model cannot be read to check for drift when there is no config to read. Both name their missing prerequisite, so a green run still states what it did not check. A third candidate skip was REFUSED: dispatcher's per-run agy-log assertion failed on a bare runner because adapters.advertised_models shells out to `agy models` when its disk cache is cold, and that probe landed inside a monkeypatched subprocess.run and overwrote the captured command. That is a stub leak, so it was fixed by ISOLATING the double rather than by skipping -- which makes CI run MORE. FLOOR 387 -> 391 on 2026-08-23 (improvement-log accessor, PR #59): exactly four new tests, all in test_improvement_log.py -- three read tracked files in the tree (the pointer's size and content, and that CLAUDE.md 0 step 3 and 5 name the accessor rather than a bare path) and one runs the accessor as a subprocess against a path that cannot exist. None reads a populated ledger, an agent CLI or ~/.codex, so all four RUN on a bare runner and NO ceiling moved: nothing new is skipped. Measured on the MERGE RESULT after rebasing onto origin/main af6654d, which collected 387 -- not on the branch base, per the rule above. FLOOR 391 -> 402 on 2026-08-23 (Gate python-ci configuration, the PR that adds the missing .github/workflows/autofix-versions.env): exactly 11 new tests, all in test_ci_gate_config.py, which read committed files only -- the pin file, ruff.toml, mypy.ini, pr-00-gate.yml's toggle annotations and docs/CI_LINT_BASELINE.md. NO ceiling moved. On any CHECKOUT -- CI, the owner's tree, a second instance -- all 11 run: they need no installed linter and no populated ledger. In the EXEC-MIRROR layout all 11 skip with one named reason, because orch-sync-mirror.sh copies root-level *.py only, so .github/workflows, docs/ and scripts/ are genuinely absent there (env_prereq.repo_files_absent). That lands at 11/26 on a machine that otherwise skips nothing, and CI stays at 26/26, so no ceiling needed raising. The skip gate is the presence of those DIRECTORIES, never of the pin file itself -- gating on the file would have made the test that checks for it unable to fail. Measured on the merge result, twice: the branch was rebuilt on origin/main after #42 and #59 merged, and re-measured after #61 merged and was merged in -- 393 passed + 9 skipped = 402 collected both times, so #61 added no collected tests and this floor is not sitting below reality. #61 itself left main's floor at 391, which is exactly main-without-these-11, so there is no inherited drift to correct. RULE CHANGE 2026-08-23: `collected` is now an EQUALITY, not a minimum. Every floor entry above this one records the number being found BELOW reality and hand-raised after the fact -- 21 low at the worst, then 8, then 1, then 2 -- because nothing ever required a test-adding PR to touch this file, so the permissive direction was silent by construction and the rule 'measure on the merge result' had to be restated three times with nothing enforcing it. verify.py now FAILS when collected exceeds the floor, printing the two integers to write. That also makes the concurrency case self-enforcing: once every test-adding branch must edit these same two lines, two concurrent branches CONFLICT IN GIT, so the second cannot merge without rebasing onto the first and re-measuring on the actual merge result. Demonstrated repeatedly on the change itself: six merges landed on main in the two hours it took to write, moving this file 368 -> 387 -> 391 -> 402, and every one would have left the floor below reality under the old one-directional rule. `passed` deliberately stays a MINIMUM on passed+skipped: only collection is machine-invariant (a skipped test is still collected), measured across machines at 391 collected on both, with pass/skip splits of 365/26 on CI against 391/0 locally. The *_max ceilings are untouched by this change and nothing new is skipped. `--update-floor` also stops REPLACING this note -- it appends -- so the warning above about restoring it by hand no longer applies; and drift does NOT block --update-floor, since a gate that forbade its own only remedy would be a deadlock (the first draft was exactly that). FLOOR 402 -> 407 on 2026-08-23 (findability admission requirement). (findability admission requirement). (findability admission requirement). (findability admission requirement). Exactly five new pytest tests, all in test_capability_admission.py: test_findability_distinguishes_its_three_sub_causes, test_findability_blocks_new_capabilities_and_reports_older_ones_as_debt, test_unreadable_reach_is_not_evaluated_and_never_a_failure, test_findability_exemption_is_declared_in_code_not_in_a_live_ledger, test_consult_sites_are_falsifiable_claims_about_real_callers. NO CEILING MOVED and nothing new skips: all five build synthetic ledgers in a tempdir or read committed tables, so none needs a populated capability ledger, an agent CLI or ~/.claude/skills. The one machine-dependent thing they touch -- an external consult site declared in capability_advisor.CONSULT_SITES whose skill prompt is not on this machine -- is reported as UNVERIFIED rather than skipped, because absence of the caller is not refutation of the claim; the in-tree site (tick) is asserted verified on every machine so the check can never degrade into 'everything unverified, nothing tested'. Measured on the merge result per the rule above: this file CONFLICTED three times while the branch was open, as main went 387 -> 391 -> 402 (#61, #64, #65, #60). Each time it was resolved as the UNION rather than by taking a side, and the count was RE-MEASURED on the new merge result rather than either number being carried forward: 402 (main at bd6da2e) + 5 (this branch's new tests) = 407. That is the rule this file already states -- measure the floor on the merge result, not on the branch -- and it mattered here, because #60 both deleted test_ci_gate_config.py and added more than it removed, so guessing in either direction would have been wrong. -> re-measured on 2026-08-23 (PR #62, the four deferred #42 review findings): three new tests, all machine-independent (each builds its own tmp_path Brain and manifests), so NO CEILING MOVED and nothing new is skipped. Fourth conflict for this branch, and the first one under the EQUALITY -- which is the point: the equality's own rationale says git conflict detection is what enforces 'measure on the merge result', and that is exactly what happened here. Under the old minimum the three earlier conflicts could each have been resolved by keeping the larger number; under the equality the count MUST be measured, and it was. RESOLVED AGAINST #68 (findability admission requirement) on 2026-08-23: taken as the UNION per the rule this file states -- #68's five-test entry is retained above and this branch's three-test entry beside it -- and the count RE-MEASURED on the merge result rather than keeping either side's number. main fc1fd42 collects 407; this branch adds 3; 410 measured with `pytest --collect-only -q` on the merge result, not assumed. Ceilings untouched at 26/7/2 and nothing new is skipped. Also resolved in the same merge: langsmith-fleet-worker-attempt.json, a CI-emitted `langsmith-fleet/v1` worker-attempt record whose two sides differed only in `emitted_at` and `pr_number` (62 here, 68 on main). Main's NEWER record was kept rather than this branch's older one -- discarding a newer provenance observation to win a merge would corrupt exactly the causal-provenance evidence CLAUDE.md 2 protects, and this branch's own run re-emits its record anyway. FLOOR 410 -> 411 on 2026-08-23 (CodeRabbit follow-up on PR #42, thread 3837879039; re-measured again after #56 made `collected` an EQUALITY, which makes an assumed number a hard RED rather than a quiet pass -- main stayed at 402 across #56, and the merge result measures 403, so #56 added no collected tests and this is main's 402 plus this branch's one): exactly one new test, test_feedback_model_provenance.test_late_sweep_completes_terminal_attempts_never_one_in_flight, which pins that ledger_reconcile.resolve_unresolved_worker_attempts completes only TERMINAL unresolved worker attempts and never one still in flight. No ceiling moved and nothing new is skipped -- the test builds its own tmp ledger and codex rollout fixture and monkeypatches adapters.CODEX_SESSIONS, so it needs no agent CLI and no populated capability ledger and runs on a bare runner. RESOLVED AGAINST #59 (improvement-log accessor), which raised the floor 387 -> 391 on main while this branch raised it to 388: taken as the UNION -- #59's rationale is retained above and the count was RE-MEASURED on the new merge result rather than keeping either number, which is the rule this file states and the mistake that once put the floor 8 below reality. 391 (main, incl. #59's four tests) + 1 (this branch's one new test) = 392 measured, not assumed: 392 passed, 0 failed, 0 skipped, 83/83 selftests, 43/43 can-fire, 5/5 gates. Three sibling follow-up branches are in flight against this same main (CI/ruff config, arm-attribution + durability, adapters label->ID); if this file conflicts with one of them, resolve as the UNION and RE-MEASURE on the new merge result rather than taking either number -- that is what #42 and #50 did, and taking a side is what put the floor 8 below reality earlier. RESOLVED AGAINST #68 (findability admission requirement) on 2026-08-23: taken as the UNION per the rule this file states -- #68's five-test entry is retained above and this branch's one-test entry beside it -- and the count RE-MEASURED on the merge result. main fc1fd42 collects 407; this branch adds 1; 408 measured with `pytest --collect-only -q` on the merge result, not assumed. Ceilings untouched at 26/7/2 and nothing new is skipped -- the one new test builds its own tmp ledger and codex rollout fixture, so it runs on a bare runner. Also resolved in the same merge: langsmith-fleet-worker-attempt.json, a CI-emitted `langsmith-fleet/v1` worker-attempt record differing only in `emitted_at` and `pr_number`; main's NEWER record was kept, since discarding a newer provenance observation to win a merge would corrupt the causal-provenance evidence CLAUDE.md 2 protects. FLOOR 411 -> 415 on 2026-08-23 (PR #70 diagnostics salvage): four new collected tests in test_capability_set_coverage.py from the PR #43 salvage plus CodeRabbit follow-ups on PR #51/#70 \u2014 union/missing-candidate fetch command, truncation after six modules, AST-scoped gate-call audit, and entrypoint-diagnosis coverage. NO CEILING MOVED and nothing new is skipped; all inject synthetic ledgers or read committed source. Measured on the merge result at 91d37fa: 389 passed + 26 skipped = 415 collected on CI, not assumed. FLOOR 415 -> 416 on 2026-08-23 (the dangling-citation follow-up, PR #74): exactly ONE new test, test_ci_gate_config.test_every_cited_repo_path_resolves, which reads the two committed config files this repo OWNS (the pin file and ruff.toml) and asserts every repo-relative path they cite exists. It exists because the pin file shipped citing docs/ci/LINT_BASELINE.md when the real path was docs/CI_LINT_BASELINE.md: the sibling checks read that file's CONTENTS thoroughly and its PROSE not at all, and the prose is the only pointer telling a reader where to re-measure before bumping a pin. Scoped to the two owned files deliberately -- scanning pr-00-gate.yml yields six findings that are all correct as written (guarded by hashFiles or a .agents check, or upstream paths), and a test that cries wolf gets waived. NO ceiling moved. RE-MEASURED SIX TIMES as the base moved under this ONE-LINE change: bd6da2e 402 -> ddb0928 402 -> fc1fd42 407 -> 0d661e3 407 -> 0593eeb 411 -> 6fed4ad 415, each +1 with this test, and the branch was rebuilt on each rather than the number carried forward. THIS BRANCH IS THE WORKED EXAMPLE of the equality's concurrency cost, so record it rather than rediscover it: main moved EIGHT times in the ~2.5 hours a one-line comment fix was open (#56, #68, #73, #69, #62, #70 and two direct commits), the floor line conflicted THREE separate times, and two merges overlapped the change directly -- #73 landed a byte-identical copy of the backplane-conformance.yml guard this branch also carried (dropped as redundant), and #69 edited this very test file in a neighbouring region. The equality is still the right call and should stay: every entry above this one records the floor being found BELOW reality, which is the permissive direction. But no amount of author care wins this race, because the correct value is only knowable on the merge result. The durable fix is CI running `verify.py --update-floor` on the merge commit, which keeps the equality and removes the race; until then a test-adding PR must be merged promptly after going green, because it re-conflicts on roughly every subsequent merge. FLOOR 416 -> 427 on 2026-08-23 (PR #72 hygiene untrack, rebased after #71 merged): exactly 11 new tests from test_repo_artifact_hygiene.py with root-anchored gitignore patterns. NO ceiling moved. Measured on merge result after #71 landed on main: 416 (main) + 11 = 427 collected via pytest --collect-only -q, not assumed. #71's simpler untrack landed first; this branch carries the full hygiene test suite and corrected root-anchored patterns. FLOOR 427 -> 428 on 2026-08-23 (PR salvaging #34/#42 remnants): exactly one new test, test_feedback_model_provenance.test_gemini_provenance_reads_the_per_run_log_before_the_conversation_store, recovered from #42's post-merge commit 4e0d6ae along with the adapters catalog work it exercises. No ceiling moved and nothing new is skipped -- it seeds adapters._ADVERTISED_MEMO instead of letting the catalog probe shell out, so it runs on any machine and adds no prerequisite. `passed` is 428 rather than the 426 verify.py suggested on this machine: two test_capabilities liveness tests (test_gate_blocks_execution_is_opt_in_and_narrow, test_evidence_gate_kind_is_not_blanket_observer) currently fail HERE on pristine main as well, because the hourly fleet tick mutated the machine-local ledger and range-lane-rollout now classifies matched_not_invoked rather than deliberately_gated. That is ledger STATE, not this branch and not the code -- CI bootstraps an empty ledger and counts 428/428. Recording 426 would have baked a local environment failure into the floor as though it were the expected result. FLOOR 428 -> 441 on 2026-08-23 (coverage measures what actually runs): exactly 12 new tests, all in test_verify_coverage_mode.py. They read committed files and verify.py's own source, and monkeypatch verify.COVERAGE in-process -- no populated ledger, no agent CLI, no ~/.codex, and no coverage RUN -- so all 12 execute on any machine and NO ceiling moved. The change itself is a measurement fix, not a gate: `verify.py --coverage` wraps each child in `coverage run --parallel-mode` and combines, because the per-module --selftest is a SUBPROCESS and a pytest-only coverage run cannot see it. That blind spot was most of the codebase -- 78 modules have no test_*.py at all, ~85,500 lines, 79.6% of non-test root Python -- so the reported 48.45% was measuring the gap in the instrument, not a gap in the tests. Combined: 76.1% (45,049 statements, 10,774 missed). Twelve of the twelve modules the old report named as worst were selftest-only; outcomes.py reported 9.0% and measures 61.9%, watch.py 9.6% -> 86.9%. Coverage is OFF by default and deliberately never touches the exit code -- one of the 12 tests pins that, because enforcing a threshold here would reward pytest wrappers around already-tested modules: metric up, assurance flat. Measured on the MERGE RESULT: branched from origin/main bcc68cd (floor 427), then REBASED onto f5f1c39 when it landed underneath and re-measured on the new merge result rather than carrying the old number: 428 + 13 = 441. +1 on 2026-08-23 (440 -> 441 after the rebase), same branch: test_the_cli_help_actually_renders. It exists because this branch BROKE `verify.py --help` and its own twelve tests did not notice. argparse interpolates help strings with `% params`, so the literal `~80%` in the --coverage help was read as an `%o` octal conversion and --help died with 'badly formed help string'. All twelve original tests passed: every one inspected source text or monkeypatched a flag, and not one RENDERED the help -- a construction-time test suite that never exercised the constructed thing, which is this repo's founding defect one layer up. CI's verify.py gate caught it, which is the check of last resort working as intended. The new test runs `verify.py --help` as a subprocess and asserts it exits 0, so it runs anywhere and NO ceiling moved. Rendering rather than grepping for `%` is deliberate: a grep would flag the legitimate `%(default)s`. FLOOR 441 -> 442 on 2026-08-23 (matched_not_invoked yields to observers and declared gates): exactly one new test, test_capabilities.test_matched_not_invoked_yields_to_observers_and_declared_gates. It is SYNTHETIC on purpose and that is the point of it: the two tests that caught this bug in the wild read the LIVE ledger, so they skipped with a named reason on the empty ledger ci.yml bootstraps -- the defect was red on every populated machine and green on CI for as long as it existed. A synthetic row asks the same question everywhere, so this one RUNS on a bare runner and NO ceiling moved. The fix itself moves matched_not_invoked below `observing` and below the DECLARED deliberately_gated check in classify_liveness: it was the first check, which made it the fourth instance of the unescapable label the comments in that function exist to fix. Audited before committing -- 12 of 43 live rows reclassify (ten observers to observing, two declared gates to deliberately_gated) and ZERO move for any other reason, so nothing is reclassified by inference and the weaker gate_reason-only branch is untouched. Measured on origin/main 0d9c3a7, whose recorded floor is 441, so 441 + 1 = 442. FLOOR RECORDED by verify.py --update-floor on 2026-08-23: collected=442, passed=442. Ceilings preserved, never re-measured \u2014 they are edited by hand. FLOOR RECORDED by verify.py --update-floor on 2026-08-24: collected=442, passed=442. Ceilings preserved, never re-measured \u2014 they are edited by hand. FLOOR RECORDED by verify.py --update-floor on 2026-08-24: collected=442, passed=442. Ceilings preserved, never re-measured \u2014 they are edited by hand. MYPY_EXEMPT_MAX introduced at 64 on 2026-08-23, the same change that flipped `typecheck` ON in pr-00-gate.yml. It was OFF because 608 whole-tree errors were drainable 0 per PR -- a gate whose clear path is blocked by the thing it measures. Two real changes opened it: the src/ move scoped the Gate's `target=src` to the 99 modules (608 -> 467), and pyproject.toml's [[tool.mypy.overrides]] exempts the modules that still have findings BY NAME so the 35 already-clean ones are checked today. 12 var-annotated findings were then drained (467 -> 455, 66 -> 64 modules) to prove the drain works rather than promise it. This ceiling may only ever be LOWERED, by typing a module and deleting its line -- raising it means agreeing one more module goes unchecked, so say which and why. NOT a suppression: no error code is disabled anywhere and the 455 stay visible via `python3 scripts/ci_lint_baseline.py`. Edited BY HAND like the other *_max values; --update-floor never re-measures it. MYPY_EXEMPT_MAX LOWERED 64 -> 43 on 2026-08-24, the ratchet's first real drain: 21 modules typed clean and removed from the exempt list, 455 -> 430 findings. Targeted the modules with 1-3 errors on purpose -- fixing 50 errors spread across the big modules would move this number by ZERO, and this number is what the gate reads. Lowering it is the drain; it may never be raised without naming the module and why. MYPY_EXEMPT_MAX LOWERED 43 -> 33 on 2026-08-24 (batch 2 of the drain): the whole <=5-finding tail typed clean. 66 of 99 modules now checked. What remains is five per-module campaigns (capability_advisor, dispatcher, capability_propensity, runtime_ac_gate, capabilities hold most of it), so future batches take ONE big module at a time rather than skimming. (batch 2 detail: 43 -> 26, 430 -> 374 findings, 73 of 99 modules checked. `mypy_path` gained `tests` so mypy can RESOLVE the recurrence-fixture roster capability_admission legitimately imports; the target stays `src`, but mypy then follows into that one test file, whose two findings were fixed rather than configured around.)", + "mypy_exempt_max": 26 } diff --git a/pyproject.toml b/pyproject.toml index ff87f57..698c9cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,10 +104,13 @@ explicit_package_bases = true # config rather than the code — 769 instead of 601 when first measured, all of the difference # spurious. The same reason `pythonpath = ["src"]` exists for pytest above: one relocation, two # tools that each need telling. -mypy_path = ["src"] +# `tests` as well as `src`: `capability_admission` reads the recurrence-fixture roster from +# `test_capability_set_coverage`, a real and declared dependency (see paths.TESTS_DIR). mypy_path +# affects RESOLUTION only — the check target stays `src`, so the tests are not themselves checked. +mypy_path = ["src", "tests"] -# THE RATCHET, and the reason the check can be ON at all. 56 of the 99 modules are already clean; -# the 43 below are not, and each is exempt BY NAME so the check can run TODAY over the +# THE RATCHET, and the reason the check can be ON at all. 73 of the 99 modules are already clean; +# the 26 below are not, and each is exempt BY NAME so the check can run TODAY over the # clean two-thirds instead of being off over everything. A blanket `ignore_errors` or a # `disable_error_code` list was rejected: both make the job green while checking nothing, which is # the defect verify.py exists to stop, and neither has anything to count. @@ -123,48 +126,31 @@ mypy_path = ["src"] # module is now a red, and a module can never quietly rejoin the exempt set. [[tool.mypy.overrides]] module = [ - "agent_auth_check", "capabilities", "capability_activation_audit", - "capability_admission", "capability_advisor", "capability_compiler", - "capability_effectiveness", - "capability_opportunity", "capability_outcome_bridge", "capability_propensity", "capability_recurrence_check", - "ccusage_reconcile", - "claims", "codemod_lane", "consumer_sync_artifact_ingest", - "cross_repo_lane", "dispatcher", "durability_sweep", "exp_abcd", - "exploration_backfill", - "exploration_collection", - "exploration_evidence_plan", "feedback", "issue_quality", "issue_readiness", "keepalive_evidence", "keepalive_outcomes", - "mcp_server", "observability_dashboard", - "pattern_miner", - "range_lane_rollout", - "redirect_apply", "redirect_sweep", "repo_knowledge", - "research_scheduler", - "research_subjects", "roles", "router", "runtime_ac", "runtime_ac_gate", "switch_review", "tick", - "verify", ] ignore_errors = true diff --git a/src/agent_auth_check.py b/src/agent_auth_check.py index 21d947d..6c64948 100644 --- a/src/agent_auth_check.py +++ b/src/agent_auth_check.py @@ -201,7 +201,7 @@ def _render(rows: list[dict]) -> str: def _selftest() -> None: - rows = [ + rows: list[dict] = [ { "agent": "a", "verdict": "OK", @@ -279,7 +279,10 @@ def _boom(self, *a, **k): raise OSError("Resource temporarily unavailable") return _real_read(self, *a, **k) - Path.read_text = _boom + # Deliberate monkeypatch inside the selftest: the point is to make a real read FAIL + # so the OSError path is exercised. `method-assign` is right in general and wrong + # here, so the exemption is narrow and stated rather than configured away. + Path.read_text = _boom # type: ignore[method-assign] _bad = _credential_file_state("vibe") assert _bad["key_present"] is None, _bad assert _bad["reason_class"] == "environment", _bad @@ -288,12 +291,12 @@ def _boom(self, *a, **k): assert CRED_READ_ATTEMPTS >= 2, "the retry is the point; one attempt is no retry" assert len(_calls) == CRED_READ_ATTEMPTS, (len(_calls), CRED_READ_ATTEMPTS) # A file that is genuinely ABSENT stays a hard failure and is NOT retried. - Path.read_text = _real_read + Path.read_text = _real_read # type: ignore[method-assign] CREDENTIAL_FILES["vibe"] = (Path(_td) / "nope.env", "MISTRAL_API_KEY") _gone = _credential_file_state("vibe") assert _gone["present"] is False and not _gone.get("reason_class"), _gone finally: - Path.read_text = _real_read + Path.read_text = _real_read # type: ignore[method-assign] if _saved is not None: CREDENTIAL_FILES["vibe"] = _saved diff --git a/src/capability_admission.py b/src/capability_admission.py index b76e161..71cbdde 100644 --- a/src/capability_admission.py +++ b/src/capability_admission.py @@ -473,7 +473,8 @@ def admit(capability_id: str, *, path: pathlib.Path | None = None, ctx: dict | N if cap is None: raise ValueError(f"unknown capability: {capability_id}") ctx = ctx or _context(path) - checks, missing = {}, [] + checks: dict[str, dict[str, Any]] = {} + missing: list[str] = [] for name, fn in REQUIREMENTS: try: ok, detail = fn(cap, ctx) @@ -537,7 +538,8 @@ def preflight(spec: dict) -> dict: "fixtures": set(), **_findability_context([stub["capability_id"]]), } - checks, missing = {}, [] + checks: dict[str, dict[str, Any]] = {} + missing: list[str] = [] # Caller/heartbeat/fixture cannot be verified for code that does not exist; they are reported as # OBLIGATIONS rather than silently skipped, because silently skipping is how they got skipped. obligations = {"caller_exists", "heartbeat", "fixture"} diff --git a/src/capability_effectiveness.py b/src/capability_effectiveness.py index 34d31c8..f85b435 100644 --- a/src/capability_effectiveness.py +++ b/src/capability_effectiveness.py @@ -35,6 +35,7 @@ import argparse import json import sys +from typing import Any import capabilities import feedback @@ -121,7 +122,7 @@ def _arm_stats(edges: list[dict]) -> dict: # Distinct subjects — a target counts as durable if ANY attempt on it landed durably. durable_subjects = {_target_of(e["run_id"]) for e in durable} terminal_subjects = {_target_of(e["run_id"]) for e in terminal} - out = { + out: dict[str, Any] = { "attributed": len(edges), "terminal": len(terminal), "durable": len(durable), diff --git a/src/capability_opportunity.py b/src/capability_opportunity.py index 928dc6f..7b93222 100644 --- a/src/capability_opportunity.py +++ b/src/capability_opportunity.py @@ -29,6 +29,7 @@ import json import os import sys +from collections.abc import Mapping import capabilities import feedback @@ -99,7 +100,12 @@ def _role_invocation_counts(conn=None) -> dict[str, int]: def assess( - cap_id: str, cap: dict, *, task_counts: dict, role_counts: dict, env: dict | None = None + cap_id: str, + cap: dict, + *, + task_counts: dict, + role_counts: dict, + env: Mapping[str, str] | None = None, ) -> dict: """One capability: its trigger, the work that matched it, and the resulting verdict.""" env = os.environ if env is None else env @@ -153,7 +159,7 @@ def assess( } -def report(*, path=None, env: dict | None = None) -> dict: +def report(*, path=None, env: Mapping[str, str] | None = None) -> dict: caps = capabilities.load(path or capabilities.REG) task_counts = _task_type_counts() role_counts = _role_invocation_counts() diff --git a/src/ccusage_reconcile.py b/src/ccusage_reconcile.py index 7a8fd1c..51f22fd 100644 --- a/src/ccusage_reconcile.py +++ b/src/ccusage_reconcile.py @@ -145,12 +145,12 @@ def _run_windows( skipped["unsupported_agent"] += 1 continue starts = [ - int(row.get("ts")) + int(row.get("ts") or 0) for row in run_rows if row.get("event") == "start" and isinstance(row.get("ts"), (int, float)) ] completes = [ - int(row.get("ts")) + int(row.get("ts") or 0) for row in run_rows if row.get("event") == "complete" and isinstance(row.get("ts"), (int, float)) ] @@ -237,7 +237,7 @@ def _match_window( if agent not in ATTRIBUTABLE_AGENTS: return None, "unsupported_agent" metadata = session.get("metadata") if isinstance(session.get("metadata"), dict) else {} - last_ts = _parse_iso_ts(metadata.get("lastActivity")) + last_ts = _parse_iso_ts((metadata or {}).get("lastActivity")) if last_ts is None: return None, "missing_last_activity" matches = [ diff --git a/src/claims.py b/src/claims.py index 8d66f7e..fe56d07 100644 --- a/src/claims.py +++ b/src/claims.py @@ -349,7 +349,8 @@ def _selftest() -> None: assert claim(T1, "claude") is False, "same-target collision must be blocked" assert claim(T2, "claude") is True assert claim(T1, "codex") is True, "idempotent same-agent re-claim" - assert holder(T1)["agent"] == "codex" + held = holder(T1) + assert held and held["agent"] == "codex" assert set(active_claims()) == {T1, T2}, active_claims() assert ( update_metadata(T1, "codex", lane="opener", task_type="implement", pid=os.getpid()) @@ -366,7 +367,8 @@ def _selftest() -> None: assert holder(T1) is None assert claim(T1, "claude") is True assert release(T1, "codex") is False, "wrong-agent release must be refused" - assert holder(T1)["agent"] == "claude" + held = holder(T1) + assert held and held["agent"] == "claude" # stale claim owned by 'codex' (dead pid, old ts) stale = _claims_dir() / _slug(T3) @@ -451,7 +453,10 @@ def _selftest() -> None: } ) ) - assert holder(T3)["agent"] == "research", "any live child pid keeps a research claim held" + held = holder(T3) + assert ( + held and held["agent"] == "research" + ), "any live child pid keeps a research claim held" release(T3) # no-meta TOCTOU guard: a fresh (unstamped) dir reads as HELD, not stale diff --git a/src/cross_repo_lane.py b/src/cross_repo_lane.py index ab7c416..1b13d4f 100644 --- a/src/cross_repo_lane.py +++ b/src/cross_repo_lane.py @@ -185,7 +185,7 @@ def validate_coordination(plan: dict[str, Any]) -> list[str]: coord_id = meta.get("id") if not _is_nonempty_string(coord_id): errors.append("coordination.id must be a non-empty string") - elif not _looks_like_slug(coord_id.strip()): + elif not _looks_like_slug(str(coord_id or "").strip()): errors.append("coordination.id must be a lowercase slug") for key in ("title", "goal", "source_repo"): if not _is_nonempty_string(meta.get(key)): @@ -267,10 +267,10 @@ def validate_coordination(plan: dict[str, Any]) -> list[str]: repo = consumer.get("repo") if not _is_nonempty_string(repo): errors.append(f"{path}.repo must be a non-empty string") - elif not _looks_like_repo(repo): + elif not _looks_like_repo(str(repo or "")): errors.append(f"{path}.repo must look like owner/repo") else: - consumer_repos.append(repo) + consumer_repos.append(str(repo)) if not _is_nonempty_string(consumer.get("reason")): errors.append(f"{path}.reason must be a non-empty string") errors.extend( @@ -352,7 +352,7 @@ def validate_coordination(plan: dict[str, Any]) -> list[str]: template = prompts.get("consumer_prompt_template") if not _is_nonempty_string(template): errors.append("prompts.consumer_prompt_template must be a non-empty string") - elif "{repo}" not in template: + elif "{repo}" not in str(template or ""): errors.append("prompts.consumer_prompt_template must contain '{repo}'") if not _is_nonempty_string(prompts.get("review_prompt")): errors.append("prompts.review_prompt must be a non-empty string") diff --git a/src/exp_abcd.py b/src/exp_abcd.py index dcebbee..fb13c35 100644 --- a/src/exp_abcd.py +++ b/src/exp_abcd.py @@ -1353,6 +1353,21 @@ def still_running(proc) -> bool: } +def _bind_synthesis(fn, repo: str, exp_id: str): + """Bind THIS iteration's repo/exp_id, which a bare lambda in a loop would not. + + Was `lambda repo=repo, exp_id=edir.name: fn(repo, exp_id)`. The default-arg trick is what made + it correct — it captures the current values so a later loop turn cannot rebind them — and it is + also what stopped mypy inferring the lambda's type. A closure over explicit parameters says the + same thing and is checkable. + """ + + def launch(): + return fn(repo, exp_id) + + return launch + + def followup( *, max_experiments: int = 1, @@ -1563,9 +1578,7 @@ def launch_fn(repo=meta["repo"], exp_id=edir.name): if launch_available and not promotion_inflight: promotion = promotion_reconcile( edir, - launch_fn=lambda repo=repo, exp_id=edir.name: ( - (synthesize_fn or synthesize)(repo, exp_id) - ), + launch_fn=_bind_synthesis(synthesize_fn or synthesize, repo, edir.name), completion_fn=promotion_completion_fn, resume_fn=promotion_resume_fn or _resume_synthesis_promotion, verify_fn=promotion_verify_fn, @@ -1657,7 +1670,7 @@ def _winner_and_harvest( denom = sum(w for _, w in scs) means[agent] = (sum(score * w for score, w in scs) / denom) if denom else 0.0 notes[agent] = ns - winner = max(means, key=means.get) + winner = max(means, key=lambda k: means[k]) return { "winner": winner, "winner_mean": means[winner], diff --git a/src/exploration_backfill.py b/src/exploration_backfill.py index 5d9bae6..624ee52 100644 --- a/src/exploration_backfill.py +++ b/src/exploration_backfill.py @@ -20,6 +20,7 @@ import shutil import tempfile import time +from collections.abc import Mapping from pathlib import Path import claims @@ -505,7 +506,7 @@ def schedule_backfill( backlog_path: Path | None = None, backlog_payload: dict | None = None, confirm: bool = False, - env: dict | None = None, + env: Mapping[str, str] | None = None, prepare_fn=None, issue_body_fn=None, ) -> dict: @@ -794,7 +795,8 @@ def fake_prepare( # template alone would leave the two halves free to drift apart, which is the same shape # as a gate whose measuring window differs from its draining window. assert calls[0]["exp_id"].startswith(f"{planned_job['exp_id_template']}-"), calls - assert claims.holder("o/r#1")["agent"] == BACKFILL_CLAIM_AGENT, claims.holder("o/r#1") + held = claims.holder("o/r#1") + assert held and held["agent"] == BACKFILL_CLAIM_AGENT, held no_progress_db = Path(tmp) / "no-progress.db" feedback.DB_PATH = no_progress_db diff --git a/src/exploration_collection.py b/src/exploration_collection.py index cdc8484..a911778 100644 --- a/src/exploration_collection.py +++ b/src/exploration_collection.py @@ -26,6 +26,7 @@ import time from contextlib import contextmanager from pathlib import Path +from typing import cast import claims import dispatcher @@ -128,6 +129,8 @@ def _filter_backlog( def _entry_is_late(task_type: str, assignment: dict) -> bool: for entry in (router.ROUTE_TABLE.get(task_type, {}) or {}).get("agents") or []: + if not isinstance(entry, dict): + continue if entry.get("agent") == assignment.get("agent") and entry.get("mode") == assignment.get( "mode" ): @@ -421,7 +424,7 @@ def build_window( require_exploration=True, ) count = _exploratory_count(probe_decision) - seed_search["attempted"] += 1 + seed_search["attempted"] = int(seed_search.get("attempted") or 0) + 1 if count > best_count: best_count = count best_decision = probe_decision @@ -456,7 +459,7 @@ def build_window( if not dry_run and rejected_assignments: _release_rejected_claims(rejected_assignments) if not dry_run and _exploratory_count(decision) < min_exploratory: - _release_assignments(decision.get("assignments") or []) + _release_assignments(cast(list, decision.get("assignments") or [])) blocked_reasons.append( "active claim race left too few direct exploration assignments to dispatch" ) diff --git a/src/exploration_evidence_plan.py b/src/exploration_evidence_plan.py index 93a0790..c9f8d9c 100644 --- a/src/exploration_evidence_plan.py +++ b/src/exploration_evidence_plan.py @@ -16,6 +16,7 @@ import tempfile import time from pathlib import Path +from typing import Any import backlog import capacity @@ -182,7 +183,7 @@ def _candidate_task_types( if len(sample_targets[task_type]) < 3: sample_targets[task_type].append(item.get("target") or "") route_rows = {row["task_type"]: row for row in coverage.get("tasks") or []} - candidates = [] + candidates: list[dict[str, Any]] = [] for task_type in sorted(set(route_rows) | set(opener_counts) | set(outcome_counts)): route = route_rows.get(task_type) or {} opener_items = opener_counts.get(task_type, 0) diff --git a/src/issue_quality.py b/src/issue_quality.py index 0329dd0..8a94bbb 100644 --- a/src/issue_quality.py +++ b/src/issue_quality.py @@ -280,7 +280,7 @@ def parallel_fetch(keys: set[tuple[str, int]], fn) -> dict[tuple[str, int], obje ) body_keys: set[tuple[str, int]] = set() for repo, pr_number in linked_cache: - issue_numbers: list[int] = linked_cache.get((repo, pr_number)) or [] + issue_numbers = list(linked_cache.get((repo, pr_number)) or []) if not isinstance(issue_numbers, list): continue for issue_number in issue_numbers: @@ -291,7 +291,7 @@ def parallel_fetch(keys: set[tuple[str, int]], fn) -> dict[tuple[str, int], obje body_cache = parallel_fetch(body_keys, fetch_body) for row, repo, pr_number in parsed_rows: - issue_numbers = linked_cache.get((repo, pr_number)) or [] + issue_numbers = list(linked_cache.get((repo, pr_number)) or []) if not issue_numbers: skipped.append( { diff --git a/src/keepalive_evidence.py b/src/keepalive_evidence.py index fbaec59..5396337 100644 --- a/src/keepalive_evidence.py +++ b/src/keepalive_evidence.py @@ -18,6 +18,7 @@ import tempfile import time from pathlib import Path +from typing import Any from urllib.parse import quote import feedback @@ -271,7 +272,7 @@ def _gotcha_lines(context: str) -> list[str]: def _knowledge_candidates(repo: str, *, knowledge_path: Path | None = None) -> list[dict]: patterns = [] seen: set[str] = set() - base_kwargs = {"path": knowledge_path} if knowledge_path is not None else {} + base_kwargs: dict[str, Any] = {"path": knowledge_path} if knowledge_path is not None else {} for task_type in KNOWLEDGE_TASK_TYPES: kwargs = dict(base_kwargs) if task_type is not None: @@ -363,7 +364,7 @@ def _process_signals( } if title: item["title"] = title - bucket = grouped.setdefault( + bucket: dict[str, Any] = grouped.setdefault( work_type, {"work_type": work_type, "prs": [], "has_revert": False} ) bucket["prs"].append(item) @@ -373,7 +374,7 @@ def _process_signals( signals = [] for work_type in sorted(grouped): bucket = grouped[work_type] - prs = bucket["prs"] + prs = list(bucket["prs"] or []) count = len(prs) signals.append( { diff --git a/src/mcp_server.py b/src/mcp_server.py index 1822bb5..4289e0f 100644 --- a/src/mcp_server.py +++ b/src/mcp_server.py @@ -20,6 +20,7 @@ import sys import time from pathlib import Path +from typing import cast ORCH = Path(__file__).resolve().parent sys.path.insert(0, str(ORCH)) @@ -429,7 +430,8 @@ def _selftest_advice_schema_matches_advise() -> None: import capability_advisor tool = next(t for t in TOOLS if t["name"] == "capability_advice") - advertised = set(tool["inputSchema"]["properties"]) + schema = cast(dict, tool["inputSchema"]) + advertised = set(schema["properties"]) sig = inspect.signature(capability_advisor.advise) # Caller-settable = keyword-only, minus the internals a remote caller must never drive. internal = {"record", "path", "lane", "context"} @@ -536,7 +538,8 @@ def _selftest_decline_schema_matches_record_decline() -> None: import capability_propensity tool = next(t for t in TOOLS if t["name"] == "capability_decline") - advertised = set(tool["inputSchema"]["properties"]) + schema = cast(dict, tool["inputSchema"]) + advertised = set(schema["properties"]) sig = inspect.signature(capability_propensity.record_decline) internal = {"path", "metadata"} callable_kw = { @@ -549,7 +552,8 @@ def _selftest_decline_schema_matches_record_decline() -> None: ) # The positional arguments must be reachable too, under the names the tool advertises. assert {"capability_id", "experiment_id"} <= advertised, sorted(advertised) - assert set(tool["inputSchema"]["required"]) == { + required_schema = cast(dict, tool["inputSchema"]) + assert set(required_schema["required"]) == { "capability_id", "experiment_id", "reason", diff --git a/src/pattern_miner.py b/src/pattern_miner.py index 5ff49c0..73a92e5 100644 --- a/src/pattern_miner.py +++ b/src/pattern_miner.py @@ -34,7 +34,7 @@ from collections.abc import Iterable from dataclasses import asdict, dataclass, replace from pathlib import Path -from typing import Any +from typing import Any, cast from capability_ir import ( CandidateTombstone, @@ -399,8 +399,8 @@ def normalize_episode(events: dict[str, CompletionEvent]) -> NormalizedEpisode: "kind": _derive_kind(decision), "signature": selected_ids or { - "action_id": _norm_string(_result(decision).get("action_id")), - "decision_source_id": _norm_string(_result(decision).get("decision_source_id")), + "action_id": (_norm_string(_result(decision).get("action_id")),), + "decision_source_id": (_norm_string(_result(decision).get("decision_source_id")),), }, "selected_ids": selected_ids, }, @@ -425,7 +425,7 @@ def normalize_episode(events: dict[str, CompletionEvent]) -> NormalizedEpisode: } semantic_graph = { **graph, - "execution": {"operation": graph["execution"]["operation"]}, + "execution": {"operation": cast(dict, graph["execution"])["operation"]}, } output_graph = { "trigger": graph["trigger"], @@ -711,7 +711,7 @@ def _candidate( progress["next_action"] = "wait_for_new_completion_evidence" progress["reasons"] = reasons self.rejections.append( - Rejection(progress["fingerprint"], "evidence_gate", tuple(reasons)) + Rejection(str(progress["fingerprint"]), "evidence_gate", tuple(reasons)) ) return None, progress diff --git a/src/range_lane_rollout.py b/src/range_lane_rollout.py index d3c7129..a263dc7 100644 --- a/src/range_lane_rollout.py +++ b/src/range_lane_rollout.py @@ -206,7 +206,7 @@ def build_rollout( } ) - decision = { + decision: dict[str, Any] = { "generated_at": int(time.time()), "dry_run": True, "assignments": [], diff --git a/src/redirect_apply.py b/src/redirect_apply.py index 76330e1..e144db9 100644 --- a/src/redirect_apply.py +++ b/src/redirect_apply.py @@ -916,7 +916,7 @@ def fake_runner(command, capture_output=True, text=True, check=False): assert any("daily bound" in b for b in bounded["blocks"]), bounded # ---- --dry-run must not be a cheap-looking door onto a per-candidate offload ------ - spent = [] + spent: list = [] guarded = apply_candidates( report_dir=tmp, corpus_path=corpus, diff --git a/src/research_scheduler.py b/src/research_scheduler.py index c78a085..0cb6bf5 100644 --- a/src/research_scheduler.py +++ b/src/research_scheduler.py @@ -468,8 +468,8 @@ def should_test( h = matches[0] runnable = [a for a in h["arms"] if all(spare.get(g, 0) > 0 for g in arm_agents(a))] if len(runnable) >= 2: - dh = data_hunger(task_type, runnable, conn=conn, weights=weights) - hunger = acquisition_hunger(task_type, runnable, conn=conn, weights=weights) + dh = data_hunger(str(task_type), runnable, conn=conn, weights=weights) + hunger = acquisition_hunger(str(task_type), runnable, conn=conn, weights=weights) return { "trigger": "hypothesis", "hypothesis": h["id"], @@ -522,7 +522,7 @@ def learned_posteriors( row = (rows or {}).get(agent) if isinstance(rows, dict) else None try: out[agent] = ( - float(row.get("posterior")) + float(row.get("posterior") or 0.0) if isinstance(row, dict) and row.get("posterior") is not None else 0.5 ) @@ -548,7 +548,7 @@ def _hypothesis_uncertainty(hyp: dict | None) -> float: def _item_stakes(item: dict) -> float: task_type = item.get("task_type", "implement") lane = item.get("lane") - return TASK_STAKES.get(task_type, 0.80) * LANE_STAKES.get(lane, 1.0) + return TASK_STAKES.get(str(task_type), 0.80) * LANE_STAKES.get(str(lane), 1.0) def _is_launchable_arm_set(arms: list) -> bool: diff --git a/src/research_subjects.py b/src/research_subjects.py index d1eece5..328fadf 100644 --- a/src/research_subjects.py +++ b/src/research_subjects.py @@ -514,7 +514,7 @@ def record_finding_issue( } if implementation_run_id: metadata["implementation_run_id"] = _validated_issue_run( - db, metadata["issue_target"], implementation_run_id + db, str(metadata["issue_target"]), implementation_run_id ) return record_event( FINDING_FILED, @@ -1376,7 +1376,11 @@ def _selftest() -> None: assert domain_target("SBA Portfolio") == "domain/sba-portfolio" for bad in ("", " ", "!!!", None): try: - domain_target(bad) + # DELIBERATELY the wrong type: this loop's whole point is that None and blank + # strings are REJECTED. Wrapping it in str() made None into the valid slug + # "None", the rejection never fired, and the selftest failed — a type fix that + # changed behaviour. A narrow ignore is the honest answer for a bad-input probe. + domain_target(bad) # type: ignore[arg-type] except ValueError: pass else: # a blank slug must not become the target "domain/" @@ -1416,9 +1420,9 @@ def _selftest() -> None: research_round_id("stranske/Workflows", "Audit", "2026-08-16") == "stranske/workflows:audit:2026-08-16" ) - for bad in (("", "audit", "2026-01-01"), ("a", "", "2026-01-01"), ("a", "audit", "")): + for bad_round in (("", "audit", "2026-01-01"), ("a", "", "2026-01-01"), ("a", "audit", "")): try: - research_round_id(*bad) + research_round_id(*bad_round) except ValueError: pass else: diff --git a/src/tick.py b/src/tick.py index 4dc0d88..dd4ef21 100644 --- a/src/tick.py +++ b/src/tick.py @@ -20,7 +20,9 @@ import sys import tempfile import time +from collections.abc import Mapping from pathlib import Path +from typing import Any import adversarial import capabilities @@ -51,7 +53,7 @@ def _adversarial_review_status( item: dict, *, dry_run: bool, - env: dict | None = None, + env: Mapping[str, str] | None = None, provision_fn=None, review_fn=None, ) -> dict | None: @@ -208,7 +210,7 @@ def research_tick( *, learned: dict | None = None, dry_run: bool = True, - env: dict | None = None, + env: Mapping[str, str] | None = None, max_experiments: int = RESEARCH_MAX_PER_TICK, conn=None, prepare_fn=None, @@ -447,7 +449,7 @@ def remote_tick( dry_run: bool = True, do_ingest: bool = True, max_delegations: int | None = None, - env: dict | None = None, + env: Mapping[str, str] | None = None, runtime_ac_gate_fn=None, research_tick_fn=None, ) -> dict: @@ -466,14 +468,12 @@ def remote_tick( if max_delegations is not None else int(os.environ.get("ORCH_MAX_REMOTE_PER_TICK", "3")) ) - chosen, no_capacity, deferred, blocked, adversarial_reviews, runtime_ac_gates = ( - [], - [], - [], - [], - [], - [], - ) + chosen: list[Any] = [] + no_capacity: list[Any] = [] + deferred: list[Any] = [] + blocked: list[Any] = [] + adversarial_reviews: list[Any] = [] + runtime_ac_gates: list[Any] = [] role_shadows: list[dict] = [] triage_shadow = roles.activate_tick_triage(items, cap, env=env, dry_run=dry_run) role_shadows.append( diff --git a/src/verify.py b/src/verify.py index b6aecf4..65b73fc 100644 --- a/src/verify.py +++ b/src/verify.py @@ -808,7 +808,7 @@ def _selftest() -> None: ("5 passed in 1s", 5, 5), ("no tests ran in 0.01s", 0, 0), ): - counts = {} + counts: dict[str, int] = {} for n, kind in COUNT_RE.findall(text): kind = "error" if kind.startswith("error") else kind counts[kind] = counts.get(kind, 0) + int(n) @@ -1063,6 +1063,7 @@ def _selftest() -> None: ], } ) + assert one is not None, "the absent-module line must render for a non-empty report" for phrase in ( "1 of 43", "evidence-acquisition", @@ -1083,7 +1084,7 @@ def _selftest() -> None: "absent": [{"capability_id": "ghost", "entrypoint": "ghost.py", "found_in": []}], } ) - assert "not found in any sibling checkout" in nowhere, nowhere + assert nowhere is not None and "not found in any sibling checkout" in nowhere, nowhere # THE TWO ROOTS. On a flat tree they coincide, which is exactly why an assertion is needed: # without one, code that re-merged them would pass here and only fail after the layout moved. diff --git a/tests/test_capability_set_coverage.py b/tests/test_capability_set_coverage.py index 228aff5..a9b3cdb 100644 --- a/tests/test_capability_set_coverage.py +++ b/tests/test_capability_set_coverage.py @@ -39,7 +39,7 @@ def _fixture_capabilities() -> set[str]: """Every capability named by a fixture (guards and flag probes excluded).""" named = [f.get("capability") for f in recurrence.FIXTURES] named += [f.get("capability") for f in recurrence.PREDICATE_FIXTURES] - return {n for n in named if n and not str(n).endswith("-flag")} + return {str(n) for n in named if n and not str(n).endswith("-flag")} def test_every_capability_has_a_recurrence_fixture(): @@ -384,7 +384,8 @@ def roster() -> str: row = rows.get(cap_id) or {} fx = "yes" if cap_id in covered else ("EXEMPT" if cap_id in FIXTURE_EXEMPT else "**NO**") can = "yes" if row.get("reachable") else "NO" - fire = {True: "fires", False: "miss"}.get(fired.get(cap_id), "—") + verdict = fired.get(cap_id) + fire = "—" if verdict is None else ("fires" if verdict else "miss") out.append( f"| {cap_id} | {fx} | {can} | {fire} | " f"{', '.join(row.get('defects') or []) or '—'} |"