Implementation Plan: Durable derived consumer census for CI fan-out

Branch: issue-6173-derived-consumer-census | Date: 2026-10-13 | Spec: spec.md Input: Mission specification from kitty-specs/derived-consumer-census-01M4V5P5/spec.md

Summary

Replace the hand-maintained CI fan-out lists (GROUP_MODULE_FANOUT in scripts/ci/gate_selection.py + the non-src router groups' module sets) with a derived consumer census: a committed, regenerable artifact mapping each test module → the src//packs/ path patterns its tests consume (static import/ read/string-literal scan plus runtime read-capture of resolver-driven opens). From that census, GROUP_MODULE_FANOUT's module sets are derived; the router globs stay hand-authored (GitHub Actions requires literal YAML, transcription- pinned) but are verified by a new red-first architectural gate that reds when a census consumer's path would not select its module under the current router+registry. The three historical pins are reproduced; a planted unrouted consumer reds the gate; the derived routing's selected-shard cost delta is measured on recent merged PR diffs; and the charter 7-shard long pole is either shrunk by within-module file selection or carved to a named follow-up.

Technical Context

Language/Version: Python 3.11+ Primary Dependencies: stdlib ast, pathlib, sys.addaudithook (runtime capture); existing scripts.ci.gate_selection (load_router, _registry_module_names, select_modules, GROUP_MODULE_FANOUT); yaml/json for artifact I/O. No new third-party dependency. Storage: a committed census artifact (JSON) under .github/ (beside ci-module-registry.yml / ci-shard-timings.json, the CI-data home) or scripts/ci/ — decided in IC-1. Deterministic, sorted keys. Testing: pytest; the new gate lives under tests/architectural/ (its only per-PR home is the heavy battery, like the other routing-authority gates); the census script's unit tests live under tests/ci/ (the ci module). make test-fast + tests/ci + the specific named routing lockstep gate files + the new gate's module (per CLAUDE.md test policy + NO_FULL_HEAVY_SUITES_IN_MISSION). Target Platform: CI (GitHub Actions Linux) + local dev; the census must run offline and deterministically. Project Type: single (CI-infrastructure tooling in scripts/ci/ + tests). Performance Goals: census regeneration < 2 min (NFR-004); the gate is a static read of the committed artifact (no test execution, battery-fast). Constraints: no src/ behaviour change (C-001); no size/length ratchet gate (C-002); no scrub/registry row for the non-src fan-out groups (C-003); router globs stay hand-authored (C-004); module-set agnostic / #6164-robust (NFR-002); no loosened gate (NFR-001). Scale/Scope**: 21 registry modules; ~177 tracked packs/built-in/missions files; the four non-src fan-out groups; ~15 test_dirs on the aggregate rows.

Charter Check

GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.

source for "module consumes path"; the fan-out module sets derive from it rather than being a second hand-curated map. PASS (reconciles, does not add a rival authority — the router globs stay the one path→group source, the registry the one module inventory).

gate is a non-vacuous call-site gate (concrete census floor + a planted-consumer self-mutation positive control). It starts empty-allowlist (no baseline entry — there is no allowlist). PASS.

pins red against a router with their globs stripped, and the planted consumer reds the gate. The gate is committed before the derivation flips GROUP_MODULE_FANOUT. PASS.

_registry_module_names / select_modules; do not re-derive routing. PASS.

run the SPECIFIC named architectural gate files, never the bare tests/architectural/ sweep. PASS (recorded in tasks' validation).

"Mission" canon respected. PASS.

  • Single canonical authority (DIRECTIVE_044): the census becomes the ONE
  • Architectural gate discipline (DIRECTIVE_043, Standing Order #5): the new
  • ATDD-first (C-011): the gate's red-first provenance is proven — the three
  • Canonical sources (DIRECTIVE_044): reuse gate_selection.load_router /
  • No full heavy suites in mission work (NO_FULL_HEAVY_SUITES_IN_MISSION):
  • Terminology: "routing" is always qualified (placement/consumer sense);
  • Git/workflow (DIRECTIVE_045): draft PR first, operator merges. PASS.

No violations → Complexity Tracking empty.

Project Structure

Documentation (this mission)

kitty-specs/derived-consumer-census-01M4V5P5/
├── spec.md
├── plan.md                 # this file
├── research/
│   └── code-grounding.md   # committed first (read-only grounding)
├── data-model.md           # census artifact schema (Phase 1)
├── analysis/               # cost-delta measurement record (Phase 1/impl)
└── tasks/                  # WP files (/spec-kitty.tasks)

Source Code (repository root)

scripts/ci/
├── gate_selection.py            # MODIFIED: GROUP_MODULE_FANOUT derived from the census
├── consumer_census.py           # NEW: build/load the census (static + runtime capture)
└── consumer_census_cost.py      # NEW: selected-shard cost-delta over recent PR diffs

.github/
└── ci-consumer-census.json      # NEW: committed, regenerable census artifact

tests/
├── architectural/
│   └── test_consumer_census_routing.py   # NEW: red-first routing gate (+ 3 pins + planted consumer)
└── ci/
    ├── test_consumer_census.py           # NEW: census-builder unit tests (determinism, runtime capture)
    └── test_consumer_census_cost.py      # NEW: cost tool unit tests

Structure Decision: single-project CI-infrastructure tooling. The census builder + cost tool live in scripts/ci/ (the ci module, test_dirs tests/ci). The routing gate lives in tests/architectural/ because its only per-PR home is the heavy battery, matching test_gate_selection_authority.py / test_ci_router_transcription_guards.py. The committed artifact lives in .github/ beside the registry and shard timings (confirmed in IC-1).

Complexity Tracking

No Charter Check violations — section intentionally empty.

Implementation Concern Map

> Concerns are NOT work packages; /spec-kitty.tasks translates them into WPs.

{path_or_glob, provenance} with provenance in {static-import, static-read, static-literal, runtime}), the artifact location (.github/ci-consumer-census.json), determinism (sorted, stable), and the load_census() reader. The module universe and path→module attribution reuse the registry (test_dirs + canonical mirror) and load_router, so a #6164 module-set change flows through. (FR-001, FR-004, NFR-002, NFR-003.)

test_dirs: imports of src/ modules, Path(...)/read_text/open arguments and string-literals naming src/ or packs/** paths, resolved to repo-relative paths/globs and attributed to the owning module(s). (FR-002.)

resolver-driven codepaths (the prompt/template/manifest resolution the tests/next/tests/runtime/tests/charter_offering consumers trigger) under an open/is_file audit hook, recording src//packs/ opens attributed to the triggering module. Canonical validation: tests/next resolves packs/.../mission-steps/software-dev/research/prompt.md. (FR-003, NFR-004.)

census (module, path), assert select_modules([path]) ⊇ {module}; red on a planted unrouted consumer (positive control); reproduce the three pins and prove each reds against a router with its glob stripped (red-first). (FR-005, FR-006, FR-007, NFR-001.)

with a loader that derives each non-src group's module set from the census (paths matching the group's router globs → consuming modules not already base- selected). The existing test_group_module_fanout_is_wired pins stay green as a cross-check. No scrub/registry row added (C-003); router globs untouched (C-004). (FR-008, NFR-001.)

measures the selected-shard count per recent merged PR diff and the delta vs. pre-census routing; recorded in analysis/ and the PR. Within-module file selection for charter (tests/charter_offering/missions/ + test_prompt*) lands if tractable, else a named follow-up issue. (FR-009, FR-010.)

  • IC-1 — Census model & artifact. The schema (module → list of
  • IC-2 — Static consumer detection. AST + text scan over each module's
  • IC-3 — Runtime read-capture. A bounded, offline harness that exercises the
  • IC-4 — Red-first routing gate. test_consumer_census_routing.py: for each
  • IC-5 — Derived fan-out. Replace the hardcoded GROUP_MODULE_FANOUT dict
  • IC-6 — Cost delta + within-module selection. consumer_census_cost.py

Phase 0 — Research

Done in research/code-grounding.md (committed): the mechanism verbatim, the runtime-resolution trace, the lockstep gates, the cost picture, the #6164 collision, churn. Open question resolved there: derive the module sets, keep the router globs hand-authored + census-verified.

Phase 1 — Design

determinism contract.

via select_modules against the router globs (independent authority for path→group), so deriving the fan-out from the census does not make the gate check itself vacuously; an unrouted path (matching no group) still reds.

  • data-model.md: the census artifact JSON schema + provenance vocabulary +
  • Confirm the derivation breaks no circularity: the gate checks census→routing

Risks

the same census, it would be vacuously green. Mitigation: the gate's authority is the router globs (path→group), not the derived module sets; a census path matching no group reds regardless of derivation. (Validated by the planted consumer.)

offline. Mitigation: a fixed, enumerated set of resolver invocations; sorted output; a unit test asserting reproducibility (NFR-003).

registry) may conflict on rebase. Mitigation: keep the diff confined to the census mechanism; re-run the lockstep gates on rebase; never take/revert #6164's module rows.

too large for this PR it is carved to a follow-up, not half-built (FR-010).

  • Circularity — if the gate checked the census against a fan-out derived from
  • Runtime capture flakiness — audit-hook capture must be deterministic and
  • #6164 lockstep churn — shared files (gate_selection.py, maybe the
  • Charter 7-shard cost — within-module selection is the only lever; if it is