scripts.factory_projection

Reference › scripts › factory_projection

The command-line interface for the SPEC-033 rule-driven projection and completeness check (B5; ADR-0006 and ADR-0025). It uses the analyzer in analyzer and the completeness check in completeness; the factory narrative describes the subsystem. No module or test imports it.

Invocation: main calls the handler of the chosen subcommand. analyze projects the source with a rule base and the given source roots (_cmd_analyze). check runs the completeness check against a list of provided items (_cmd_check, with the list split by _split_provided). Both take the rule base and roots options added by _add_rulebase_and_roots.

At a glance

Source scripts/factory_projection.py
Lines of code 143
Classes 0
Functions 8
Imports (internal) orchestrata_ultimate.factory.projection.analyzer, orchestrata_ultimate.factory.projection.completeness
Imported by none
Tests that import it none

Class diagram

No classes; see Functions.

Dependency diagram

Dependency diagram scripts.factory_projection

API

Functions

Function Signature Summary
_split_provided _split_provided(raw: str) -> list[str]
_cmd_analyze _cmd_analyze(args: argparse.Namespace) -> int
_cmd_check _cmd_check(args: argparse.Namespace) -> int
_add_rulebase_and_roots _add_rulebase_and_roots(sub_parser: argparse.ArgumentParser) -> None
_add_analyze_subparser _add_analyze_subparser(sub: argparse._SubParsersAction) -> None
_add_check_subparser _add_check_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_projection.py

#!/usr/bin/env python3
"""CLI for the SPEC-033 rule-driven projection + completeness check (B5, ADR-0006/0025).

Subcommands
-----------
analyze   Derive the required-predicate set (name/arity) for a rulebase from
          one or more ``--root`` predicates; prints it as JSON alongside any
          manual ``'%! requires:'`` header cross-check mismatches. Exits
          non-zero on :class:`ProjectionUndecidable` (an ADR-0025-banned
          construct makes the call graph statically undecidable).
check     Same derivation, then asserts completeness against a
          caller-supplied ``--provided`` list (ADR-0006's pre-run meta-check);
          exits non-zero on :class:`IncompleteProjection`.

Examples
--------
    python scripts/factory_projection.py analyze \\
        src/orchestrata_ultimate/prolog/policy_layers.pl --root layered_verdict/3

    python scripts/factory_projection.py check \\
        src/orchestrata_ultimate/prolog/policy_layers.pl --root layered_verdict/3 \\
        --provided "constitutional:requires_human_consent/1,resources:resource_access_allowed/4"
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

# Ensure repo root is on path when run directly (mirrors factory_detectors.py):
# `orchestrata_ultimate.*` resolves via the editable install, but the
# analyzer's own lazy `from scripts.prolog_meta_lint import ...` (see its
# module docstring) needs the REPO ROOT itself importable as a namespace
# package too.
_REPO_ROOT = Path(__file__).resolve().parent.parent
if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

from orchestrata_ultimate.factory.projection.analyzer import (  # noqa: E402
    PredId,
    ProjectionUndecidable,
    required_predicates,
    verify_declaration,
)
from orchestrata_ultimate.factory.projection.completeness import (  # noqa: E402
    IncompleteProjection,
    assert_projection_complete,
    projection_manifest,
)


def _split_provided(raw: str) -> list[str]:
    return [spec.strip() for spec in raw.split(",") if spec.strip()]


def _cmd_analyze(args: argparse.Namespace) -> int:
    path = Path(args.rulebase)
    try:
        required = required_predicates(path, args.root)
    except ProjectionUndecidable as exc:
        print(f"ProjectionUndecidable: {exc}", file=sys.stderr)
        return 1
    payload = {
        "rulebase": str(path),
        "roots": args.root,
        **projection_manifest(required),
        "declaration_mismatches": verify_declaration(path),
    }
    print(json.dumps(payload, indent=2))
    return 0


def _cmd_check(args: argparse.Namespace) -> int:
    path = Path(args.rulebase)
    try:
        required = required_predicates(path, args.root)
    except ProjectionUndecidable as exc:
        print(f"ProjectionUndecidable: {exc}", file=sys.stderr)
        return 1
    provided = [PredId.parse(spec) for spec in _split_provided(args.provided)]
    try:
        assert_projection_complete(required, provided)
    except IncompleteProjection as exc:
        print(f"IncompleteProjection: {exc}", file=sys.stderr)
        return 1
    print("projection complete")
    print(json.dumps(projection_manifest(required), indent=2))
    return 0


def _add_rulebase_and_roots(sub_parser: argparse.ArgumentParser) -> None:
    sub_parser.add_argument("rulebase", help="Path to the .pl rulebase file")
    sub_parser.add_argument(
        "--root",
        action="append",
        required=True,
        help="Root predicate 'name/arity' (repeatable)",
    )


def _add_analyze_subparser(sub: argparse._SubParsersAction) -> None:
    analyze_p = sub.add_parser(
        "analyze", help="Derive + print the required-predicate set as JSON"
    )
    _add_rulebase_and_roots(analyze_p)
    analyze_p.set_defaults(func=_cmd_analyze)


def _add_check_subparser(sub: argparse._SubParsersAction) -> None:
    check_p = sub.add_parser(
        "check", help="Derive, then assert completeness against --provided"
    )
    _add_rulebase_and_roots(check_p)
    check_p.add_argument(
        "--provided",
        default="",
        help="Comma-separated 'name/arity' predicates the caller is about "
        "to project (e.g. 'p/2,q/3')",
    )
    check_p.set_defaults(func=_cmd_check)


def _build_parser() -> argparse.ArgumentParser:
    ap = argparse.ArgumentParser(
        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
    )
    sub = ap.add_subparsers(dest="command", required=True)
    _add_analyze_subparser(sub)
    _add_check_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())