# Census rules — turning a diff into the worklist

The census is the worklist: an explicit list of cells, each one a `target × axis` pair, built once by the orchestrator before any reviewer is dispatched. This file defines how a diff becomes cells. The rules a cell is judged against live in the coding-preflight pack; the verdict format lives in `review-calibration.md`.

## Cell granularity — one rule group per axis

**Each axis carries exactly one rule group, and that group is the axis itself.** A cell is `target × <axis>`, never `target × <individual rule>`. Twelve changed files therefore produce twelve `structural` cells, not one per file per rule.

This is what makes the cell count reproducible: the axis list is fixed at four, so the census size depends only on the diff. Splitting an axis per individual rule would make the count depend on how many rules the reader chose to enumerate that day — the exact variance this census exists to remove.

The specific rule a finding violates is named **inside the verdict**, in its `rule` field (`review-calibration.md`), not in the cell label. One cell can therefore carry findings from several rules of its group, and it still counts as one cell for coverage.

## The one principle

**Membership is mechanical; judgment only ever decides a cell's verdict.**

Never ask "does this rule apply to this file?" while building the census — that question is the reviewer's, and its answer is a verdict (`clean` when the rule turns out not to apply). Over-inclusion is cheap: a clean verdict costs one line. A judged exclusion is variance — it is the exact failure this census exists to remove, because the next run judges differently and the review changes shape.

Two consequences:

- A cell exists because a mechanical test matched (file extension, path, "the diff touched this function"), never because the orchestrator expected a violation.
- The orchestrator never reads a file to decide membership. It reads the diff's file list and hunks, and nothing else.

## The four axes

| Axis | Cell unit | Membership test (mechanical) |
|---|---|---|
| `structural` | non-test source file × `structural` | every changed non-test source file |
| `semantic` | function × `semantic` | every function the diff touched — at least one changed line anywhere in its signature or body. Signature-only and type-annotation-only changes count |
| `state-and-tests` | non-test source file × `state-and-tests`, test file × `state-and-tests` | every changed non-test source file gets one cell; every changed test file gets one cell |
| `general` | non-test source file × `general` | every changed non-test source file |

"Is there a mutation or a state boundary in this file?" is **not** a membership question — the `state-and-tests` cell exists for every changed non-test source file, and the reviewer answers with a verdict.

Test files (the project's test-path or test-suffix convention — `*_test.*`, `*.test.*`, `*.spec.*`, `tests/`, `spec/`) are censused on `state-and-tests` only. They are not excluded: they are covered by a different rule group.

Functions are enumerated from the diff's hunks. A hunk that lands outside any function (imports, module wiring, top-level declarations, class-level constants) is file-level code — it belongs to the file cells, not to a function cell, and it is what the second completion criterion checks for.

## Exclusions

Only these are excluded from the census, and each exclusion is listed **with its reason** in the response and in the report's Coverage section:

- lockfiles (`package-lock.json`, `poetry.lock`, `Cargo.lock`, `*.sum`, …)
- generated code (anything under a build/output directory, or carrying a "do not edit" generation header)
- pure configuration and data (`.json`, `.yml`, `.toml`, fixtures) that contains no executable project code
- prose documentation with no executable or agent-directive content
- binary files and vendored third-party trees

**Skill, prompt, and agent-directive markdown IS censused.** In a repo whose behaviour is carried by prompts, a markdown file that instructs an agent is the executable surface, and a review that skips it skips the change. Extension is not the test — a `.md` file is excluded only when it is prose a human reads and nothing acts on.

To keep that mechanical rather than judged, a path allowlist overrides the extension list: anything under `skills/**`, `prompts/**`, `agents/**`, or `templates/**` is **always censused**, whatever its extension.

Everything else is censused. When a file is arguably code — a build script, a shell entrypoint, a template that carries logic — it is censused, not excluded; if the rules turn out not to apply, that is a `clean` verdict.

An exclusion without a stated reason is a defect in the census: it is indistinguishable from a file that was forgotten.

## Pack allocation

Step 2 of the skill routes the coding-preflight packs once. Distribute the resulting absolute paths across the axes like this:

| Pack | Goes to |
|---|---|
| `clean-code.md` | all four axes — it is language-agnostic and every axis draws from a different part of it |
| `overview.md` core principles | all four axes (each brief names the principles that are its own; see the skill's axis table) |
| routed `languages/*.md` | `state-and-tests` (self-mock signals) and `general` (language-specific traps) |
| routed `frameworks/*.md` | `general` |
| routed `architectures/*.md` | `structural` |

A pack that the routing did not select is not handed to anyone. A pack a reviewer receives is one it must read before verdicting.

## Unregistered language

When no Stage 1 language rule in `overview.md` matches any changed file, the project's language has no pack. Do not invent one.

1. Ask the user for the project's canonical style guide (a path, a URL, or a named standard) and, if they name one, hand its path to `state-and-tests` and `general` in place of the language pack.
2. If the user does not answer, run all four axes anyway with `clean-code.md` alone, and state in the report's Coverage section that language-specific rules were not applied and why.

Never skip an axis for a missing pack, and never silently proceed as if the language pack existed.

## Completion criteria

Both must hold exactly, and both are shown in the response before dispatch:

1. **Files:** distinct files censused (in any axis) + files excluded = files in the diff.

   One `git diff --name-status` row is one file, whatever its status letter. A rename row carries two paths (`R100 <old> <new>`) and still counts **once**: census the `<new>` path and never count `<old>` on either side of the equation. A delete row (`D <path>`) counts once too, and it is always an exclusion, with the reason `deleted — no remaining line to anchor a finding on`.
2. **Hunks:** every hunk in a censused file maps either to a censused function or to file-level code (imports, module wiring, declarations).

A count that does not balance means a file was dropped, not that the diff was odd. Find it before dispatching.

## What to emit

In the orchestrator's response, before Step 3:

- one table per axis, in the order `structural`, `semantic`, `state-and-tests`, `general`, listing every cell
- the exclusion list, one line per file, each with its reason
- the applied pack list (absolute paths)
- the two completion criteria with their actual numbers

Never truncate a table to save space. If the census is large, report the cell count and ask whether to proceed — a silent cut turns "every cell was verdicted" into a false claim.
