scripts.check_requirement_corpus

Reference › scripts › check_requirement_corpus

The requirement-corpus gate: production code must not cite a requirement that the traceability matrix cannot grade. It takes the cited requirement ids from src/ and checks them against the ratified specification corpus, using the harvest functions in generate_rtm_evidence. tests/scripts/test_check_requirement_corpus.py and tests/scripts/test_check_requirement_corpus_main.py import it.

Invocation: main takes --advisories, which also prints spec and checklist documentation drift as advisory lines. collect builds a CorpusFacts record, whose dangling view maps each unresolvable id to the files citing it, and check returns the violations with the numbers of ids and citations examined. A run that examined no ids or no citations returns the invalid code, because a pass with nothing examined is not a pass. Violations print a FAIL line and a list of files and return the violations code; otherwise an OK line reports the number of distinct ids cited and the size of the ratified corpus. checklist_advisories produces the advisory lines.

At a glance

Source scripts/check_requirement_corpus.py
Lines of code 218
Classes 1
Functions 4
Imports (internal) scripts.generate_rtm_evidence
Imported by none
Tests that import it tests/scripts/test_check_requirement_corpus.py, tests/scripts/test_check_requirement_corpus_main.py

Class diagram

Class diagram scripts.check_requirement_corpus

Dependency diagram

Dependency diagram scripts.check_requirement_corpus

API

Constants

Name Type Value
NOT_SPECS frozenset[str] frozenset({'codegen', 'spec-review'})

Classes

CorpusFacts (dataclass)

Decorators: dataclass(frozen=True) · Line 75

Attribute Type Default
ratified dict[str, str]
universe frozenset[str]
cited dict[str, list[dict]]
Method Signature Summary
dangling property dangling() -> dict[str, list[dict]] src/ citations of an id the RTM cannot grade.

Functions

Function Signature Summary
collect collect(repo_root: Path | None = None) -> CorpusFacts The ratified corpus, the RTM's universe, and what src/ actually cites.
check check(repo_root: Path | None = None) -> tuple[list[str], int, int] (violations, requirement_ids_examined, distinct_src_citations_examined).
checklist_advisories checklist_advisories(repo_root: Path | None = None) -> list[str] spec.md/checklist documentation drift — reported, never failing.
main main(argv: list[str] | None = None) -> int

Source

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

#!/usr/bin/env python3
"""Requirement-corpus gate — production code must not cite an ungradeable requirement.

This repo had two things deciding "what requirements exist":

* ``serialize_codebase_ast._requirement_source_files`` — the RATIFIED corpus:
  ``<spec>/checklists/requirements.md`` + ``<spec>/spec.md`` + ``<spec>/*-addendum.md``.
* ``generate_rtm_evidence.load_requirements`` — what the RTM actually grades against.

They drifted, and the drift was invisible for as long as nobody compared them.
The toolkit widened its scan after finding that a checklist-only glob made 43
real ``@traces`` look dangling; the RTM generator never received that fix. So
``trace-lint`` called those citations clean while the RTM graded them GHOST,
and 68 requirement ids across 6 files — all of SPEC-033 and SPEC-034, 22 of
SPEC-110, the ``001-kernel`` and ``097`` addenda — sat outside the matrix's
universe. Not UNLINKED. Not GHOST. Absent from the denominator, so every
coverage percentage was computed against the wrong total.

The defect was never a missing checklist. It was two readers of one corpus with
nobody asserting they agree.

WHAT THIS GATE ASSERTS, and why it is this and not the obvious thing:

The obvious gate — compare ``load_requirements()`` against the corpus function
it calls — is a TAUTOLOGY. It can never fail, which is the same false green as
a checker that never looked. So the load-bearing comparison is against
``@traces`` decorators harvested from the AST of ``src/``, which know nothing
about how requirements are loaded and can therefore genuinely disagree:

``DANGLING``
    shipping code cites a requirement id the RTM cannot grade. Either the
    decorator is wrong, or the loader has been narrowed again. Both are
    defects, and this gate fails on either without needing to know which.

Documentation drift between a spec.md and its checklist is an ADVISORY, not a
failure: the loader now reads both shapes, so a lagging checklist no longer
hides requirements from the matrix. It is still worth seeing — SPEC-110's
checklist asserts "29/29 requirements implemented" against a spec.md defining
51 — but it does not break traceability, and a gate that fails on documentation
cadence gets switched off, which costs more than the drift it catches.

Exit codes: 0 pass · 1 violations · 2 INVALID. A run that examined zero
requirement ids or zero citations reports 2, never 0.
"""

from __future__ import annotations

import argparse
import sys
from dataclasses import dataclass
from pathlib import Path

_repo_root = Path(__file__).resolve().parent.parent
if str(_repo_root) not in sys.path:
    sys.path.insert(0, str(_repo_root))

from scripts.generate_rtm_evidence import (  # noqa: E402
    harvest_src_declarations,
    load_requirements,
    requirement_ids_in,
    requirement_source_files,
)

REPO_ROOT = _repo_root

EXIT_PASS = 0
EXIT_VIOLATIONS = 1
EXIT_INVALID = 2

# Directories under specs/ that are not specs and define no requirements.
NOT_SPECS: frozenset[str] = frozenset({"codegen", "spec-review"})


@dataclass(frozen=True)
class CorpusFacts:
    ratified: dict[str, str]  # id -> defining file
    universe: frozenset[str]  # ids load_requirements() returns
    cited: dict[str, list[dict]]  # id -> src/ @traces sites citing it

    @property
    def dangling(self) -> dict[str, list[dict]]:
        """src/ citations of an id the RTM cannot grade."""
        return {r: s for r, s in self.cited.items() if r not in self.universe}


def collect(repo_root: Path | None = None) -> CorpusFacts:
    """The ratified corpus, the RTM's universe, and what src/ actually cites.

    Gathered by INDEPENDENT means on purpose — see the module docstring on why
    comparing the universe against its own source function proves nothing.
    """
    root = repo_root or REPO_ROOT
    specs_dir = root / "specs"
    src_dir = root / "src"

    ratified: dict[str, str] = {}
    if specs_dir.is_dir():
        for source in requirement_source_files(specs_dir):
            try:
                text = source.read_text(encoding="utf-8")
            except OSError, UnicodeDecodeError:
                continue
            for rid in requirement_ids_in(text):
                ratified.setdefault(rid, str(source.relative_to(root)))

    universe = (
        frozenset(load_requirements(specs_dir)) if specs_dir.is_dir() else frozenset()
    )
    cited = dict(harvest_src_declarations(src_dir)) if src_dir.is_dir() else {}
    return CorpusFacts(ratified=ratified, universe=universe, cited=cited)


def check(repo_root: Path | None = None) -> tuple[list[str], int, int]:
    """(violations, requirement_ids_examined, distinct_src_citations_examined)."""
    facts = collect(repo_root)
    violations: list[str] = []

    root = repo_root or REPO_ROOT
    by_file: dict[str, set[str]] = {}
    for rid, sites in facts.dangling.items():
        for site in sites:
            raw = Path(str(site.get("file", "?")))
            # Report repo-relative: an absolute path per line makes 29
            # violations unreadable, and the reader needs the path they would
            # actually open.
            try:
                shown_path = str(raw.relative_to(root))
            except ValueError:
                shown_path = str(raw)
            by_file.setdefault(shown_path, set()).add(rid)

    for path, ids in sorted(by_file.items()):
        uniq = sorted(ids)
        shown = ", ".join(uniq[:6])
        more = f" (+{len(uniq) - 6} more)" if len(uniq) > 6 else ""
        violations.append(
            f"DANGLING {path}: @traces cites {len(uniq)} requirement id(s) the "
            f"RTM cannot grade — {shown}{more}"
        )

    return violations, len(facts.ratified), len(facts.cited)


def checklist_advisories(repo_root: Path | None = None) -> list[str]:
    """spec.md/checklist documentation drift — reported, never failing."""
    root = repo_root or REPO_ROOT
    specs_dir = root / "specs"
    out: list[str] = []
    if not specs_dir.is_dir():
        return out
    for spec_dir in sorted(p for p in specs_dir.iterdir() if p.is_dir()):
        if spec_dir.name in NOT_SPECS:
            continue
        spec_md = spec_dir / "spec.md"
        checklist = spec_dir / "checklists" / "requirements.md"
        if not spec_md.is_file() or not checklist.is_file():
            continue
        try:
            spec_ids = set(requirement_ids_in(spec_md.read_text(encoding="utf-8")))
            list_ids = set(requirement_ids_in(checklist.read_text(encoding="utf-8")))
        except OSError, UnicodeDecodeError:
            continue
        missing = sorted(spec_ids - list_ids)
        if missing:
            shown = ", ".join(missing[:6])
            more = f" (+{len(missing) - 6} more)" if len(missing) > 6 else ""
            out.append(
                f"{spec_dir.name}: checklist omits {len(missing)} id(s) its "
                f"spec.md defines — {shown}{more}"
            )
    return out


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument(
        "--advisories",
        action="store_true",
        help="Also print spec.md/checklist documentation drift",
    )
    args = ap.parse_args(argv)

    violations, ids_examined, citations_examined = check()

    if args.advisories:
        for a in checklist_advisories():
            print(f"[requirement-corpus] advisory — {a}")

    if ids_examined == 0 or citations_examined == 0:
        print(
            "[requirement-corpus] INVALID — examined "
            f"{ids_examined} requirement id(s) and {citations_examined} "
            "src/ citation(s). A pass with nothing examined is not a pass.",
            file=sys.stderr,
        )
        return EXIT_INVALID

    if violations:
        print(
            f"[requirement-corpus] FAIL — {len(violations)} file(s) cite "
            f"requirements the RTM cannot grade "
            f"({ids_examined} id(s) / {citations_examined} citation(s) examined):",
            file=sys.stderr,
        )
        for v in violations:
            print(f"  {v}", file=sys.stderr)
        return EXIT_VIOLATIONS

    print(
        f"[requirement-corpus] OK — {citations_examined} distinct requirement "
        f"id(s) cited by src/, all gradeable against the "
        f"{ids_examined}-id ratified corpus"
    )
    return EXIT_PASS


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