Skip to content

refactor(layout): src/ + tests/ + pyproject.toml, and a decluttered root - #90

Merged
stranske merged 5 commits into
mainfrom
claude/src-layout-and-declutter
Aug 24, 2026
Merged

refactor(layout): src/ + tests/ + pyproject.toml, and a decluttered root#90
stranske merged 5 commits into
mainfrom
claude/src-layout-and-declutter

Conversation

@stranske

Copy link
Copy Markdown
Owner

Sets up the src/ layout, adds pyproject.toml, and declutters the repo root. Three commits, in dependency order — read them separately, the middle one is what makes the third cheap.

commit what review
462d8e4 four handoff briefs → docs/briefs/ trivial
98eaf02 separate the module dir from the checkout root — a verified no-op read this one
c573065 git mv 99 modules → src/, 33 tests → tests/, add pyproject.toml mostly moves

The structure evaluation you asked for

Tracked root before: 131 .py (98 modules + 33 tests), 15 .md, plus config. After: zero .py.

finding disposition
131 root .py files src/ and tests/
4 transient handoff briefs among 15 root docs docs/briefs/, 6 inbound refs rewritten
stray runtime artifacts (gh-rate-ledger.ndjson, __pycache__, .coverage) already gitignored by #71/#72 — nothing to do
.coveragerc + mypy.ini alongside a new pyproject.toml deleted into it — see below, this was mandatory, not tidying

Deliberately not moved, because moving would break a contract rather than tidy one: README.md, CLAUDE.md, AGENTS.md, ARCHITECTURE.md (a contract from PLANNING.md/ORCHESTRATOR.md), and IMPROVEMENT_BACKLOG.md + CLAUDE.md (which orch-sync-mirror.sh copies by root path).

Why this was a git mv and not a 126-file rewrite

The middle commit is the whole trick. Every path in the tree came from Path(__file__).resolve().parent, because two different questions had the same answer while the modules sat at the root:

  • "where is my sibling module?"MODULE_DIR
  • "where is the checkout?" (orchestrate.sh, .verify-floor.json, the fleet repos) → REPO_ROOT

Separated in src/paths.py, the move became a rename. And no imports were rewritten: python3 src/tick.py puts src/ on sys.path itself, so every import capabilities still resolves; pytest gets the same from pythonpath = ["src"]. Both mechanics were proved on a scratch tree before anything moved.

The rule is detected, not hardcoded — module dir named src ⇒ checkout is its parent, else they coincide — because the exec mirror is flat. A hardcoded parent.parent would be right in one tree and wrong in the other, which is precisely the failure _fleet_roots already documents (37 of 37 in the checkout, 36 of 37 in the mirror). orchestrate.sh does the same detection in shell, which is why all 47 of its $ORCH/... sites needed one line changed.

Flat src/, not src/orchestrator/

A package would rewrite 115 imports and break the capability system's static analysis, which is built end-to-end on basename resolution from a known root — DRIVER_MODULES, _callers_of, heartbeat_reachable, emits_heartbeat, plus 31 modules that shell out to a sibling by filename. Flat src/ buys the two things actually wanted (mypy scoping, clean root) at a fraction of the risk. Namespace de-collision needs the package and is a separate decision.

pyproject.toml: tool config only, and the deletions were mandatory

No [project], no [build-system]. It can exist at all only because stranske/Workflows#3202 made the editable-install gate metadata-based instead of filename-based.

.coveragerc and mypy.ini had to be absorbed, not kept: CI passes --cov-config=pyproject.toml and --config-file pyproject.toml whenever the file exists, so leaving them beside it would have silently dropped parallel = true (reporting whichever subprocess finished last — the "plausible-but-wrong number" its own comment warns about) and explicit_package_bases (aborting mypy on a setup error). test_ci_gate_config now asserts the boundary that matters — the moment this file declares a distribution, five jobs start attempting -e '.[app,dev]' — replacing the old blanket ban, whose docstring explicitly invited relaxing it "deliberately".

Measured win

mypy 608 → 467 errors, purely from scoping. The Gate does target="src"; [ -d "$target" ] || target=".", so it now checks the 99 modules instead of the whole tree. This is the peer-parity the earlier evaluation said src/ alone would only buy 9% of — the scoping half is real, the other 489 still need typing.

scripts/ci_lint_baseline.py was measuring . and would have recorded 613 against the Gate's 467, 30 of the gap being import-not-found artefacts. A baseline that doesn't match the command it claims to record reads as measured, so it now runs the Gate's exact command. The toggle comment carries the new number and says why it moved.

The tail — every item found by the suite, not by reasoning

Each was a repo-root path derived from the module directory:

  • verify.py — gates now live in two directories (3 modules, 2 test files), so each resolves by where its file is; and the test-file gates needed src on PYTHONPATH, since python3 tests/x.py puts tests/ on sys.path. Both had been reporting ModuleNotFoundError instead of a verdict.
  • _callers_of / heartbeat_env_gateHERE / driver silently found nothing for orchestrate.sh, reporting two live capabilities as having no caller.
  • env_prereq.repo_files_absent — looked for .github/, docs/, scripts/ beside the modules, turning 12 real assertions into skips. Caught because skips are ceiling-bounded here.
  • capability_admission — reaches into the test suite for the fixture roster; that directory is now on sys.path explicitly rather than by accident.
  • test_improvement_log — conflated the accessor's doc spelling with its invocation path, so doc assertions started hunting an absolute path inside CLAUDE.md.

⚠️ One external dependency, not landed here

~/.codex/bin/orch-sync-mirror.sh copies "$SRC"/*.py flat — after this move that glob matches nothing, and the mirror launchd runs hourly would have no modules. That file is outside the repo and yours. The patch, why the mirror stays flat, and how to confirm it are in docs/MIRROR_SYNC_PATCH.md; CLAUDE.md §1 now points at it. It also needs its .coveragerc copy changed to pyproject.toml.

Do not sync the mirror until that patch is applied. The manual sync gap is the natural gate.

Test gate

python3 src/verify.py442 passed, 0 failed, 0 skipped, 85/85 selftests, 43/43 can-fire, 5/5 gates. Floor unchanged at 442: nothing added or removed. ruff check . and black --check --line-length 100 clean.

Note the command itself moved — python3 src/verify.py — and all 133 doc invocations gained the prefix, matched against the real module list so no generic word ending in .py was rewritten.

🤖 Generated with Claude Code

Tim Stranske and others added 3 commits August 23, 2026 19:37
Structure evaluation, step one of two. The repo root carried 15 tracked markdown files, of which
four are transient handoff briefs rather than standing documentation: `BRIEF_expand_range.md`,
`BRIEF_keepalive_transfers.md`, `BRIEF_process_improvement.md` and `CODEX_BRIEF.md`. They now live
in `docs/briefs/`, and their six inbound references are rewritten in the same change so no link
goes stale.

WHAT DELIBERATELY DID NOT MOVE, because the move would break a contract rather than tidy one:
  * `README.md`, `CLAUDE.md`, `AGENTS.md` — root is where tooling and readers look for them.
  * `ARCHITECTURE.md` — wired as a contract from `PLANNING.md` and `ORCHESTRATOR.md` (CLAUDE.md 5).
  * `IMPROVEMENT_BACKLOG.md` — a tracked pointer `test_improvement_log.py` asserts on, and
    `orch-sync-mirror.sh` copies it to the mirror BY ROOT PATH.
  * `CLAUDE.md` for the same mirror-sync reason.
  * `ORCHESTRATOR.md`, `PLANNING.md`, `ADDING_CAPABILITIES.md`, `EVAL_AND_TESTING.md`,
    `FEEDBACK_LOOP.md`, `WORKFLOW_USER_GUIDE.md` — standing docs with live inbound references.

The stray runtime artifacts that used to sit at the root (`gh-rate-ledger.ndjson`, `__pycache__`,
`.coverage`) are already gitignored as of #71/#72, so there was nothing left to untrack.

Verified: 442 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…o-op, on purpose

PREREQUISITE for a `src/` layout, landed and verified BEFORE anything moves so the move itself
changes no behaviour here. On a flat tree the two roots are the same directory, so this commit is
provably inert; that is exactly why it goes first.

THE ACCIDENT THIS UNPICKS. Every module derived two different answers from one expression,
`Path(__file__).resolve().parent`, because the modules sat at the repo root and the two questions
happened to have the same answer:

  * "where is my sibling MODULE?"  -> `capabilities.py`, `dispatcher.py`      -> MODULE_DIR
  * "where is the CHECKOUT?"       -> `orchestrate.sh`, `.verify-floor.json`,
                                      `.coverage*`, the sibling fleet repos    -> REPO_ROOT

Conflating them is what made a `src/` move look like a 126-file rewrite. Separated, it is this
commit plus a `git mv`.

`paths.py` is dependency-free (pathlib only) so any module can import it without closing a cycle —
`capabilities` imports `feedback`, and `feedback`'s selftest imports `env_prereq`, so putting this
in an existing module would have created a loop for somebody.

DETECTED, NOT HARDCODED, and that is the load-bearing part. `orch-sync-mirror.sh` copies modules
into the mirror launchd actually runs, and it may copy them FLAT or under `src/`. A hardcoded
`parent.parent` would be right in one tree and wrong in the other — precisely the failure
`capability_activation_audit._fleet_roots` already documents, where byte-identical code scored 37
of 37 in the canonical tree and 36 of 37 in the mirror. So the layout is OBSERVED: module dir named
`src` => checkout is its parent; otherwise the two coincide. Both mirror shapes stay correct.

THE RULE IS A FUNCTION, not just a constant, and that mattered. The first attempt used module-level
constants and broke `capability_activation_audit`'s selftest, which patches its own `HERE` to build
synthetic trees — a constant ignored the tree the caller believed it was inspecting. `checkout_root(
module_dir)` / `fleet_root(module_dir)` apply the rule wherever asked; the constants are defined
through the same function so there is one implementation, not a matching pair that can drift.

Rewired: `capability_recurrence_check` (ORCHESTRATE, the `*.sh` glob — while its `*.py` glob moves
to MODULE_DIR), `capability_propensity` (driver), `capability_activation_audit` (`_driver_path` for
shell-vs-module drivers, `_repo_root`, `_fleet_roots`), `capability_admission` (`known_controls`
reads orchestrate.sh from the checkout and modules from the module dir; `_audits_dir` anchors to
FLEET_ROOT), and `verify.py` — the module that needed it most, since it DISCOVERS modules beside
itself while READING repo files and running pytest from the checkout.

`verify.py` now also invokes each selftest BY PATH (`MODULES / f"{mod}.py"`) rather than by bare
filename, because its cwd is the checkout and a bare name would resolve against the wrong
directory once the modules move. `test_verify_coverage_mode`'s instrumentation needle tracks the
real call, so instrumentation and layout cannot drift apart silently — deliberate break -> revert:
removing `child_argv` from that runner still fails the test, reverted byte-identical.

`verify.py`'s own selftest now patches BOTH `HERE` and `MODULES` and asserts the two roots stay
distinguished. On a flat tree they are equal, which is why the assertion is needed: without it,
code that re-merged them would pass here and only fail after the layout moved.

Verified: 442 passed, 0 failed, 0 skipped, 85/85 selftests, 43/43 can-fire, 5/5 gates. Floor
unchanged at 442 — no test added or removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nd add pyproject.toml

The reorganisation the previous commit made cheap. 99 modules -> `src/`, 33 test files -> `tests/`.
The repo root goes from 131 tracked `.py` files to zero.

NO IMPORT REWRITES, and that is why this is a `git mv` rather than a 126-file edit. `python3
src/tick.py` puts `src/` on `sys.path` itself, so every `import capabilities` keeps resolving
unchanged; pytest gets the same via `pythonpath = ["src"]`. Both mechanics were proved on a
scratch tree before a single file moved.

WHY A FLAT `src/` AND NOT `src/orchestrator/`. A package would rewrite 115 import statements AND
break the capability system's static analysis, which is built end-to-end on basename resolution
from a known root — `DRIVER_MODULES`, `_callers_of`, `heartbeat_reachable`, `emits_heartbeat`,
`shell_heartbeat_gate`, plus 31 modules that shell out to a sibling by filename. A flat `src/`
gives the two things actually wanted — mypy scoping and a clean root — at a fraction of the risk.
Namespace de-collision would need the package; it is a separate decision.

pyproject.toml carries TOOL CONFIG ONLY, deliberately: no `[project]`, no `[build-system]`. It can
exist at all only because stranske/Workflows#3202 made the editable-install gate metadata-based
instead of filename-based. `.coveragerc` and `mypy.ini` are DELETED into it — not tidying: CI
passes `--cov-config=pyproject.toml` and `--config-file pyproject.toml` whenever the file exists,
so leaving them beside it would have silently dropped `parallel = true` (reporting whichever
subprocess finished last) and `explicit_package_bases` (aborting mypy on a setup error).
`test_ci_gate_config` now asserts the boundary that matters — the moment this file declares a
distribution, five jobs start attempting `-e '.[app,dev]'` — replacing the old blanket ban, which
its own docstring invited relaxing "deliberately".

MEASURED WIN: mypy drops 608 -> 467 errors, purely from scoping. The Gate does
`target="src"; [ -d "$target" ] || target="."`, so it now checks the 99 modules instead of the whole
tree. `scripts/ci_lint_baseline.py` was measuring `.` and would have recorded 613 against the
Gate's 467 — a baseline that does not match the command it claims to record reads as measured, so
it now runs the Gate's exact command. The toggle comment carries the new number and says why.

THE TAIL, each found by the suite rather than reasoned about, and each a repo-root path derived
from the module directory:
  * `verify.py` — gates live in TWO directories now (three modules, two test files), so each is
    resolved by where its file actually is; and the two test-file gates needed `src` on PYTHONPATH,
    because `python3 tests/x.py` puts `tests/` on `sys.path`, not `src/`. Both reported
    `ModuleNotFoundError` instead of a verdict.
  * `capability_activation_audit._callers_of` — `HERE / driver` silently found NOTHING for
    orchestrate.sh, reporting two LIVE capabilities as having no caller. `heartbeat_env_gate` the
    same.
  * `env_prereq.repo_files_absent` — looked for `.github/`, `docs/`, `scripts/` beside the modules,
    so 12 real assertions became skips. Skips are ceiling-bounded here, which is what surfaced it.
  * `capability_admission` — reaches into the test suite for the recurrence-fixture roster; that
    directory is now added to `sys.path` EXPLICITLY rather than by accident.
  * `test_improvement_log` — conflated the accessor's DOC spelling with its invocation path, so the
    doc assertions started hunting for an absolute path inside CLAUDE.md.

`orchestrate.sh` needed one line: `$ORCH` points at the modules, detected (`$ORCH_REPO/src`, falling
back when absent) because THE MIRROR IS FLAT. All 47 of its `$ORCH/...` uses are modules, so
nothing else changed. 133 doc invocations gained their `src/` prefix, matched against the real
module list so no generic `.py` word was rewritten.

EXTERNAL DEPENDENCY, NOT LANDED HERE: `~/.codex/bin/orch-sync-mirror.sh` copies `"$SRC"/*.py` flat
and now matches nothing, so the mirror launchd runs would have no modules. It is outside the repo
and owner-owned; the patch, the reason the mirror stays flat, and how to confirm it are in
`docs/MIRROR_SYNC_PATCH.md`, and CLAUDE.md §1 now points at it.

Verified: 442 passed, 0 failed, 0 skipped, 85/85 selftests, 43/43 can-fire, 5/5 gates. Floor
unchanged at 442 — nothing added or removed. `ruff check .` and `black --check --line-length 100`
clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 154 files, which is 4 over the limit of 150.

To get a review, reduce the PR to 150 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to Pro+ to raise the limit.

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fa8d18ff-4470-4cb2-803d-fae36e785802

📥 Commits

Reviewing files that changed from the base of the PR and between d8e0fa6 and 672d670.

📒 Files selected for processing (154)
  • .coveragerc
  • .github/workflows/ci.yml
  • .github/workflows/pr-00-gate.yml
  • .verify-floor.json
  • ADDING_CAPABILITIES.md
  • ARCHITECTURE.md
  • CLAUDE.md
  • IMPROVEMENT_BACKLOG.md
  • ORCHESTRATOR.md
  • PLANNING.md
  • README.md
  • docs/MIRROR_SYNC_PATCH.md
  • docs/briefs/BRIEF_expand_range.md
  • docs/briefs/BRIEF_keepalive_transfers.md
  • docs/briefs/BRIEF_process_improvement.md
  • docs/briefs/CODEX_BRIEF.md
  • mypy.ini
  • orchestrate-seat.sh
  • orchestrate.sh
  • pyproject.toml
  • ruff.toml
  • scripts/ci_lint_baseline.py
  • src/adapters.py
  • src/adversarial.py
  • src/agent_auth_check.py
  • src/backlog.py
  • src/cadence_registry.py
  • src/capabilities.py
  • src/capability_activation_audit.py
  • src/capability_admission.py
  • src/capability_advisor.py
  • src/capability_compiler.py
  • src/capability_effectiveness.py
  • src/capability_firing_monitor.py
  • src/capability_ir.py
  • src/capability_lifecycle.py
  • src/capability_matcher_proposals.py
  • src/capability_opportunity.py
  • src/capability_outcome_bridge.py
  • src/capability_propensity.py
  • src/capability_recurrence_check.py
  • src/capability_targets.py
  • src/capacity.py
  • src/ccusage_reconcile.py
  • src/claims.py
  • src/codemod_lane.py
  • src/completion_event_adapter.py
  • src/consumer_sync_artifact_ingest.py
  • src/consumer_sync_shadow.py
  • src/cross_repo_lane.py
  • src/dispatcher.py
  • src/dry_seam_audit.py
  • src/durability_sweep.py
  • src/env_prereq.py
  • src/epic_lane.py
  • src/evidence_acquisition.py
  • src/evidence_schema.py
  • src/execution_profiles.py
  • src/exp_abcd.py
  • src/experiment_recovery.py
  • src/exploration_backfill.py
  • src/exploration_collection.py
  • src/exploration_evidence_plan.py
  • src/exploration_review.py
  • src/feature_scan.py
  • src/features.py
  • src/feedback.py
  • src/frontend_verify.py
  • src/gh_capacity.py
  • src/human_calibration.py
  • src/improvement_log.py
  • src/issue_quality.py
  • src/issue_readiness.py
  • src/judge_reliability.py
  • src/keepalive_evidence.py
  • src/keepalive_outcomes.py
  • src/keepalive_shadow.py
  • src/keepalive_supervisor.py
  • src/langsmith_direct.py
  • src/langsmith_fetch.py
  • src/langsmith_pull.py
  • src/ledger_reconcile.py
  • src/local_verify.py
  • src/mcp_server.py
  • src/merge_guard.py
  • src/model_profile_trial.py
  • src/model_profile_trial_bridge.py
  • src/objective_anchor.py
  • src/observability_dashboard.py
  • src/outcomes.py
  • src/partitioned_review.py
  • src/paths.py
  • src/pattern_miner.py
  • src/periodic_report.py
  • src/provision.py
  • src/range_lane_rollout.py
  • src/redirect_apply.py
  • src/redirect_plan.py
  • src/redirect_policy.py
  • src/redirect_shadow.py
  • src/redirect_sweep.py
  • src/relearn_report.py
  • src/repo_knowledge.py
  • src/research_scheduler.py
  • src/research_subjects.py
  • src/roles.py
  • src/router.py
  • src/runner_effect_bridge.py
  • src/runtime_ac.py
  • src/runtime_ac_flow_monitor.py
  • src/runtime_ac_gate.py
  • src/runtime_ac_panel.py
  • src/strategy_experiment.py
  • src/switch_review.py
  • src/synthesis_promotion.py
  • src/testgen_gate.py
  • src/testgen_lane.py
  • src/tick.py
  • src/ux_review.py
  • src/verify.py
  • src/watch.py
  • tests/test_capabilities.py
  • tests/test_capability_admission.py
  • tests/test_capability_causal_core.py
  • tests/test_capability_epic.py
  • tests/test_capability_lifecycle_e2e.py
  • tests/test_capability_set_coverage.py
  • tests/test_capacity_profiles.py
  • tests/test_ci_gate_config.py
  • tests/test_completion_events.py
  • tests/test_consumer_sync_artifact_ingest.py
  • tests/test_consumer_sync_shadow.py
  • tests/test_evidence_contract_compiler.py
  • tests/test_experiment_arm_identity.py
  • tests/test_feedback_model_provenance.py
  • tests/test_improvement_log.py
  • tests/test_model_profile_trial.py
  • tests/test_model_profile_trial_bridge.py
  • tests/test_model_tier_resolution.py
  • tests/test_observability_activation.py
  • tests/test_partitioned_review.py
  • tests/test_pattern_miner.py
  • tests/test_playbook_compiler.py
  • tests/test_repo_artifact_hygiene.py
  • tests/test_research_control.py
  • tests/test_role_compiler.py
  • tests/test_roles_lineage.py
  • tests/test_runner_effect_bridge.py
  • tests/test_runtime_ac_flow_monitor.py
  • tests/test_skill_compiler.py
  • tests/test_synthesis_promotion.py
  • tests/test_ux_review.py
  • tests/test_verify_coverage_mode.py
  • tests/test_workflow_compiler.py

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Comment @coderabbitai help to get the list of available commands.

@agents-workflows-bot

Copy link
Copy Markdown
Contributor

Workflow source needed

PR #90 needs either a linked GitHub issue or one valid non-issue Workflow Source before PR metadata automation can manage it safely.

Please do one of:

  • Add <!-- meta:issue:123 --> or a normal Closes #123 / Related to #123 line.
  • Check one Workflow Source option in the PR body.
  • Add a hidden marker such as <!-- workflow-source:local_request -->, <!-- workflow-source:manual_remote -->, <!-- workflow-source:review_followup -->, <!-- workflow-source:sync_campaign -->, or <!-- workflow-source:dependabot -->.
  • Add a workflow source label such as workflow:source-direct-pr, workflow:source-local-request, workflow:source-review-followup, workflow:source-sync, or workflow:no-automation.

Once a valid source is present, this warning will not be reposted.

…t a ratchet

ANSWER TO "has mypy addressed all the typing issues": NO, and it could not have been. There are 455
real findings across 64 modules — `arg-type` 109, `index` 104, `assignment` 69, `union-attr` 43 —
each a distinct annotation or narrowing decision in a distinct module, with no `mypy --fix` in
existence. That is what the toggle's old `drainable: 0 per PR` recorded, and it is why the check had
been OFF: an all-or-nothing gate over code nobody can fix in one change is a gate whose clear path
is blocked by the thing it measures, so it stayed shut and checked NOTHING.

So this changes the shape of the problem rather than pretending to finish it. The check is now ON.

WHAT MADE IT OPENABLE:
  * the src/ move scoped the Gate's `target="src"` to the 99 modules — 608 -> 467 errors;
  * `[[tool.mypy.overrides]]` exempts the modules that still have findings BY NAME, so the 35
    already-clean ones are checked TODAY instead of nothing being checked at all.

`mypy --config-file pyproject.toml --exclude .workflows-lib src` reports "no issues found in 99
source files", and new untyped code in a clean module is now a RED.

SCOPE IS NOT SILENCE, and the distinction is enforced, not asserted in prose.
`test_mypy_config_silences_nothing_by_error_code` still forbids `disable_error_code` and
`follow_imports` — those blind the check to a CLASS of error everywhere, with nothing to count and
no mechanism that removes them; fifteen codes would have covered 603 of 608. A per-module list is
different in kind: every finding stays discoverable via `scripts/ci_lint_baseline.py`, and deleting
a name restores that module's errors instantly (verified: removing `dispatcher` surfaced its 65).
A top-level `ignore_errors` is forbidden too, because it exempts everything in one keyword.

THE RATCHET, with the mandatory latched-gate questions answered in pyproject.toml itself:
  1. What decrements it? Typing a module and deleting its line. Not "time passes".
  2. Can that run while the gate is CLOSED? Yes — green never blocks typing work.
  3. Same measuring and draining window? Yes: ONE list, counted by verify.py, drained by editing
     those same lines.
`.verify-floor.json`'s `mypy_exempt_max` (64) FAILS if the list grows, so a module can never
quietly rejoin the exempt set, and `verify.py` prints `64/64 max of 99 module(s) exempt, 35 checked`
on EVERY run — "mypy on" can never come to mean "mypy checks nothing".

AND IT IS DRAINED, not just mechanised: 12 `var-annotated` findings fixed with real annotations
(467 -> 455, 66 -> 64 modules). Purely additive, no runtime behaviour touched, every module
re-imported. Five more were left alone deliberately — they are tuple-unpacking or loop targets where
the annotation is not a one-line edit, and guessing at those is how a "typing" commit becomes a
behaviour change.

Two tests needed real changes rather than relaxation:
  * `test_every_disabled_toggle_states_blocking_and_drainable` asserted that SOME toggle is forced
    off; none is now. Generalised rather than deleted: a toggle that is ON but BOUNDED owes the same
    blocking/drainable/drains-by annotation, because "on over a scoped subset" hides as much as
    "off" if the scope is unstated. It still refuses to pass vacuously.
  * `test_mypy_config_silences_nothing` substring-matched the config text and tripped on a COMMENT
    explaining why `disable_error_code` was rejected. It now parses the TOML — a check that fails on
    prose about itself teaches people to weaken it.

Deliberate break -> revert, each caught then reverted byte-identically: adding
`disable_error_code`; adding a module to the exempt list without raising the ceiling; deleting the
ceiling while the list exists; removing `dispatcher` from the list (65 errors returned).

Verified: 442 passed, 0 failed, 0 skipped, 85/85 selftests, 43/43 can-fire, 5/5 gates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@stranske
stranske merged commit 6c6648d into main Aug 24, 2026
38 checks passed
@stranske
stranske deleted the claude/src-layout-and-declutter branch August 24, 2026 01:56
stranske added a commit that referenced this pull request Aug 24, 2026
…bound assertion (#92)

Two regressions from #90, both found by running verify FROM THE MIRROR rather than reasoning about it.

The tests stopped travelling: one cp glob used to carry modules and tests together when both were root-level .py. A mirror run collected zero tests and reported 'floor 442 NOT MET', which reads as a broken change rather than a broken sync. pyproject.toml now carries pythonpath = ['src', '.'] so one list is right in both the src/ checkout and the flat mirror; the sync script (outside the repo) copies tests/ and fails loudly on either half being empty, and removes the .coveragerc that pyproject.toml replaced.

And an assertion added in #90 was true only under src/ — it claimed a module name is absent from the checkout root, which is false in the flat mirror where those are the same directory. Removed rather than made conditional: a test that holds in one layout makes the other look broken.

Verified in both trees: repo 442 passed / 0 skipped; mirror 419 passed / 23 skipped of 442 collected, floor met, 5/5 gates in each.
stranske added a commit that referenced this pull request Aug 24, 2026
THREE THINGS, and the third is why this is one PR rather than three.

1. --floor-may-lag, on PULL REQUESTS ONLY. .verify-floor.json's collected is
   an EQUALITY, which is right -- a floor behind reality is permissive by
   exactly the gap. But the correct number is only knowable on the MERGE
   RESULT: one one-line change needed SIX re-measurements in two hours while
   main moved eight times, and that file's own note records the floor being
   found behind reality four separate times before that. On a PR a LAGGING
   floor is now reported and not fatal; a collection DROP stays fatal. On main
   the equality is enforced by the same job WITHOUT the flag, and the
   reconcile job heals a lag within one run, so main's invariant is unchanged.
   The lag still PRINTS -- forgiven is not hidden.

   This is the third consumer of _blocks_floor_update, which is what makes it
   safe: one predicate distinguishes "you added tests" from "tests vanished",
   so the flag structurally cannot widen into the dangerous direction.

2. The wiring-pin convention, in CLAUDE.md. Three source-literal pins fired on
   entirely correct changes on 2026-08-23/24 -- the pin file's baseline
   citation, test_verify_coverage_mode's verify() call, and verify.py's own
   --update-floor guard -- because each pinned a WHOLE LINE. A test that calls
   a reformat a regression gets waived, and a waived test protects nothing. The
   rule: pin the smallest fragment that would be ABSENT if the wiring were
   removed; nothing a formatter owns; prefer a behavioural assertion where one
   is cheap. All three instances are already fragment-form; this stops the
   regression.

3. Six F821 "Undefined name Any" on main, from #90. Not a runtime break --
   from __future__ import annotations means those annotations never evaluate,
   and all six modules import cleanly -- but they fail lint-ruff on every
   future PR, including this one.

   WHY THEY GOT IN, because the mechanism matters more than the fix: the Gate
   runs on pull_request only, #90's Gate run was held at action_required with
   0 jobs (the suspicious-workflow review, because #90 edits workflow files),
   and pushes to main do not trigger it. So #90 merged with ZERO
   lint/format/typecheck verification. That is this repo's founding failure --
   merging past a gate that never ran -- recurring through a different
   mechanism than the missing pin file that started it.

Verified on the merge result after rebasing through #90's 82 renames:
442 passed, 0 failed, 0 skipped, floor 442, 85/85 selftests, 5 of 5 gates,
mypy Success (99 files, 35 checked / 64 exempt), ruff clean, black clean.

Break -> revert: widening --floor-may-lag to `return []` fails the
collection-drop assertion; reverted byte-identical.

Co-authored-by: Tim Stranske <tim@stranskemo.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
stranske pushed a commit that referenced this pull request Aug 24, 2026
THE DEFECT. main has no branch protection, and `gh pr checks` lists what DID
report -- so a check that never started is not red, it is missing, and a PR
with no Gate reads exactly like a PR whose Gate passed. Silence
indistinguishable from success: this repo's founding defect, twice over.
2026-08-23, five python-ci jobs died at a shared install step and #61/#64/#65
merged with all five red. 2026-08-24, #90's Gate run was held at
`action_required` with ZERO jobs and merged with no lint, no format and no
typecheck, landing six F821s found only because somebody ran ruff by hand.

THE CAUSE WILL BE DIFFERENT NEXT TIME, so nothing here models holds. A check
can vanish to a hold, a cancellation, a deleted or renamed workflow, a rate
limit, a mistaken path filter or a GitHub incident. All present identically to
whoever is merging. scripts/check_checks_reported.py asks only: did every check
that NORMALLY reports also report here.

TWO SIMPLER DESIGNS WERE TRIED AND REJECTED BY REAL DATA, both recorded in the
file because the next person will reach for them:
  * one reference PR -- failed on the actual incident. #90 had no Gate, and the
    newest merged PR (#93) had no Gate checks either, so #90 was declared
    healthy. The hold had already swallowed the yardstick.
  * the union across recent merges -- caught #90 (21 absent) but reported 25-26
    absences on entirely healthy PRs, sweeping in event-driven checks. A test
    that cries wolf 25 times gets waived.
Frequency (>=75% of 12 merged PRs) discriminates: #90 exit 1 with 10 absent,
be a second copy of the CI topology.

NOT BRANCH PROTECTION, deliberately. A required check that is HELD never
reports, so the PR could never merge -- the clear path blocked by the very
thing the gate measures. On a solo-maintained repo "unverified but movable"
beats "permanently stuck".

pr-00-gate.yml also gains `push: [main]`. A held PR run cannot be fixed from
inside CI, but the silence AFTER the merge can: #90's F821s would have gone red
on main within one run instead of never. Fail toward noise.

docs/ABSENT_CHECK_LANE_WIRING.md carries the one step this repo cannot land --
the closer's pre-merge call and the prerun `--sweep` line, since the lane TOMLs
live outside any repository. Same shape as docs/MIRROR_SYNC_PATCH.md.

Already found a live one: --sweep flags open PR #91 with 10 absent checks,
which I would have merged on a green-looking list.

Verified: 446 passed, 0 failed, 0 skipped, floor 446, 85/85 selftests, 5 of 5
gates, mypy Success, ruff and black clean. Break -> revert: removing the
max(2, ...) threshold floor fails test_the_threshold_never_falls_to_one;
reverted byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
stranske added a commit that referenced this pull request Aug 24, 2026
* feat(ci): an absent check is a red, whatever made it absent

THE DEFECT. main has no branch protection, and `gh pr checks` lists what DID
report -- so a check that never started is not red, it is missing, and a PR
with no Gate reads exactly like a PR whose Gate passed. Silence
indistinguishable from success: this repo's founding defect, twice over.
2026-08-23, five python-ci jobs died at a shared install step and #61/#64/#65
merged with all five red. 2026-08-24, #90's Gate run was held at
`action_required` with ZERO jobs and merged with no lint, no format and no
typecheck, landing six F821s found only because somebody ran ruff by hand.

THE CAUSE WILL BE DIFFERENT NEXT TIME, so nothing here models holds. A check
can vanish to a hold, a cancellation, a deleted or renamed workflow, a rate
limit, a mistaken path filter or a GitHub incident. All present identically to
whoever is merging. scripts/check_checks_reported.py asks only: did every check
that NORMALLY reports also report here.

TWO SIMPLER DESIGNS WERE TRIED AND REJECTED BY REAL DATA, both recorded in the
file because the next person will reach for them:
  * one reference PR -- failed on the actual incident. #90 had no Gate, and the
    newest merged PR (#93) had no Gate checks either, so #90 was declared
    healthy. The hold had already swallowed the yardstick.
  * the union across recent merges -- caught #90 (21 absent) but reported 25-26
    absences on entirely healthy PRs, sweeping in event-driven checks. A test
    that cries wolf 25 times gets waived.
Frequency (>=75% of 12 merged PRs) discriminates: #90 exit 1 with 10 absent,
be a second copy of the CI topology.

NOT BRANCH PROTECTION, deliberately. A required check that is HELD never
reports, so the PR could never merge -- the clear path blocked by the very
thing the gate measures. On a solo-maintained repo "unverified but movable"
beats "permanently stuck".

pr-00-gate.yml also gains `push: [main]`. A held PR run cannot be fixed from
inside CI, but the silence AFTER the merge can: #90's F821s would have gone red
on main within one run instead of never. Fail toward noise.

docs/ABSENT_CHECK_LANE_WIRING.md carries the one step this repo cannot land --
the closer's pre-merge call and the prerun `--sweep` line, since the lane TOMLs
live outside any repository. Same shape as docs/MIRROR_SYNC_PATCH.md.

Already found a live one: --sweep flags open PR #91 with 10 absent checks,
which I would have merged on a green-looking list.

Verified: 446 passed, 0 failed, 0 skipped, floor 446, 85/85 selftests, 5 of 5
gates, mypy Success, ruff and black clean. Break -> revert: removing the
max(2, ...) threshold floor fails test_the_threshold_never_falls_to_one;
reverted byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(ci): degrade the absent-check sweep gracefully on a rate limit

The sweep runs every lane round from handoff-prerun.sh. Dumping GitHub's full
rate-limit paragraph hourly would train the reader to skip the whole section,
which is how a health report stops being read. One line instead, and it says
UNKNOWN rather than implying clean -- a reporter that cannot report must not
read as a clean bill of health.

Found by testing the prerun block while genuinely rate-limited, which is the
degraded path I would otherwise have had to simulate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(ci): ratchet the expected-check set so a sustained outage cannot erode it

FOUND BY DOGFOODING, not by review. Running --pr 91 before merging it, the check
PASSED -- and should not have. While pr-00-gate.yml sat held, every newly merged
PR merged WITHOUT the Gate, so after twelve such merges the Gate's checks no
longer appeared on 75% of the reference window, stopped counting as "normally
reporting", and their absence stopped being flagged. The expected set fell
23 -> 14 names and #91 was pronounced clean by the tool written to catch exactly
that.

A SUSTAINED outage is the case that matters most, and it was the one case the
frequency rule could not see. The erosion test already in this file covered only
PRs that reported NOTHING; a PR reporting some checks but not the Gate's slid
straight through.

config/expected-checks.json is now the high-water mark, seeded from PRs #87/#89
whose Gate demonstrably ran (33 names, 8 of them python-ci). A name that has ever
been expected stays expected until somebody DELETES ITS LINE -- a visible act in
a diff. Same shape as pyproject.toml's mypy exempt ratchet, and the same reason:
an automatic downward move is indistinguishable from the defect.

--update-ratchet raises it and never lowers it.

With the ratchet, PR 91's head correctly reports 19 absences including the whole
python-ci set. PR 89 (Gate ran) reports one, "guard", which is a TRUE positive:
agents-guard.yml was already held by then.

Break -> revert: returning the observed set instead of its union with
ratchet_names() fails test_the_ratchet_is_wired_into_the_expected_set;
byte-identical after.

448 passed, floor 448, 85/85 selftests, 5 of 5 gates, mypy Success, ruff and
black clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Tim Stranske <tim@stranskemo.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
stranske added a commit that referenced this pull request Aug 24, 2026
…wanted, plus .json (#110)

Maint Coverage Guard failed the moment it was unheld, on both runs (#3 scheduled,
#4 manual), with:

  error: Coverage verification could not find required coverage artifacts on a
         successful Gate run.
  warn:  Unable to locate a recent successful Gate workflow run with required
         coverage artifacts: gate-coverage-trend, gate-coverage-trend-history,
         gate-coverage.

maint-coverage-guard.yml downloads exactly three artifact NAMES. The Gate
produced the first two under those names and the third as "gate-coverage.json",
so the guard probed ten successful runs, found two of three every time, and
reported no usable run at all.

Measured rather than inferred: Gate run #241 (main, success) uploaded
gate-coverage-summary.md, gate-coverage.json, gate-summary.md,
gate-coverage-3.13-1, gate-coverage-trend-history, gate-coverage-trend,
gate-coverage-summary, gate-coverage-3.12-1 -- trend and trend-history present,
"gate-coverage" absent.

An artifact NAME and the FILE inside it are different things, and this step
conflated them. Renaming the artifact is safe and was checked, not assumed:
"gate-coverage.json" as an artifact name appears nowhere else in this repo, and
the only download-artifact steps in any workflow here are the guard's three. The
file inside is still gate-coverage.json, so anything reading contents is
unaffected.

A paired literal in two files that disagreed -- this repo's signature defect --
and it stayed invisible for as long as the guard was held. That is the second
thing the holds were concealing, after PR #90's six F821s.

pr-00-gate.yml is create_only in the Workflows sync manifest with Orchestrator
absent from overwrite_repos, so this local fix persists. The same mismatch
likely affects every consumer whose Gate emits coverage, which is worth raising
upstream separately.

448 collected, ruff and black clean, test_ci_gate_config 12 passed.

Co-authored-by: Tim Stranske <tim@stranskemo.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant