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
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())