scripts.derive_rtm

Reference › scripts › derive_rtm

Derives requirements-traceability-matrix attribution over the merged static and runtime call graph. Starting from the code that declares @traces for a requirement, it follows calls to attribute the requirement to the code reachable from it. tests/scripts/test_derive_rtm.py imports it; it uses trace_intel and writes JSON through atomic_json. See also build_runtime_callgraph, which supplies the runtime half of the graph.

Invocation: main has one subcommand, build, which writes a JSON result and a Markdown report (_cmd_build, build); the paths of the structural and traceability fact files can be overridden. With no subcommand it prints help and exits 1.

The flow: parse_call_graph reads the structural facts into an adjacency map and a map from function to module. parse_traceability reads the traceability facts into the functions that declare each requirement and the specs that define each requirement (_spec_name_map resolves spec names). compute_anchors combines these into the anchor functions for each requirement. compute_attribution runs a breadth-first search (_bfs) outward from the anchors over the call graph and records which requirements reach which functions and modules. write_report renders the outcome.

At a glance

Source scripts/derive_rtm.py
Lines of code 425
Classes 0
Functions 11
Imports (internal) orchestrata_ultimate.core.atomic_json, scripts, scripts.trace_intel
Imported by none
Tests that import it tests/scripts/test_derive_rtm.py

Class diagram

No classes; see Functions.

Dependency diagram

Dependency diagram scripts.derive_rtm

API

Functions

Function Signature Summary
parse_call_graph parse_call_graph(structural_facts_text: str) -> tuple[dict[str, set[str]], dict[str, str]] Parse calls_resolved/3 ∪ runtime_call/2 into a merged adjacency (caller -> {callees}) and symbol/7 class/function/ method rows into a qname -> module map.
parse_traceability parse_traceability(traceability_text: str) -> tuple[dict[str, set[str]], dict[str, set[str]]] Parse traces('Q', 'REQ') (qname -> {req}) and spec_requirement('SPEC', 'REQ', ...) (req -> {spec}).
_spec_name_map _spec_name_map(specs_root: Path) -> dict[str, str] specs/<id>-<name>/ directory names -> {id: name}.
compute_anchors compute_anchors(traces: dict[str, set[str]], req_specs: dict[str, set[str]]) -> dict[str, set[str]] A spec's anchors: every @traces-linked qname whose requirement belongs to that spec.
_bfs _bfs(seeds: set[str], adj: dict[str, set[str]]) -> set[str] Forward BFS over the merged adjacency, seeded from seeds.
compute_attribution compute_attribution(adj: dict[str, set[str]], module_of: dict[str, str], anchors: dict[str, set[str]], traced: set[str]) -> tuple[dict[str, set[str]], dict[str, list[str]], dict[str, list[str]]] Returns (closure, attributed, aligned) per the module docstring's method section — closure is the full merged-reachable set per spec (traced and untraced), attributed/aligned are…
build build(out_json: Path = DEFAULT_OUT_JSON, out_report: Path = DEFAULT_REPORT, structural_facts: Path | None = None, traceability_facts: Path | None = None, specs_root: Path | None = None) -> dict[str, Any] Compute the attribution and write both output artifacts.
write_report write_report(path: Path, result: dict[str, Any]) -> None
_cmd_build _cmd_build(args: argparse.Namespace) -> int
_build_parser _build_parser() -> argparse.ArgumentParser
main main(argv: list[str] | None = None) -> None

Source

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

#!/usr/bin/env python3
"""scripts/derive_rtm.py — derived RTM attribution over the MERGED static +
runtime call graph (reachability-attribution follow-on to the SPEC-023
runtime call-graph capture).

The traceability facts under ``.cache/trace_intel/`` (owned by
``scripts/trace_intel.py``) now carry both static edges
(``calls_resolved/3``, from AST call-site resolution) and runtime edges
(``runtime_call/2``, from the ``sys.monitoring`` capture merged in by
``scripts/build_runtime_callgraph.py`` + ``scripts/code_intel.py reindex``).
This script walks the UNION of those two edge sets to propose ``@traces``
attributions the static graph alone would miss (DI/dispatch/registry calls
routed through ``self.x()``, a factory, or a lookup table).

Method
------
1. Parse ``calls_resolved/3`` ∪ ``runtime_call/2`` from
   ``codebase_facts.pl`` into one merged adjacency ``caller -> {callees}``,
   and ``symbol/7`` (class/function/method rows only) into a
   ``qname -> module`` map.
2. Parse ``traces('Q', 'REQ')`` and ``spec_requirement('SPEC', 'REQ', ...)``
   from ``traceability_facts.pl``.
3. A spec's ANCHORS are the functions already ``@traces``-linked to one of
   its requirements. A spec's CLOSURE is everything merged-reachable
   (forward BFS over the merged adjacency) from its anchors.
4. ATTRIBUTED(spec) = untraced functions in the spec's closure that are
   reachable from EXACTLY ONE spec's closure — the discriminating filter
   that separates spec-specific code from shared infrastructure code
   incidentally reachable from many specs.
5. ALIGNED(spec) ⊆ ATTRIBUTED(spec) = the subset that ALSO lives in a
   module the spec already owns an anchor in — the high-confidence tier,
   dropping shared utilities that land in one closure by chance rather
   than by ownership.

The per-spec "reached by exactly one spec's closure" discriminator needs
every spec's closure materialized at once, which is why this lives in
Python rather than the corpus-global Prolog rules in ``traceability_db.pl``
(see that file's Layer-3 comment) — this script is the reference-numbers
producer those rules point to.

Usage::

    uv run python scripts/derive_rtm.py build

Writes ``plans/derived-rtm.json`` (deterministic, sorted — no timestamps)
and ``reports/derived-rtm.md`` (human digest), and prints a compact JSON
summary to stdout. Exits 1 with a clear stderr message if the structural
facts predate the runtime-call-graph merge (no ``runtime_call(`` fact at
all) rather than silently falling back to a static-only result.
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from collections import defaultdict, deque
from pathlib import Path
from typing import Any

# Repo root on sys.path so `scripts` (imported below) resolves regardless of
# how this file is invoked (direct `python scripts/derive_rtm.py`, `-m`, or
# as a test import) — same pattern as scripts/check_wiring.py.
_REPO_ROOT = Path(__file__).resolve().parent.parent
if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

from scripts import trace_intel as t  # noqa: E402

DEFAULT_OUT_JSON = _REPO_ROOT / "plans" / "derived-rtm.json"
DEFAULT_REPORT = _REPO_ROOT / "reports" / "derived-rtm.md"

_CALL_EDGE_RE = re.compile(r"(?:calls_resolved|runtime_call)\('([^']+)',\s*'([^']+)'")
_SYMBOL_RE = re.compile(
    r"symbol\('([^']+)',\s*'([^']+)',\s*(?:class|function|method)\b"
)
_TRACES_RE = re.compile(r"traces\('([^']+)',\s*'([^']+)'\)")
_SPEC_REQ_RE = re.compile(r"spec_requirement\('([^']+)',\s*'([^']+)'")
_SPEC_DIR_RE = re.compile(r"^(\d{3})-(.+)$")

_MISSING_RUNTIME_CALL_MSG = (
    "ERROR: no runtime_call/2 facts found in {facts} -- the structural KB "
    "predates the runtime call-graph merge, and attribution over a "
    "static-only graph would silently under-count DI/dispatch edges. Run "
    "`python scripts/code_intel.py reindex` then "
    "`python scripts/trace_intel.py extract` to regenerate the merged "
    "facts, then re-run this command."
)


# ---------------------------------------------------------------------------
# Fact parsing
# ---------------------------------------------------------------------------


def parse_call_graph(
    structural_facts_text: str,
) -> tuple[dict[str, set[str]], dict[str, str]]:
    """Parse ``calls_resolved/3`` ∪ ``runtime_call/2`` into a merged
    adjacency (``caller -> {callees}``) and ``symbol/7`` class/function/
    method rows into a ``qname -> module`` map."""
    adj: dict[str, set[str]] = defaultdict(set)
    for m in _CALL_EDGE_RE.finditer(structural_facts_text):
        adj[m.group(1)].add(m.group(2))
    module_of: dict[str, str] = {}
    for m in _SYMBOL_RE.finditer(structural_facts_text):
        module_of[m.group(1)] = m.group(2)
    return adj, module_of


def parse_traceability(
    traceability_text: str,
) -> tuple[dict[str, set[str]], dict[str, set[str]]]:
    """Parse ``traces('Q', 'REQ')`` (``qname -> {req}``) and
    ``spec_requirement('SPEC', 'REQ', ...)`` (``req -> {spec}``)."""
    traces: dict[str, set[str]] = defaultdict(set)
    for m in _TRACES_RE.finditer(traceability_text):
        traces[m.group(1)].add(m.group(2))
    req_specs: dict[str, set[str]] = defaultdict(set)
    for m in _SPEC_REQ_RE.finditer(traceability_text):
        req_specs[m.group(2)].add(m.group(1))
    return traces, req_specs


def _spec_name_map(specs_root: Path) -> dict[str, str]:
    """``specs/<id>-<name>/`` directory names -> ``{id: name}``."""
    mapping: dict[str, str] = {}
    if not specs_root.is_dir():
        return mapping
    for entry in sorted(specs_root.iterdir()):
        if not entry.is_dir():
            continue
        m = _SPEC_DIR_RE.match(entry.name)
        if m:
            mapping[m.group(1)] = m.group(2)
    return mapping


# ---------------------------------------------------------------------------
# Attribution
# ---------------------------------------------------------------------------


def compute_anchors(
    traces: dict[str, set[str]], req_specs: dict[str, set[str]]
) -> dict[str, set[str]]:
    """A spec's anchors: every ``@traces``-linked qname whose requirement
    belongs to that spec."""
    anchors: dict[str, set[str]] = defaultdict(set)
    for qname, reqs in traces.items():
        for req in reqs:
            for spec in req_specs.get(req, ()):
                anchors[spec].add(qname)
    return anchors


def _bfs(seeds: set[str], adj: dict[str, set[str]]) -> set[str]:
    """Forward BFS over the merged adjacency, seeded from ``seeds``."""
    seen: set[str] = set()
    queue: deque[str] = deque(seeds)
    while queue:
        node = queue.popleft()
        if node in seen:
            continue
        seen.add(node)
        for nxt in adj.get(node, ()):
            if nxt not in seen:
                queue.append(nxt)
    return seen


def compute_attribution(
    adj: dict[str, set[str]],
    module_of: dict[str, str],
    anchors: dict[str, set[str]],
    traced: set[str],
) -> tuple[dict[str, set[str]], dict[str, list[str]], dict[str, list[str]]]:
    """Returns ``(closure, attributed, aligned)`` per the module docstring's
    method section — ``closure`` is the full merged-reachable set per spec
    (traced and untraced), ``attributed``/``aligned`` are keyed by spec and
    hold only the untraced qnames each tier proposes."""
    closure = {spec: _bfs(anch, adj) for spec, anch in anchors.items()}
    spec_mods = {
        spec: {module_of.get(a) for a in anch} for spec, anch in anchors.items()
    }

    reach_count: dict[str, int] = defaultdict(int)
    for cl in closure.values():
        for q in cl:
            if q not in traced:
                reach_count[q] += 1

    attributed: dict[str, list[str]] = defaultdict(list)
    aligned: dict[str, list[str]] = defaultdict(list)
    for spec, cl in closure.items():
        for q in cl:
            if q not in traced and reach_count[q] == 1:
                attributed[spec].append(q)
                if module_of.get(q) in spec_mods[spec]:
                    aligned[spec].append(q)
    return closure, attributed, aligned


_METHOD_SUMMARY = (
    "Merged static (calls_resolved/3) ∪ runtime (runtime_call/2) call "
    "graph; a spec's anchors are its @traces-linked functions, its closure "
    "is forward-BFS reachability from those anchors; ATTRIBUTED = untraced "
    "closure functions reachable from exactly one spec's closure (the "
    "discriminator separating spec-specific code from shared infra); "
    "ALIGNED (high-confidence tier) = the ATTRIBUTED subset that also lives "
    "in a module the spec already anchors."
)


def build(
    out_json: Path = DEFAULT_OUT_JSON,
    out_report: Path = DEFAULT_REPORT,
    structural_facts: Path | None = None,
    traceability_facts: Path | None = None,
    specs_root: Path | None = None,
) -> dict[str, Any]:
    """Compute the attribution and write both output artifacts.

    ``structural_facts``/``traceability_facts``/``specs_root`` default to
    ``trace_intel``'s own constants; overridable so tests can point at a
    synthetic fixture without touching the real repo KB."""
    structural_path = (
        structural_facts if structural_facts is not None else t.STRUCTURAL_FACTS
    )
    traceability_path = (
        traceability_facts if traceability_facts is not None else t.TRACEABILITY_FACTS
    )
    specs_root_path = specs_root if specs_root is not None else t.SPECS_ROOT

    if structural_facts is None and traceability_facts is None:
        # Reading the real repo KB: refresh it first. Without this, build()
        # answers from whatever facts sit on disk — a specs/ edit that never
        # triggered a trace_intel query left the KB pre-edit for four ship
        # suites (2026-08-15), so the acceptance numbers reported the state
        # BEFORE the checklist restoration and the +55 surfaced mid-ship.
        t.ensure_kb(no_refresh=False)

    structural_text = structural_path.read_text(encoding="utf-8")
    if "runtime_call(" not in structural_text:
        print(
            _MISSING_RUNTIME_CALL_MSG.format(facts=structural_path),
            file=sys.stderr,
        )
        raise SystemExit(1)

    adj, module_of = parse_call_graph(structural_text)
    traceability_text = traceability_path.read_text(encoding="utf-8")
    traces, req_specs = parse_traceability(traceability_text)
    traced = set(traces)
    anchors = compute_anchors(traces, req_specs)
    closure, attributed, aligned = compute_attribution(adj, module_of, anchors, traced)

    spec_names = _spec_name_map(specs_root_path)

    per_spec: list[dict[str, Any]] = []
    for spec in closure:
        aligned_qnames = sorted(aligned.get(spec, []))
        per_spec.append(
            {
                "spec": spec,
                "spec_name": spec_names.get(spec, ""),
                "aligned_count": len(aligned_qnames),
                "closure_count": len(attributed.get(spec, [])),
                "aligned": aligned_qnames,
            }
        )
    per_spec.sort(key=lambda row: (-row["aligned_count"], row["spec"]))

    all_specs_with_reqs: set[str] = {s for specs in req_specs.values() for s in specs}
    specs_without_anchors = sorted(all_specs_with_reqs - set(closure))

    result: dict[str, Any] = {
        "method": _METHOD_SUMMARY,
        "totals": {
            "specs_with_anchors": len(closure),
            "total_attributed": sum(len(v) for v in attributed.values()),
            "total_aligned": sum(len(v) for v in aligned.values()),
        },
        "per_spec": per_spec,
        "specs_without_anchors": specs_without_anchors,
    }

    # Atomic: `open(..., "w")` truncates at OPEN and then streams the document
    # over many writes, so a concurrent reader sees a shrinking file and a
    # concurrent writer splices with it. Byte-identical output (indent=2 plus
    # the trailing newline written explicitly before).
    from orchestrata_ultimate.core.atomic_json import dump_json_atomic

    dump_json_atomic(out_json, result, indent=2, trailing_newline=True)

    write_report(out_report, result)
    return result


# ---------------------------------------------------------------------------
# reports/derived-rtm.md
# ---------------------------------------------------------------------------


def write_report(path: Path, result: dict[str, Any]) -> None:
    totals = result["totals"]
    lines = [
        "# Derived RTM — merged static+runtime attribution",
        "",
        "Generated by `scripts/derive_rtm.py build` over the merged static "
        "(`calls_resolved/3`) + runtime (`runtime_call/2`) call graph in "
        "`.cache/trace_intel/codebase_facts.pl`.",
        "",
        "## Method",
        "",
        result["method"],
        "",
        "## Corpus totals",
        "",
        f"- Specs with anchors: {totals['specs_with_anchors']}",
        f"- Total attributed (untraced, exactly-one-spec closure): "
        f"{totals['total_attributed']}",
        f"- Total aligned (high-confidence, module-matched): "
        f"{totals['total_aligned']}",
        "",
        "## Per-spec proposed @traces",
        "",
    ]
    for row in result["per_spec"]:
        title = f"### SPEC-{row['spec']}"
        if row["spec_name"]:
            title += f" ({row['spec_name']})"
        title += (
            f" — {row['aligned_count']} aligned / {row['closure_count']} attributed"
        )
        lines.append(title)
        lines.append("")
        if not row["aligned"]:
            lines.append("_No high-confidence (module-aligned) attributions._")
        else:
            for qname in row["aligned"]:
                lines.append(f"- [ ] `{qname}`")
        lines.append("")
    lines.append("## Gaps")
    lines.append("")
    if result["specs_without_anchors"]:
        lines.append(
            "Specs with requirements but zero anchors (no `@traces`-linked "
            "code to seed a closure from):"
        )
        lines.append("")
        for spec in result["specs_without_anchors"]:
            lines.append(f"- SPEC-{spec}")
    else:
        lines.append("No specs with requirements are anchor-less.")
    lines.append("")
    lines.append(
        "Abstract Protocol/interface/model code is correctly unreachable "
        "from any anchor closure and therefore excluded from attribution "
        "-- that is expected, not a gap."
    )
    lines.append("")
    from orchestrata_ultimate.core.atomic_json import dump_text_atomic

    dump_text_atomic(path, "\n".join(lines) + "\n")


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------


def _cmd_build(args: argparse.Namespace) -> int:
    out_json = Path(args.out_json)
    out_report = Path(args.out_report)
    result = build(out_json=out_json, out_report=out_report)
    summary = {
        "specs_with_anchors": result["totals"]["specs_with_anchors"],
        "total_attributed": result["totals"]["total_attributed"],
        "total_aligned": result["totals"]["total_aligned"],
        "out_json": str(out_json),
        "out_report": str(out_report),
    }
    print(json.dumps(summary, indent=2))
    return 0


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="derive_rtm",
        description=(
            "Derive an RTM attribution over the merged static+runtime call "
            "graph and propose @traces for untraced, spec-discriminated code."
        ),
    )
    sub = parser.add_subparsers(dest="command", required=True)
    b = sub.add_parser(
        "build",
        help="Compute attribution; write plans/derived-rtm.json + reports/derived-rtm.md.",
    )
    b.add_argument(
        "--out-json",
        default=str(DEFAULT_OUT_JSON),
        help=f"Derived-RTM JSON output path (default: {DEFAULT_OUT_JSON}).",
    )
    b.add_argument(
        "--out-report",
        default=str(DEFAULT_REPORT),
        help=f"Markdown digest output path (default: {DEFAULT_REPORT}).",
    )
    return parser


def main(argv: list[str] | None = None) -> None:
    parser = _build_parser()
    args = parser.parse_args(argv)
    if args.command == "build":
        sys.exit(_cmd_build(args))
    parser.print_help()
    sys.exit(1)


if __name__ == "__main__":
    main()