scripts.factory_amend

Reference › scripts › factory_amend

The command-line interface for the SPEC-033 amendment-direction differential classifier (B16). It uses the classifier in classify and the coupling analysis in coupling; the factory narrative describes the wider subsystem. No module or test imports it.

Invocation: main parses the arguments and dispatches to the handler of the chosen subcommand. The only subcommand is classify (_cmd_classify, with its parser in _add_classify_subparser). _print_report prints the resulting DirectionVerdict together with a summary of the coupling.

At a glance

Source scripts/factory_amend.py
Lines of code 125
Classes 0
Functions 5
Imports (internal) orchestrata_ultimate.factory.amendment.classify, orchestrata_ultimate.factory.amendment.coupling, orchestrata_ultimate.factory.amendment.models
Imported by none
Tests that import it none

Class diagram

No classes; see Functions.

Dependency diagram

Dependency diagram scripts.factory_amend

API

Functions

Function Signature Summary
_print_report _print_report(verdict: DirectionVerdict, coupling_summary: str) -> None
_cmd_classify _cmd_classify(args: argparse.Namespace) -> int
_add_classify_subparser _add_classify_subparser(sub: argparse._SubParsersAction) -> None
_build_parser _build_parser() -> argparse.ArgumentParser
main main() -> int Parse argv and dispatch to the selected subcommand's handler.

Source

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

#!/usr/bin/env python3
"""CLI for the SPEC-033 amendment-direction differential classifier (B16,
ADR-0009/0020/0021).

    python scripts/factory_amend.py classify --old OLD.pl --new NEW.pl
    python scripts/factory_amend.py classify --old OLD.pl --new NEW.pl \\
        --changed-file src/orchestrata_ultimate/gateway/handlers/auth.py \\
        --repo-root .

Exit code is the routing signal itself: 0 for TIGHTENING (quorum suffices,
ADR-0009), 2 for UNDETERMINED or LOOSENING (both route to the human -- kept
as one distinct non-zero code rather than two, since the CLI's only decision
point downstream is "does this need the human", not which of the two it is;
the printed report always states which).

``--changed-file`` gives ONLY a single-snapshot, informational coupling
note (cross-component import edges the changed files touch, from the
CURRENT tree via :func:`~...coupling.import_edges`) -- it is NOT a before/after
diff (that needs an old-tree snapshot this CLI has no flag for) and it never
detects a component-boundary merge/split/move on its own; both edge sets
passed to the classifier are the same snapshot, so the coupling signal can
only ever come out "unchanged" (compatible with tightening), never
"conflicting". Boundary-change detection
(:func:`~orchestrata_ultimate.factory.amendment.coupling.boundary_change`) is
exposed by the library for callers with two component sets, but this CLI
has no flag pair to supply them.
"""

from __future__ import annotations

import argparse
import sys
from pathlib import Path
from typing import TYPE_CHECKING

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

if TYPE_CHECKING:
    from orchestrata_ultimate.factory.amendment.models import DirectionVerdict


def _print_report(verdict: DirectionVerdict, coupling_summary: str) -> None:
    print(f"direction: {verdict.direction.value}")
    print(f"reason: {verdict.reason}")
    print(f"requires_human: {verdict.requires_human}")
    if not verdict.changes:
        print("changes: none")
    else:
        print(f"changes ({len(verdict.changes)}):")
        for change in verdict.changes:
            print(f"  [{change.kind.value}] {change.predicate}: {change.detail}")
    print(f"coupling: {coupling_summary}")


def _cmd_classify(args: argparse.Namespace) -> int:
    from orchestrata_ultimate.factory.amendment.classify import classify_amendment
    from orchestrata_ultimate.factory.amendment.coupling import (
        coupling_note,
        import_edges,
    )
    from orchestrata_ultimate.factory.amendment.models import Direction

    old_text = Path(args.old).read_text(encoding="utf-8")
    new_text = Path(args.new).read_text(encoding="utf-8")

    edges: set[tuple[str, str]] = set()
    coupling_summary = "no --changed-file given -- no coupling signal"
    if args.changed_file:
        repo_root = Path(args.repo_root) if args.repo_root else _REPO_ROOT
        edges = import_edges(repo_root, args.changed_file)
        coupling_summary = (
            "single-snapshot only (no before/after tree diff): "
            + coupling_note(edges, edges, False)
        )

    verdict = classify_amendment(
        old_text,
        new_text,
        old_edges=edges,
        new_edges=edges,
        boundary_changed=False,
    )
    _print_report(verdict, coupling_summary)
    return 0 if verdict.direction is Direction.TIGHTENING else 2


def _add_classify_subparser(sub: argparse._SubParsersAction) -> None:
    classify_p = sub.add_parser(
        "classify",
        help="Classify an amendment's direction (tighten/loosen/undetermined)",
    )
    classify_p.add_argument("--old", required=True, help="Path to the OLD .pl text")
    classify_p.add_argument("--new", required=True, help="Path to the NEW .pl text")
    classify_p.add_argument(
        "--changed-file",
        action="append",
        default=[],
        help="A changed .py file (repeatable) -- informational coupling note only",
    )
    classify_p.add_argument(
        "--repo-root", default="", help="Repo root for --changed-file resolution"
    )
    classify_p.set_defaults(func=_cmd_classify)


def _build_parser() -> argparse.ArgumentParser:
    ap = argparse.ArgumentParser(
        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
    )
    sub = ap.add_subparsers(dest="command", required=True)
    _add_classify_subparser(sub)
    return ap


def main() -> int:
    """Parse argv and dispatch to the selected subcommand's handler."""
    args = _build_parser().parse_args()
    result: int = args.func(args)
    return result


if __name__ == "__main__":
    sys.exit(main())