scripts.check_generated_artifacts

Reference › scripts › check_generated_artifacts

The generated-artifacts gate, the mechanism for ruling RUL-030: generated artifacts belong in git. It is non-waivable, with no warn-only mode and no inline override. Three test modules import it: tests/scripts/test_affected_manifest_rules.py, tests/scripts/test_check_generated_artifacts.py and tests/scripts/test_rtm_evidence_paths.py. Invocation: main takes only --json. It refuses to pass (exit invalid) if the artifact registry is empty, since an empty registry would mean nothing was checked. It runs four checks and merges their violations:

Exclusion entries record files that look generated but are not.

At a glance

Source scripts/check_generated_artifacts.py
Lines of code 1314
Classes 2
Functions 19
Imports (internal) none
Imported by none
Tests that import it tests/scripts/test_affected_manifest_rules.py, tests/scripts/test_check_generated_artifacts.py, tests/scripts/test_rtm_evidence_paths.py

Class diagram

Class diagram scripts.check_generated_artifacts

Dependency diagram

No internal imports and no importers.

API

Constants

Name Type Value
GENERATED_ARTIFACTS tuple[Artifact, ...] (Artifact('plans/ast-ultimate.json', 'scripts/serialize_codebase_ast.py seriali…
NOT_ARTIFACTS tuple[Exclusion, ...] (Exclusion('*.warmcache', 'machine-specific pickled AST cache, content-sha1 sel…

Classes

Artifact (dataclass)

Decorators: dataclass(frozen=True) · Line 158

A generated file that RUL-030 requires git to track.

Attribute Type Default
path str
generator str
consumer str
runtime_input bool False

Exclusion (dataclass)

Decorators: dataclass(frozen=True) · Line 187

A machine-written path that is deliberately NOT an artifact.

Attribute Type Default
path str
reason str

Functions

Function Signature Summary
_expand _expand(artifact: Artifact, repo_root: Path) -> list[tuple[Artifact, str]] Resolve one registry entry to the concrete paths to check.
_git _git(args: list[str], repo_root: Path) -> subprocess.CompletedProcess[str]
_is_ignored _is_ignored(path: str, repo_root: Path) -> str | None The .gitignore rule excluding path, or None.
_is_tracked _is_tracked(path: str, repo_root: Path) -> bool
check check(artifacts: tuple[Artifact, ...] | None = None, repo_root: Path | None = None) -> tuple[list[str], int] Return (violations, examined).
_generator_path_hint _generator_path_hint(generator: str) -> str | None The leading repo-relative path in generator, if it looks like one.
_generator_path_candidates _generator_path_candidates(token: str) -> tuple[str, ...] Repo-relative path strings worth trying for token.
_git_deleted_path _git_deleted_path(token: str, repo_root: Path) -> tuple[str, str, str] | None If a path matching token was DELETED from git history, return (sha, date, path) for the commit that deleted it -- proof of LOSS rather than NEVER-EXISTED (see :data:_LOSS_MARKER).
_related_requirements _related_requirements(paths: list[str], repo_root: Path) -> list[str] Best-effort REQ-* ids that paths serve, read from docs/rtm.json -- the ONE materialized traceability corpus this project already maintains (core/traceability_utils.py save_rtm(), the…
_provenance_violation _provenance_violation(generator: str, paths: list[str], repo_root: Path) -> str One loud, actionable line for a generator that does not resolve.
check_generators_exist check_generators_exist(artifacts: tuple[Artifact, ...] | None = None, repo_root: Path | None = None) -> tuple[list[str], int] Return (violations, examined) for the FORWARD provenance check.
_read_header _read_header(path: Path, window: int = _HEADER_WINDOW) -> str The first window lines of path, decoded best-effort.
_extract_header_generator _extract_header_generator(head: str) -> str | None The producer named by a header's "Generated by X" claim, or None if the header makes no such claim (or only a prose one -- see :data:_HEADER_GENERATOR_RE).
_generator_reference_exists _generator_reference_exists(token: str, repo_root: Path) -> bool Resolve a header-claimed generator name to real source on disk.
scan_generator_headers scan_generator_headers(repo_root: Path | None = None, source_dirs: tuple[str, ...] = ('tests', 'src', 'docs')) -> dict[str, list[str]] Map every header-claimed generator to the files that name it.
check_generator_headers check_generator_headers(repo_root: Path | None = None, source_dirs: tuple[str, ...] = ('tests', 'src', 'docs')) -> tuple[list[str], int] Return (violations, examined) for the header-DISCOVERY check.
_truncating_writes _truncating_writes(source: str, basename: str) -> list[int] Line numbers where source truncates its declared artifact in place.
check_atomic_writes check_atomic_writes(repo_root: Path | None = None, artifacts: tuple[Artifact, ...] | None = None) -> tuple[list[str], int] Return (violations, examined) — no generator may truncate in place.
main main(argv: list[str] | None = None) -> int

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/scripts/check_generated_artifacts.py

#!/usr/bin/env python3
"""Generated-artifacts gate — the mechanism for ruling **RUL-030**.

User ruling, 2026-08-13: *"generated artifacts belong in git."*

The dispute this settles is concrete. ``plans/ast-*.json`` and
``reports/run-tests-latest.json`` were committed by some sessions
(``66cd4191``, ``6626dee4``) and deliberately withheld by others, so the
working tree carried a permanent diff nobody could resolve, and a reader
could not tell whether a missing snapshot meant "not regenerated" or "we
don't keep those". 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.

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. A ruling with no boundary
is unenforceable, so this gate applies a mechanical test:

    An artifact belongs in git when it is a DERIVED SNAPSHOT OF PROJECT
    STATE that a human or the code READS, and whose content is a function of
    the commit rather than of the machine or the run.

Three properties, all three required:

1. **Derived** -- reproducible by running a named generator, never hand-authored.
2. **Read** -- something consumes it: a gate, a doc @import, a report, a human.
3. **A function of the commit** -- regenerating it at the same commit on
   another machine yields the same content. This is what excludes
   machine-local caches, per-run telemetry, wall-clock logs and locks.

Anything failing (3) is not an artifact for this purpose; it is a cache, a
log or runtime state. Those are listed in :data:`NOT_ARTIFACTS` **with a
reason**, so the exclusions are visible and auditable rather than implicit
in a ``.gitignore`` line nobody reads.

WHAT IT CHECKS
--------------
For every entry in :data:`GENERATED_ARTIFACTS`:

* **ignored** -- ``git check-ignore`` matches it. This is the violation the
  ruling names: an ignore rule permanently keeps a generated artifact out of
  git. Reported with the offending ``.gitignore`` line.
* **untracked** -- it exists on disk but git does not track it. It was
  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 make
every session red the moment a hook regenerates the codebase map.

THE OTHER DIRECTION
--------------------
RUL-030 makes artifacts stay IN git. It says nothing about the GENERATOR
that produces them staying in the tree too -- and on 2026-07-09, commit
``6a186c9d`` deleted ``scripts/ast_serializer/testgen/`` (9 files, 2,392
lines) with the rationale "audited, zero consumers", because the audit
equated *no importer* with *no consumer*. For a generator, the consumer is
its OUTPUT, not its callers: ~100 files under ``tests/generated_recovery/``
still named ``ast_serializer.testgen`` in their headers, but that directory
sits under ``--ignore=tests/generated_recovery`` in pytest addopts, so
nothing ever executed those files and nothing went red. The deletion was
invisible, and the artifacts' own "Edit freely -- this file is NOT
regenerated automatically" header told the auditor the generator was
dispensable. Self-fulfilling.

The missing invariant is bidirectional provenance: an artifact must name its
generator in machine-readable form, AND a generator must not be deletable
while artifacts still referencing it remain in the tree. This gate checks
that invariant two ways:

* **forward** (:func:`check_generators_exist`) -- for every
  :data:`GENERATED_ARTIFACTS` entry whose ``generator`` field names a
  checkable repo-relative path, that path must exist. Cheap, but only as
  honest as the registry: an entry nobody bothered to register is invisible
  to it.
* **discovery** (:func:`check_generator_headers`) -- the direction that
  would actually have caught 6a186c9d, because it does not trust the
  registry at all. Every file under ``tests/``, ``src/``, ``docs/`` is
  scanned for a header CLAIMING machine generation ("Generated by X",
  "generated-by: X"), and the named producer must exist on disk. A registry
  entry can be stale or simply missing; a file's own header cannot be
  argued with -- it is the artifact making the claim about itself.

AUTHORITY -- and why there is no escape hatch
----------------------------------------------
Both directions are ruling **RUL-030**: "generated artifacts belong in git"
already presupposes a generator exists to have produced them, so a generator
vanishing out from under its artifacts is the SAME defect the ruling names,
seen from the other side. Where a specific orphaned artifact or missing
generator serves a graded requirement, the violation names that ``REQ-*`` id
too -- read from ``docs/rtm.json``, the ONE materialized traceability corpus
this project already maintains (``core/traceability_utils.py save_rtm()``,
the same corpus ``scripts/check_spec_authority.py`` and
``scripts/check_requirement_corpus.py`` read). This gate does not invent a
second notion of "spec" -- an empty result means the corpus does not
currently link anything to that path, not that nothing could.

This check is deliberately **NON-WAIVABLE**. It does not import
``core/gate_waivers.py`` and must never be made to: six other checkers honour
the ``# gate-ok: <reason>`` convention because SOME findings are a judgment
call an author can reasonably override at the line. A missing generator is
not that kind of finding -- it is a fact about the filesystem and git
history, and a loss-detector a line comment can silence is exactly the
mechanism that let 6a186c9d go unnoticed for a month. There is likewise no
``--warn-only``/``--severity``/env-var downgrade path (contrast
``scripts/doc_lint.py --warn-only``, which is warn-only ON PURPOSE for a
different, softer class of finding): this gate always exits non-zero on a
real violation, and treats "could not determine the answer" as a violation
too (see "WHY IT COUNTS WHAT IT EXAMINED" below) -- unknown is not clean.

WHY IT COUNTS WHAT IT EXAMINED
------------------------------
This repo's recurring defect is a checker whose "0 violations" is
indistinguishable from "I never looked" (see the wiki concept page *A checker
must prove it looked*). So this gate refuses to report a pass it cannot
justify: an empty registry, or a registry whose entries all resolve to
nothing on disk, is a FAILURE, not a clean run -- and every run prints the
number of artifacts examined alongside the verdict.

Exit codes: 0 pass · 1 violations found · 2 the gate could not do its job.
"""

from __future__ import annotations

import argparse
import ast
import json
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path

#: Matches "Generated by X" / "generated-by: X" and captures only a
#: path-like or dotted-module token -- never a bare English phrase like "the
#: protocol buffer compiler", which has neither a file extension nor a dot
#: joining two identifiers. Prose header claims are (correctly) invisible to
#: the discovery check below: they name an external tool, not something in
#: this repo. Declared at module scope, next to the other module constants,
#: so it compiles once.
_HEADER_GENERATOR_RE = re.compile(
    r"generated[- ]by:?\s+"
    r"([A-Za-z0-9_./-]+\.(?:py|pyi|sh|pl|j2|proto)"
    r"|[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+)",
    re.IGNORECASE,
)

REPO_ROOT = Path(__file__).resolve().parent.parent

EXIT_PASS = 0
EXIT_VIOLATIONS = 1
EXIT_INVALID = 2


@dataclass(frozen=True)
class Artifact:
    """A generated file that RUL-030 requires git to track.

    ``generator`` is more than documentation: :func:`check_generators_exist`
    resolves it to an on-disk path and fails when the code it names is
    gone -- the FORWARD half of the bidirectional-provenance invariant (see
    the module docstring's "THE OTHER DIRECTION"). It is free text because
    generators genuinely vary in shape (a script invocation, an external
    tool like ``uv``, a frozen/non-re-runnable description); the check only
    fires on the shapes that name a checkable repo path.

    ``runtime_input`` marks the artifacts the RUNNING SUITE consumes —
    imported, consulted, or installed-from — as opposed to snapshots and
    reports that tools read as data behind their own freshness gates.
    ``scripts/affected_tests.py`` certifies generated artifacts inert
    (a change costs only the smoke floor), and that is exactly wrong for
    this subset: a ``uv.lock`` change swaps the dependency versions every
    test imports, and a ``cxp_pb2.py`` regeneration changes the codec the
    CXP tests exercise. Declared HERE, on the one canonical registry,
    so the selector derives the split instead of growing a second list.
    """

    path: str
    generator: str
    consumer: str
    runtime_input: bool = False


@dataclass(frozen=True)
class Exclusion:
    """A machine-written path that is deliberately NOT an artifact.

    Recorded here rather than only in ``.gitignore`` so that the boundary is
    stated where the rule is enforced, and so a future reader can challenge
    a specific exclusion instead of guessing why something is missing.
    """

    path: str
    reason: str


#: Every derived snapshot that must be tracked. Seeded from a measured sweep
#: of the generator surfaces (scripts/, .claude/hooks/, src/ writers) rather
#: than from filename guessing.
GENERATED_ARTIFACTS: tuple[Artifact, ...] = (
    Artifact(
        "plans/ast-ultimate.json",
        "scripts/serialize_codebase_ast.py serialize",
        "the Prolog gate fact base, code_intel, trace_intel, rts",
    ),
    Artifact(
        "plans/ast-tests.json",
        # DERIVED from ast-ultimate.json (ast_pipeline step 1b), never parsed
        # on its own. This entry used to name `serialize_codebase_ast.py
        # serialize --source tests`, which is the abandoned second-parse
        # approach: two snapshots of the same files made by different
        # invocations drift, and tests/test_orchestrata_ast_derive_subtree.py
        # exists to forbid exactly that. The declaration stayed behind after
        # the pipeline moved to derive_subtree, and on 2026-08-19 a reader
        # followed it, regenerated the artifact the wrong way, and the ship's
        # suite caught the disagreement. check_generators_exist could not:
        # it resolves the named path and serialize_codebase_ast.py does exist,
        # so a wrong-but-present generator passes the forward check.
        "scripts/run_ast_pipeline.py --snapshot-only",
        "test-side traceability and affected-test selection",
    ),
    Artifact(
        "plans/ast-deep-comparison.json",
        "scripts/serialize_codebase_ast.py deep-compare",
        "port-completion reporting",
    ),
    Artifact(
        "docs/CODEBASE_MAP.md",
        ".claude/hooks/inject-codebase-map.sh",
        "@imported into every session and every subagent",
    ),
    Artifact(
        "docs/CODEBASE_MAP_FULL.md",
        ".claude/hooks/inject-codebase-map.sh",
        "on-demand symbol lookup (grep + ranged reads); deliberately NOT @imported",
    ),
    Artifact(
        "docs/LEVEL0.md",
        "scripts/gen_level0.py",
        "@imported as the always-in-context foundation tier",
    ),
    Artifact(
        "reports/run-tests-latest.json",
        "scripts/run_tests.py",
        "the session error report; the evidence behind any green claim",
    ),
    Artifact(
        "src/orchestrata_ultimate/prolog/intent_lexicon.gen.pl",
        "scripts/gen_intent_lexicon.py",
        "consulted by the intent pipeline; gated by lexicon-drift",
        runtime_input=True,
    ),
    Artifact(
        "uv.lock",
        "uv",
        "reproducible installs across machines and CI",
        runtime_input=True,
    ),
    Artifact(
        "infra/terraform/.terraform.lock.hcl",
        "terraform init",
        "terraform, to pin provider versions + hashes",
    ),
    Artifact(
        "infra/rehearsal/.terraform.lock.hcl",
        "terraform init (rehearsal overlay)",
        "terraform, to pin provider versions + hashes",
    ),
    # --- generated CODE ---------------------------------------------------
    Artifact(
        "src/orchestrata_ultimate/cxp/proto/cxp_pb2.py",
        "scripts/gen_cxp.py (protoc)",
        "every CXP codec path; imported at runtime",
        runtime_input=True,
    ),
    Artifact(
        "src/orchestrata_ultimate/cxp/proto/cxp_pb2.pyi",
        "scripts/gen_cxp.py (protoc)",
        "type checking of the CXP codec",
        runtime_input=True,
    ),
    Artifact(
        "tests/generated_*/**/*.py",
        "scripts/generate_pytest.py",
        "the suite runs them -- 268 files the runner drives every time",
        runtime_input=True,
    ),
    Artifact(
        "plugins/orchestrata/**",
        "claude/scripts/gen_plugin.py",
        "Claude Code -- the installable plugin, a generated mirror of claude/",
    ),
    Artifact(
        ".claude-plugin/marketplace.json",
        "claude/scripts/gen_plugin.py",
        "Claude Code marketplace discovery for the orchestrata plugin",
    ),
    # --- traceability / RTM -----------------------------------------------
    Artifact(
        "plans/derived-rtm.json",
        "scripts/derive_rtm.py build()",
        "trace_intel and the RTM acceptance pin",
    ),
    Artifact(
        "reports/derived-rtm.md",
        "scripts/derive_rtm.py write_report()",
        "the human-readable RTM",
    ),
    Artifact(
        "docs/rtm.json",
        "src/orchestrata_ultimate/core/traceability_utils.py save_rtm()",
        "requirement coverage reporting",
    ),
    Artifact(
        "docs/rtm.md",
        "src/orchestrata_ultimate/core/traceability_utils.py save_rtm()",
        "requirement coverage reporting (human tier)",
    ),
    Artifact(
        "reports/rtm-evidence.json",
        "scripts/generate_rtm_evidence.py",
        "evidence-graded RTM rows and their proof class",
    ),
    Artifact(
        "reports/rtm-evidence.md",
        "scripts/generate_rtm_evidence.py",
        "evidence-graded RTM (human tier)",
    ),
    # --- code-intelligence inputs -----------------------------------------
    Artifact(
        "plans/ast-orchestrata.json",
        "scripts/serialize_codebase_ast.py serialize (frozen monorepo)",
        "deep-compare's other side; port-completion reporting",
    ),
    Artifact(
        "plans/reference-edges.json",
        "scripts/extract_reference_edges.py",
        "non-import reference edges for the code-intelligence KB",
    ),
    Artifact(
        "plans/runtime-callgraph.json",
        "scripts/build_runtime_callgraph.py",
        "runtime call-graph merged into the AST snapshot",
    ),
    Artifact(
        "reports/runtime-callgraph.md",
        "scripts/build_runtime_callgraph.py write_report()",
        "the human-readable runtime call graph",
    ),
    # --- baselines a gate compares against ---------------------------------
    Artifact(
        "reports/graph-write-swallow-baseline.json",
        "scripts/check_graph_write_swallow.py --update-baseline",
        "the graph-write-swallow gate's own baseline",
    ),
    # --- session record ----------------------------------------------------
    Artifact(
        "context.history.md",
        "session CLI (ContextFileManager)",
        "the append-only session history a new session reads",
    ),
    # --- secrets CONFIG (not a secret: public recipients only) -------------
    Artifact(
        ".sops.yaml",
        "scripts/secrets_cli.py write_sops_config()",
        "sops, to pick recipients when encrypting; carries public age "
        "recipients only, never a private key",
    ),
    Artifact(
        "context.json",
        "session CLI (~/.claude/tools/session_cli.py)",
        "session state injected at every session start",
    ),
    Artifact(
        "context.md",
        "session CLI (~/.claude/tools/session_cli.py)",
        "the human-readable session log",
    ),
    # --- registered by the RUL-030 follow-up sweep --------------------------
    # (backlog 503: tracked, generator-confirmed, previously unprotected.
    # docs/diagrams/pkg-*.mmd and environment-*.{dot,png,svg} stay OUT: no
    # re-runnable writer names those paths in code, and an entry whose
    # generator cannot be re-run is exactly the "registry nobody maintains"
    # this module's docstring forbids. plans/class-mapping.json stays out
    # too — it is a HAND-CURATED input to deep-compare, not a generated
    # artifact, and must never be registered as one.)
    Artifact(
        "specs/*/checklists/requirements.md",
        "scripts/parse_spec_requirements.py",
        "per-spec requirement checklists; consumed by spec-review tooling",
    ),
    Artifact(
        "plans/spec_term_prominence.json",
        "scripts/spec_word_cloud.py",
        "spec term-prominence data behind the word-cloud rendering",
    ),
    Artifact(
        "docs/diagrams/spec-word-cloud.svg",
        "scripts/spec_word_cloud.py",
        "the rendered spec word cloud embedded in docs",
    ),
    Artifact(
        "plans/semantic_graph_qa_results_latest.md",
        "scripts/evaluate_semantic_graph_qa.py",
        "the 100-question semantic-graph retrieval benchmark record",
    ),
    Artifact(
        "reports/code-intel-slo.json",
        "scripts/benchmark_code_intel.py --json",
        "Step 3.4 code-intelligence SLO measurements",
    ),
    Artifact(
        "reports/prolog-codeintel-benchmark.md",
        "scripts/benchmark_prolog_codeintel.py",
        "Prolog-vs-Python code-intelligence benchmark record",
    ),
    Artifact(
        "rtm/*.json",
        "legacy monorepo RTM tooling (frozen; not re-runnable here)",
        "historical traceability record, mirrored into the plugin bundle",
    ),
    Artifact(
        "rtm/*.md",
        "legacy monorepo RTM tooling (frozen; not re-runnable here)",
        "historical traceability record (human tier)",
    ),
)

#: Machine-written paths that fail the "function of the commit" test. Each
#: needs a reason a reviewer can argue with.
NOT_ARTIFACTS: tuple[Exclusion, ...] = (
    Exclusion(
        "*.warmcache",
        "machine-specific pickled AST cache, content-sha1 self-validating and "
        "regenerable; identical commits produce different bytes per machine",
    ),
    Exclusion(
        "vendor/wheels/*.whl",
        "free-threaded tree-sitter wheels built per-host by "
        "scripts/build_ft_tree_sitter.py: platform-tagged binaries "
        "(cp314t-linux_x86_64), so identical commits produce different "
        "bytes per machine — the backlog-503 sweep listed them as tracked, "
        "but vendor/wheels/.gitignore deliberately excludes them",
    ),
    Exclusion(
        ".ast_daemon/",
        "live-service scratch seeded from the AST snapshot, never read back",
    ),
    Exclusion(
        "reports/test-durations.ndjson",
        "append-only per-run telemetry, 282 MB and unbounded -- a function of "
        "how many times the suite has run, not of the commit",
    ),
    Exclusion(
        "reports/session-logs/",
        "per-run logs; wall-clock content, not commit content",
    ),
    Exclusion(
        "reports/promotion-state.json",
        "promote_env.py's runtime ledger: it records WHEN a promotion ran, so "
        "its content is wall-clock state rather than a function of the commit "
        "(and it currently carries a known stale-timestamp defect)",
    ),
    Exclusion(
        ".context.lock",
        "runtime cross-process lock",
    ),
    Exclusion(
        "backups/",
        "graph dumps; large binaries that describe a database, not this commit",
    ),
    # These were excluded ONLY by .gitignore until an exhaustive sweep found
    # them sitting outside both registries -- precisely the "buried in a
    # .gitignore line nobody reads" failure this file's docstring says it
    # exists to prevent. Declaring them here is the fix.
    Exclusion(
        ".cache/",
        "gate + tool caches (226 MB): keyed by machine-local input "
        "fingerprints, so identical commits produce different bytes per host",
    ),
    Exclusion(
        ".claude/run/",
        "AST-daemon runtime dir, incl. a 75 MB codebase_kb/facts.pl rebuilt "
        "from the tracked AST snapshot -- derived FROM an artifact, not one",
    ),
    Exclusion(
        ".flake_recoveries.ndjson",
        "solo-retry flake log: appended per run, so its content is a "
        "function of how often the suite ran, not of the commit",
    ),
    Exclusion(
        ".claude/settings.local.json",
        "per-machine local overrides of shared settings, by definition not a "
        "function of the commit",
    ),
    Exclusion(
        "factory_ledger.jsonl / factory_attestations.jsonl / factory_kb/",
        "SPEC-033 build-factory ledgers: append-only wall-clock records of "
        "what a run did, written to the CWD of whoever ran it",
    ),
    Exclusion(
        "pipeline_checkpoints/ + pipeline_output/",
        "per-run pipeline state and products, keyed by run id",
    ),
    Exclusion(
        "reviews/",
        "per-run review outputs (1,730 files): a function of how many "
        "reviews have been run",
    ),
)


def _expand(artifact: Artifact, repo_root: Path) -> list[tuple[Artifact, str]]:
    """Resolve one registry entry to the concrete paths to check.

    A plain path yields itself. A GLOB (any entry containing ``*``) yields
    every match, because several generators emit a whole class of files --
    ``scripts/generate_pytest.py`` alone writes 268 of them under
    ``tests/generated_*/`` -- and enumerating those by hand would be a
    registry nobody maintains.

    A glob that matches nothing yields the glob itself as a single unmatched
    entry, so it is reported rather than vanishing. That direction matters:
    a glob silently expanding to zero paths would let the gate examine
    nothing while still printing a pass, which is the defect this whole file
    is built to refuse.
    """
    if "*" not in artifact.path:
        return [(artifact, artifact.path)]
    matches = sorted(
        str(p.relative_to(repo_root))
        for p in repo_root.glob(artifact.path)
        if p.is_file()
    )
    return [(artifact, m) for m in matches] or [(artifact, artifact.path)]


def _git(args: list[str], repo_root: Path) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        ["git", *args],
        cwd=repo_root,
        capture_output=True,
        text=True,
        check=False,
    )


def _is_ignored(path: str, repo_root: Path) -> str | None:
    """The ``.gitignore`` rule excluding *path*, or ``None``.

    ``git check-ignore -v`` prints ``<source>:<line>:<pattern>\\t<path>`` when
    the path is ignored, which is what makes the violation actionable -- the
    fix is to delete that specific line.
    """
    result = _git(["check-ignore", "-v", "--no-index", path], repo_root)
    if result.returncode != 0:
        return None
    first = result.stdout.strip().splitlines()
    return first[0].split("\t")[0] if first else "(unknown rule)"


def _is_tracked(path: str, repo_root: Path) -> bool:
    return _git(["ls-files", "--error-unmatch", path], repo_root).returncode == 0


def check(
    artifacts: tuple[Artifact, ...] | None = None,
    repo_root: Path | None = None,
) -> tuple[list[str], int]:
    """Return ``(violations, examined)``.

    *examined* is returned so the caller can refuse to report a pass it did
    not earn -- see the module docstring.

    ``artifacts``/``repo_root`` are injection seams: the tests drive this
    against a REAL temporary git repository with real ``.gitignore`` rules,
    rather than patching module state (RUL-002 forbids monkeypatch, and a
    gate whose tests patch its internals is not evidence the gate works).
    """
    registry = GENERATED_ARTIFACTS if artifacts is None else artifacts
    root = REPO_ROOT if repo_root is None else repo_root
    violations: list[str] = []
    examined = 0

    for entry in registry:
        for artifact, path in _expand(entry, root):
            on_disk = (root / path).exists()
            tracked = _is_tracked(path, root)
            if not on_disk and not tracked:
                # Neither present nor tracked: the generator has never run
                # here. Not a RUL-030 violation (nothing is being kept out),
                # but it is not evidence of compliance either, so it does not
                # count as examined.
                continue

            examined += 1
            rule = _is_ignored(path, root)
            if rule is not None:
                violations.append(
                    f"{path}: IGNORED by {rule} — a generated artifact "
                    f"cannot be excluded from git (RUL-030). Generator: "
                    f"{artifact.generator}; read by: {artifact.consumer}."
                )
                continue
            if not tracked:
                violations.append(
                    f"{path}: exists but is UNTRACKED — generated and "
                    f"never added (RUL-030). Generator: {artifact.generator}."
                )

    return violations, examined


# --- forward check: every registered generator must still exist -----------


def _generator_path_hint(generator: str) -> str | None:
    """The leading repo-relative path in *generator*, if it looks like one.

    Registry entries mix three shapes: a script invocation ("scripts/x.py
    subcommand"), an external tool ("uv", "terraform init"), and prose
    ("legacy monorepo RTM tooling (frozen; not re-runnable here)"). Only the
    first shape names something this repo can check for -- a bare command or
    a sentence has no file to look up, and a ``~``-relative path names
    something outside the repo entirely. The heuristic: take the first
    whitespace-delimited token, and treat it as checkable only when it
    contains a path separator -- a bare word like "uv" or "terraform" never
    does, and every observed script-invocation entry does.
    """
    tokens = generator.split()
    if not tokens:
        return None
    token = tokens[0]
    if token.startswith("~") or "/" not in token:
        return None
    return token


#: Prefix tags on a provenance violation line, load-bearing for two things:
#: a human skimming ``  - `` bullets sees the verdict before the prose, and
#: :func:`main` counts them to build the loud summary banner. LOSS means a
#: matching path was deleted from git history (see :func:`_git_deleted_path`)
#: -- a one-line fix, `git checkout <sha>~1 -- <path>`. NEVER-EXISTED means
#: no such path was ever committed here: the claim itself may be wrong, and
#: that is a different, harder conversation than "restore what we deleted".
#: Conflating the two wastes the reader's time deciding which question to
#: even ask.
_LOSS_MARKER = "[LOSS]"
_NEVER_EXISTED_MARKER = "[NEVER-EXISTED]"


def _generator_path_candidates(token: str) -> tuple[str, ...]:
    """Repo-relative path strings worth trying for *token*.

    Shared by :func:`_generator_reference_exists`'s dotted-module branch and
    :func:`_git_deleted_path`'s git-history search, so the two "where might
    this generator live" guesses can never silently drift apart. Every
    observed header/registry token omits the ``scripts/``/``src/`` prefix,
    so each known generator home is tried in turn, as a bare filename, as a
    package directory, and as a single module file.
    """
    if "/" in token:
        return (token,)
    if "." not in token:
        return ()
    as_path = "/".join(token.split("."))
    candidates: list[str] = []
    for base_dir in _GENERATOR_SEARCH_ROOTS:
        candidates.append(f"{base_dir}/{token}")
        candidates.append(f"{base_dir}/{as_path}")
        candidates.append(f"{base_dir}/{as_path}.py")
    return tuple(candidates)


def _git_deleted_path(token: str, repo_root: Path) -> tuple[str, str, str] | None:
    """If a path matching *token* was DELETED from git history, return
    ``(sha, date, path)`` for the commit that deleted it -- proof of LOSS
    rather than NEVER-EXISTED (see :data:`_LOSS_MARKER`). Searched with
    ``--all`` (every branch, not just the current one) and
    ``--diff-filter=D`` (only commits that removed the path), so a generator
    deleted on a branch that never merged still counts as a loss. The first
    candidate with any hit wins; ``git log`` lists newest first, so that hit
    is the most recent deletion.
    """
    for candidate in _generator_path_candidates(token):
        result = _git(
            [
                "log",
                "--all",
                "--diff-filter=D",
                "--format=%H|%ad",
                "--date=short",
                "--",
                candidate,
            ],
            repo_root,
        )
        if result.returncode != 0 or not result.stdout.strip():
            continue
        sha, _, date = result.stdout.strip().splitlines()[0].partition("|")
        return sha, date, candidate
    return None


def _related_requirements(paths: list[str], repo_root: Path) -> list[str]:
    """Best-effort ``REQ-*`` ids that *paths* serve, read from
    ``docs/rtm.json`` -- the ONE materialized traceability corpus this
    project already maintains (``core/traceability_utils.py save_rtm()``,
    the same corpus ``scripts/check_spec_authority.py`` and
    ``scripts/check_requirement_corpus.py`` read from ``@traces``
    decorators). Never invents an id: an empty result means the corpus does
    not currently link anything to these paths, not that nothing does.
    """
    rtm_path = repo_root / "docs" / "rtm.json"
    try:
        data = json.loads(rtm_path.read_text(encoding="utf-8"))
    except OSError, json.JSONDecodeError:
        return []

    wanted = {str(Path(p)) for p in paths}
    req_ids: set[str] = set()
    for entry in data.get("entries", ()):
        file_path = str(entry.get("file_path", ""))
        if any(file_path == w or file_path.endswith(f"/{w}") for w in wanted):
            req_id = entry.get("req_id", "")
            if req_id and req_id != "INVALID-ID":
                req_ids.add(req_id)
    return sorted(req_ids)


def _provenance_violation(generator: str, paths: list[str], repo_root: Path) -> str:
    """One loud, actionable line for a generator that does not resolve.

    Leads with the LOSS/NEVER-EXISTED tag (see :data:`_LOSS_MARKER`), never
    truncates the affected-artifact count silently (a capped sample plus an
    explicit "+N more"), names the ``REQ-*`` id it serves when the
    traceability corpus knows one, and states the remedy explicitly --
    including that retiring an artifact needs the user's approval, because
    this gate reports and never deletes anything itself.
    """
    count = len(paths)
    sample = ", ".join(paths[:3])
    more = f" (+{count - 3} more)" if count > 3 else ""

    deletion = _git_deleted_path(generator, repo_root)
    if deletion is not None:
        sha, date, deleted_path = deletion
        tag = _LOSS_MARKER
        origin = (
            f"deleted in commit {sha[:12]} on {date} "
            f"(inspect: git show {sha[:12]} -- {deleted_path})"
        )
        remedy = f"restore it (git checkout {sha}~1 -- {deleted_path})"
    else:
        tag = _NEVER_EXISTED_MARKER
        origin = "no matching path was ever committed to this repository"
        remedy = "the header/registry claim naming it may itself be wrong"

    reqs = _related_requirements(paths, repo_root)
    req_note = f" Serves: {', '.join(reqs)}." if reqs else ""

    return (
        f"{tag} generator '{generator}' does not exist ({origin}), but "
        f"{count} artifact(s) declare it: {sample}{more}.{req_note} Remedy: "
        f"{remedy}, OR retire the artifacts that cite it -- retiring "
        f"requires the USER'S EXPLICIT APPROVAL; this gate reports, it "
        f"never deletes."
    )


def check_generators_exist(
    artifacts: tuple[Artifact, ...] | None = None,
    repo_root: Path | None = None,
) -> tuple[list[str], int]:
    """Return ``(violations, examined)`` for the FORWARD provenance check.

    This is the direction 6a186c9d broke: a generator was deleted while its
    artifacts were still in the tree, and nothing asserted the generator
    itself still existed. For every registry entry with a checkable
    generator path, that path must be present on disk -- missing it means
    every artifact citing that generator is now orphaned, and the violation
    names how many, whether it is a LOSS or a NEVER-EXISTED claim, and the
    ``REQ-*`` id it serves when the traceability corpus knows one.

    ``artifacts``/``repo_root`` are the same injection seams :func:`check`
    uses, for the same reason: real fixtures under ``tmp_path``, never
    patched module state. This check does NOT consult ``core/gate_waivers``
    -- see the module docstring's "AUTHORITY" section for why a loss-detector
    must not be waivable.
    """
    registry = GENERATED_ARTIFACTS if artifacts is None else artifacts
    root = REPO_ROOT if repo_root is None else repo_root
    violations: list[str] = []
    examined = 0

    for entry in registry:
        hint = _generator_path_hint(entry.generator)
        if hint is None:
            continue  # external tool or prose -- nothing on disk to check
        examined += 1
        if (root / hint).exists():
            continue

        expanded = _expand(entry, root)
        paths = [path for _, path in expanded]
        violations.append(_provenance_violation(hint, paths, root))

    return violations, examined


# --- discovery check: headers claiming machine generation ------------------

#: Lines scanned per file. Headers, not fixture data buried in a test body --
#: bounding the read is also what keeps this check cheap on a 1.6 MB tracked
#: artifact like docs/CODEBASE_MAP_FULL.md.
_HEADER_WINDOW = 30

#: File kinds worth reading a header from. Deliberately excludes binaries
#: (``.svg``, ``.gz``, ``.whl``, ...) -- a text-mode ``readline()`` against a
#: compressed blob is both meaningless and, without a newline for a long
#: stretch, needlessly slow.
_SCANNABLE_SUFFIXES = frozenset(
    {
        ".py",
        ".pyi",
        ".md",
        ".sh",
        ".pl",
        ".j2",
        ".proto",
        ".yaml",
        ".yml",
        ".json",
        ".ts",
        ".tsx",
        ".js",
        ".txt",
        ".toml",
    }
)

#: The known generator homes -- every producer name observed in a header
#: omits this prefix (``ast_serializer.testgen`` really means
#: ``scripts/ast_serializer/testgen``), so resolution tries each in turn.
_GENERATOR_SEARCH_ROOTS = ("scripts", "src", "claude", ".claude")


def _read_header(path: Path, window: int = _HEADER_WINDOW) -> str:
    """The first *window* lines of *path*, decoded best-effort.

    Bounded reads only -- ``readline()`` in a loop, never ``.read()`` or
    ``.readlines()``, so a multi-MB tracked artifact costs a few dozen
    lines, not the whole file.
    """
    lines: list[str] = []
    try:
        with path.open("r", encoding="utf-8", errors="ignore") as handle:
            for _ in range(window):
                line = handle.readline()
                if not line:
                    break
                lines.append(line)
    except OSError:
        return ""
    return "".join(lines)


def _extract_header_generator(head: str) -> str | None:
    """The producer named by a header's "Generated by X" claim, or ``None``
    if the header makes no such claim (or only a prose one -- see
    :data:`_HEADER_GENERATOR_RE`)."""
    match = _HEADER_GENERATOR_RE.search(head)
    return match.group(1) if match else None


def _generator_reference_exists(token: str, repo_root: Path) -> bool:
    """Resolve a header-claimed generator name to real source on disk.

    Headers name a generator three ways, observed across the corpus: a
    literal relative path, a bare filename with no directory
    (``generate_pytest.py``), or dotted-module notation with no file
    extension (``ast_serializer.testgen``). All three are tried against the
    known generator homes (:data:`_GENERATOR_SEARCH_ROOTS`), because every
    observed header omits that prefix.

    A directory candidate only counts as existing when it holds at least one
    real ``.py`` file. An empty package shell -- a stale ``__pycache__/``
    left behind by a deleted generator, directory present but no source --
    is NOT existence. That distinction is exactly what 6a186c9d's "zero
    consumers" audit missed, and exactly what would make this check a false
    pass if skipped.
    """
    if (repo_root / token).exists():
        return True
    if "/" in token or "." not in token:
        return False  # no relative-path shape left worth trying

    # Bare filename with an extension, e.g. "generate_pytest.py".
    for base_dir in _GENERATOR_SEARCH_ROOTS:
        search_root = repo_root / base_dir
        if search_root.is_dir() and any(search_root.rglob(token)):
            return True

    # Dotted-module notation, e.g. "ast_serializer.testgen".
    as_path = Path(*token.split("."))
    for base_dir in _GENERATOR_SEARCH_ROOTS:
        base = repo_root / base_dir / as_path
        if base.with_suffix(".py").exists():
            return True
        if base.is_dir() and any(base.rglob("*.py")):
            return True

    return False


def scan_generator_headers(
    repo_root: Path | None = None,
    source_dirs: tuple[str, ...] = ("tests", "src", "docs"),
) -> dict[str, list[str]]:
    """Map every header-claimed generator to the files that name it.

    This is the DISCOVERY half of the provenance check -- the one that
    would actually have caught 6a186c9d, because it does not trust the
    registry at all. It reads the artifacts' own headers instead: a file
    that says "Generated by ast_serializer.testgen" makes that claim
    regardless of whether anyone remembered to register it in
    :data:`GENERATED_ARTIFACTS`.
    """
    root = REPO_ROOT if repo_root is None else repo_root
    found: dict[str, list[str]] = {}

    for source_dir in source_dirs:
        base = root / source_dir
        if not base.is_dir():
            continue
        for path in sorted(base.rglob("*")):
            if not path.is_file() or path.suffix not in _SCANNABLE_SUFFIXES:
                continue
            generator = _extract_header_generator(_read_header(path))
            if generator is None:
                continue
            found.setdefault(generator, []).append(str(path.relative_to(root)))

    return found


def check_generator_headers(
    repo_root: Path | None = None,
    source_dirs: tuple[str, ...] = ("tests", "src", "docs"),
) -> tuple[list[str], int]:
    """Return ``(violations, examined)`` for the header-DISCOVERY check.

    ``examined`` counts distinct producer NAMES discovered, not files -- one
    deleted generator can be declared by a hundred files, and the violation
    message already states that count; counting every file as its own
    "examination" would only inflate the number without adding information.
    Like :func:`check_generators_exist`, this does NOT consult
    ``core/gate_waivers`` -- non-waivable by design, see the module
    docstring's "AUTHORITY" section.
    """
    root = REPO_ROOT if repo_root is None else repo_root
    found = scan_generator_headers(root, source_dirs)
    violations: list[str] = []

    for generator, paths in sorted(found.items()):
        if _generator_reference_exists(generator, root):
            continue
        violations.append(_provenance_violation(generator, paths, root))

    return violations, len(found)


#: Sinks that PUBLISH a file by replacing it. ``os.replace`` gives the
#: destination a NEW inode, so a reader already holding the old one keeps
#: reading a complete file instead of faulting.
_ATOMIC_SINKS = frozenset(
    {
        "dump_json_atomic",
        "dump_text_atomic",
        "_atomic_write_json",
        "_atomic_write_text",
        "_atomic_write",
        "replace",
    }
)

#: Calls that truncate the destination IN PLACE, keeping its inode.
_TRUNCATING_METHODS = frozenset({"write_text", "write_bytes"})


def _truncating_writes(source: str, basename: str) -> list[int]:
    """Line numbers where *source* truncates its declared artifact in place.

    A write counts when the DESTINATION expression either names the artifact
    literally (``ROOT / "docs" / "CODEBASE_MAP.md"``) or is one of the
    conventional output holders in :data:`_OUTPUT_NAMES` (``args.output``).

    LIMITATION, stated because a silent gap reads as a pass: a destination
    reached through neither route -- a path stored in a dict, or computed in a
    helper and returned -- is NOT seen. This check is deliberately quiet
    rather than noisy; it is a floor on the obvious shapes, not a proof of
    absence. The same posture ``check_generators_exist`` takes with generator
    strings it cannot resolve.
    """
    try:
        tree = ast.parse(source)
    except SyntaxError:
        return []

    # Names that PROVABLY hold this artifact's path, resolved from the script
    # rather than guessed from the identifier. Guessing was tried and was
    # wrong: accepting any ``args.output`` produced 36 false positives in one
    # run, because `serialize_codebase_ast.py` writes ``args.output`` from a
    # dozen code-GENERATION subcommands (gen-dto, gen-serde, diagram, ...)
    # that have nothing to do with the AST snapshot, and it is the registered
    # generator for three artifacts, so each write was attributed to all
    # three. A matcher that reads an argument NAME as intent repeats the
    # defect this gate exists to catch elsewhere.
    holders: set[str] = set()
    constants: dict[str, str] = {}

    def _as_path_string(node: ast.AST | None) -> str | None:
        """A literal path, whether bare or wrapped in ``Path(...)``.

        Generators declare defaults both ways (``default="reports/x.json"``
        and ``default=Path("reports/x.json")``); reading only the bare form
        left three of five known writers unresolved.
        """
        if isinstance(node, ast.Constant) and isinstance(node.value, str):
            return node.value
        if isinstance(node, ast.Name):
            return constants.get(node.id)
        if (
            isinstance(node, ast.Call)
            and isinstance(node.func, ast.Name)
            and node.func.id in {"Path", "PurePath"}
            and node.args
        ):
            return _as_path_string(node.args[0])
        # ``ROOT / "plans" / "rtm.json"`` — only the RIGHT operand matters,
        # since the basename is what identifies the artifact.
        if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div):
            return _as_path_string(node.right)
        return None

    for node in ast.walk(tree):
        if isinstance(node, ast.Assign):
            value = _as_path_string(node.value)
            if value:
                for tgt in node.targets:
                    if isinstance(tgt, ast.Name):
                        constants[tgt.id] = value
    for name, value in constants.items():
        if Path(value).name == basename:
            holders.add(name)
    # argparse: --json-out default=DEFAULT_JSON_OUT  ->  args.json_out
    for node in ast.walk(tree):
        if not (
            isinstance(node, ast.Call)
            and isinstance(node.func, ast.Attribute)
            and node.func.attr == "add_argument"
            and node.args
            and isinstance(node.args[0], ast.Constant)
            and isinstance(node.args[0].value, str)
        ):
            continue
        flag = node.args[0].value
        if not flag.startswith("--"):
            continue
        default = next((k.value for k in node.keywords if k.arg == "default"), None)
        resolved = _as_path_string(default)
        if not (resolved and Path(resolved).name == basename):
            continue
        # An explicit ``dest=`` WINS over the flag spelling. Deriving the name
        # from the flag alone missed `add_argument("--json", dest="json_out",
        # default=DEFAULT_JSON_OUT)` -- it bound "json", while the code writes
        # `args.json_out`.
        explicit = next((k.value for k in node.keywords if k.arg == "dest"), None)
        if isinstance(explicit, ast.Constant) and isinstance(explicit.value, str):
            holders.add(explicit.value)
        else:
            holders.add(flag[2:].replace("-", "_"))

    # Function parameters whose DEFAULT is the artifact: `derive_rtm.build`
    # takes `out_json: Path = DEFAULT_OUT_JSON` and writes to that name.
    for node in ast.walk(tree):
        if not isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
            continue
        spec = node.args
        positional = spec.posonlyargs + spec.args
        pairs = list(
            zip(
                positional[len(positional) - len(spec.defaults) :],
                spec.defaults,
                strict=False,
            )
        )
        pairs += [
            (a, d) for a, d in zip(spec.kwonlyargs, spec.kw_defaults, strict=False) if d
        ]
        for arg, default_node in pairs:
            value = _as_path_string(default_node)
            if value and Path(value).name == basename:
                holders.add(arg.arg)

    def _names_the_artifact(dest: str) -> bool:
        if basename in dest:
            return True
        return any(re.search(rf"\b{re.escape(h)}\b", dest) for h in holders)

    hits: list[int] = []
    for node in ast.walk(tree):
        if not isinstance(node, ast.Call):
            continue
        dest: str | None = None
        func = node.func
        if isinstance(func, ast.Attribute) and func.attr in _TRUNCATING_METHODS:
            dest = ast.unparse(func.value)
        elif isinstance(func, ast.Name) and func.id == "open" and len(node.args) > 1:
            mode = node.args[1]
            if (
                isinstance(mode, ast.Constant)
                and isinstance(mode.value, str)
                and mode.value[:1] in {"w", "a"}
            ):
                dest = ast.unparse(node.args[0])
        if dest and _names_the_artifact(dest):
            hits.append(node.lineno)
    return hits


def check_atomic_writes(
    repo_root: Path | None = None,
    artifacts: tuple[Artifact, ...] | None = None,
) -> tuple[list[str], int]:
    """Return ``(violations, examined)`` — no generator may truncate in place.

    RUL-030 already says a generated artifact belongs in git. This adds the
    property that makes it SAFE to regenerate one while the tree is live: the
    write must replace the destination, never truncate it.

    ``Path.write_text`` and ``open(p, "w")`` truncate at the call and keep the
    inode. A reader that already mmap'd that inode -- ``git update-index``
    hashing the file to stage it -- then faults on a page past the new end and
    is killed with ``SIGBUS``. Measured 2026-08-19: two ``git update-index``
    cores 28 minutes apart, one inside ``zng_deflate``; hardware excluded on
    four counters (NVMe media errors 0, btrfs corruption/csum/generation 0,
    scrub clean over 344 GiB, SMART PASSED). Reproduced deterministically in
    ``tests/core/test_atomic_json.py`` -- truncating write gives the reader
    ``-SIGBUS`` and REUSES the inode; ``dump_text_atomic`` gives it ``0`` and a
    new one.

    This gate is why the fix stays fixed: the generators were corrected once
    on 2026-08-21, and nothing but a mechanism stops the next one regressing.
    """
    root = repo_root if repo_root is not None else REPO_ROOT
    registry = artifacts if artifacts is not None else GENERATED_ARTIFACTS

    violations: list[str] = []
    examined = 0
    for artifact in registry:
        hint = _generator_path_hint(artifact.generator)
        if hint is None:
            continue
        script = next(
            (
                root / candidate
                for candidate in _generator_path_candidates(hint)
                if (root / candidate).is_file()
            ),
            None,
        )
        if script is None:
            continue
        examined += 1
        lines = _truncating_writes(
            script.read_text(encoding="utf-8", errors="replace"),
            Path(artifact.path).name,
        )
        for line in lines:
            violations.append(
                f"{script.relative_to(root)}:{line} truncates "
                f"{artifact.path} in place — a reader holding it mmap'd dies "
                f"with SIGBUS. Publish it with one of "
                f"{', '.join(sorted(_ATOMIC_SINKS))} instead."
            )
    return violations, examined


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        description="RUL-030: generated artifacts belong in git. "
        "Non-waivable: no --warn-only, no # gate-ok: override."
    )
    parser.add_argument("--json", action="store_true", help="machine-readable output")
    args = parser.parse_args(argv)

    if not GENERATED_ARTIFACTS:
        print(
            "[generated-artifacts] INVALID — the registry is empty, so a pass "
            "would mean nothing was checked",
            file=sys.stderr,
        )
        return EXIT_INVALID

    violations, examined = check()
    gen_violations, gen_examined = check_generators_exist()
    header_violations, header_examined = check_generator_headers()
    atomic_violations, atomic_examined = check_atomic_writes()
    all_violations = violations + gen_violations + header_violations + atomic_violations
    provenance_violations = gen_violations + header_violations

    if args.json:
        print(
            json.dumps(
                {
                    "examined": examined,
                    "registered": len(GENERATED_ARTIFACTS),
                    "excluded": len(NOT_ARTIFACTS),
                    "violations": violations,
                    "generators_examined": gen_examined,
                    "generator_violations": gen_violations,
                    "header_generators_examined": header_examined,
                    "header_generator_violations": header_violations,
                    "atomic_generators_examined": atomic_examined,
                    "atomic_write_violations": atomic_violations,
                    "loss_count": sum(
                        1 for v in provenance_violations if v.startswith(_LOSS_MARKER)
                    ),
                    "never_existed_count": sum(
                        1
                        for v in provenance_violations
                        if v.startswith(_NEVER_EXISTED_MARKER)
                    ),
                },
                indent=2,
            )
        )
    # "Could not determine the answer" is a FAIL, never a quiet pass -- an
    # empty registry (checked above), a registry that examined nothing, or a
    # provenance check that found no checkable generator and no header claim
    # ANYWHERE under tests/src/docs are each, on their own terms, "the gate
    # did not do its job" rather than "nothing is wrong".
    if examined == 0:
        print(
            f"[generated-artifacts] INVALID — {len(GENERATED_ARTIFACTS)} "
            "artifact(s) registered but none present or tracked; a PASS here "
            "would report 'nothing wrong' when the truth is 'nothing looked at'",
            file=sys.stderr,
        )
        return EXIT_INVALID
    if gen_examined == 0 and header_examined == 0:
        print(
            "[generated-artifacts] INVALID — the generator-provenance checks "
            "examined nothing: no registry entry named a checkable generator "
            "path, and no header claiming machine generation was found under "
            "tests/, src/, or docs/. That is 'nothing looked at', not "
            "'nothing wrong'.",
            file=sys.stderr,
        )
        return EXIT_INVALID

    if all_violations:
        if not args.json:
            loss_n = sum(1 for v in provenance_violations if v.startswith(_LOSS_MARKER))
            never_n = sum(
                1 for v in provenance_violations if v.startswith(_NEVER_EXISTED_MARKER)
            )
            if provenance_violations:
                print("=" * 72, file=sys.stderr)
                print(
                    f"GENERATOR PROVENANCE LOSS — {loss_n} generator(s) "
                    f"CONFIRMED DELETED from git history, {never_n} "
                    "generator(s) NEVER EXISTED in this repository",
                    file=sys.stderr,
                )
                print("=" * 72, file=sys.stderr)
            print(
                f"[generated-artifacts] FAIL — {len(all_violations)} "
                f"violation(s) across {examined} tracked artifact(s), "
                f"{gen_examined} registry generator(s), and {header_examined} "
                "header-claimed generator(s) examined:",
                file=sys.stderr,
            )
            for violation in all_violations:
                print(f"  - {violation}", file=sys.stderr)
            print(
                "\nFix: delete the .gitignore line, `git add` the artifact, "
                "restore a LOST generator from the commit named above, or "
                "retire the artifacts that cite it -- retiring requires the "
                "USER'S EXPLICIT APPROVAL; this gate never deletes anything "
                "itself. If a tracked path genuinely is not a function of "
                "the commit, add it to NOT_ARTIFACTS in this file WITH A "
                "REASON. This gate is non-waivable: no # gate-ok:, no "
                "--warn-only.",
                file=sys.stderr,
            )
        return EXIT_VIOLATIONS

    if not args.json:
        print(
            "[generated-artifacts] OK — RUL-030 and generator provenance "
            f"satisfied: {examined} tracked artifact(s), {gen_examined} "
            f"registry generator(s), and {header_examined} header-claimed "
            f"generator(s) all check out; {len(NOT_ARTIFACTS)} declared "
            "non-artifact(s) excluded by reason"
        )
    return EXIT_PASS


if __name__ == "__main__":
    raise SystemExit(main())