RUL-030 — Generated artifacts belong in git
Ruling
Generated artifacts belong in git (user ruling, 2026-08-13).
A generated artifact is a snapshot of project state at a commit. If it is not in the commit, the commit does not describe the project: a reader cannot tell whether a missing AST snapshot means "not regenerated yet" or "we do not keep those", and two checkouts of the same SHA can disagree about what the codebase contains.
The boundary
"Generated" cannot mean every machine-written byte. reports/test-durations.ndjson
is 282 MB of append-only per-run telemetry, and *.warmcache is a
machine-specific pickle whose bytes differ per host at an identical commit.
Committing those would bloat the repo without describing it. A ruling with no
boundary is unenforceable, so the boundary is mechanical — three properties,
all three required:
- Derived — reproducible by running a named generator; never hand-authored.
- Read — something consumes it: a gate, a doc
@import, a report, a human. - A function of the commit — regenerating it at the same commit on another machine yields the same content.
Property 3 is what does the work. It excludes machine-local caches, per-run telemetry, wall-clock logs and runtime locks, and it does so on a test anyone can apply rather than on taste.
Anything excluded is listed in NOT_ARTIFACTS in the gate with a reason, so
the boundary is visible where it is enforced instead of implicit in a
.gitignore line nobody reads.
Why
The dispute was concrete and recurring. plans/ast-*.json and
reports/run-tests-latest.json were committed by some sessions (66cd4191,
6626dee4) and deliberately withheld by others under an earlier handover
convention. The result was a working tree that carried a permanent diff nobody
could resolve, resurfacing on every ast-sync, with each session re-litigating
it.
Applying the ruling immediately found a live violation the convention had
created: docs/CODEBASE_MAP_FULL.md was gitignored (.gitignore:223,
"too big to track/import"). It is derived, it is read (grep -n <symbol> plus
ranged reads — the documented way to look up a symbol), and it is a function of
the commit. It is 1.6 MB. The old exclusion conflated tracking it with
@importing it, and only the latter was ever the problem: the two-tier map
exists precisely so the big tier is not imported.
What the gate checks
For every registered artifact:
- ignored —
git check-ignorematches it. This is the violation the ruling names: an ignore rule permanently keeps a generated artifact out of git. The failure names the offending.gitignoreline, because that line is the fix. - untracked — it exists on disk but git does not track it: generated and never added.
A tracked artifact passes whether or not it is currently dirty. Regeneration cadence is a workflow question, not this ruling; requiring clean would turn every session red the moment a session-start hook regenerates the codebase map.
The gate refuses to report a pass it cannot justify. An empty registry, or a registry whose entries all resolve to nothing on disk, exits 2 (INVALID) rather than 0, and every run prints how many artifacts it examined. That is a checker must prove it looked applied to this gate itself — a "0 violations" that could equally mean "0 files examined" is exactly the defect class this project keeps paying for.
Enforcement
- gate scripts/check_generated_artifacts.py RUL-030
Related
- RUL-014 — A ruling that exists only in prose is incomplete work — why this page ships with a gate rather than a convention.
- A checker must prove it looked, not just that it found nothing — the failure mode the gate's INVALID exit code guards against.
- Starting context — where the generated artifacts and their generators sit in the wider system.