orchestrata_ultimate.factory.amendment.classify

Reference › orchestrata_ultimate › factory › amendment › classify

classify is the entry point of the amendment-direction classifier (SPEC-033 row B16, ADR-0009, ADR-0020, ADR-0021). classify_amendment takes the old and new text of a Prolog rulebase and returns a DirectionVerdict from models: TIGHTENING, LOOSENING or UNDETERMINED. It is fail-closed in one direction only. It first runs the syntactic classifier from syntactic; a verdict that is not TIGHTENING is final (_non_tightening_syntactic_verdict) and no later evidence rescues it. A syntactic TIGHTENING is provisional and is downgraded to UNDETERMINED by either of two refutation-only checks: ledger_screen / _ledger_downgrade (a newly accepted known case, using fingerprints from the assumptions ledger) or _coupling_downgrade, which uses the import edges and boundary flag from coupling (ADR-0021). Neither check can promote a verdict to TIGHTENING. The scripts.factory_amend CLI imports it, and tests/factory/test_amendment_classify.py tests it.

At a glance

Source src/orchestrata_ultimate/factory/amendment/classify.py
Lines of code 153
Classes 0
Functions 5
Imports (internal) orchestrata_ultimate.factory.amendment.coupling, orchestrata_ultimate.factory.amendment.models, orchestrata_ultimate.factory.amendment.syntactic, orchestrata_ultimate.factory.assumptions.models
Imported by scripts.factory_amend
Tests that import it tests/factory/test_amendment_classify.py

Class diagram

No classes; see Functions.

Dependency diagram

Dependency diagram orchestrata_ultimate.factory.amendment.classify

API

Functions

Function Signature Summary
ledger_screen ledger_screen(old_text: str, new_text: str, accepted_fingerprints: list[str]) -> Direction | None ADR-0020 step 2: the ledger differential, as a SCREEN only.
_non_tightening_syntactic_verdict _non_tightening_syntactic_verdict(syntactic_direction: Direction, changes: list[ClauseChange]) -> DirectionVerdict | None Step 1's early exit: a non-TIGHTENING syntactic verdict is final -- None means "still provisionally tightening, keep going".
_ledger_downgrade _ledger_downgrade(old_text: str, new_text: str, accepted_fingerprints: list[str] | None, changes: list) -> DirectionVerdict | None Step 2: None means the screen did not refute (keep going).
_coupling_downgrade _coupling_downgrade(old_edges: set[tuple[str, str]] | None, new_edges: set[tuple[str, str]] | None, boundary_changed: bool, changes: list) -> DirectionVerdict | None ADR-0021: None means the coupling signal is compatible (keep going).
classify_amendment classify_amendment(old_text: str, new_text: str, *, old_edges: set[tuple[str, str]] | None = None, new_edges: set[tuple[str, str]] | None = None, boundary_changed: bool = False, accepted_fingerprints: list[str] | None = None) -> DirectionVerdict Classify one rulebase amendment, fail-closed (ADR-0020 step 3).

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/factory/amendment/classify.py

"""The ADR-0020 fail-closed cascade: syntactic first, else the human.

1. Run the syntactic classifier (:mod:`~...syntactic`). Anything not
   provably TIGHTENING is already UNDETERMINED/LOOSENING -- done, no further
   evidence changes that (a non-tightening verdict is never "rescued" back
   to tightening by a ledger or coupling signal; ADR-0020 is fail-closed in
   one direction only).
2. A syntactic TIGHTENING verdict is provisional: it is DOWNGRADED to
   UNDETERMINED if either the ledger screen refutes it (a newly-accepted
   known case) or the coupling signal contradicts it (a boundary change or a
   mixed closure delta). Neither the ledger screen nor coupling can ever
   PROMOTE a verdict to TIGHTENING -- both are refutation-only checks
   (ADR-0020 step 2's explicit contract), so their absence of evidence
   (``None`` / a compatible :class:`~...models.Direction.TIGHTENING` signal)
   simply leaves the syntactic verdict standing.
"""

from __future__ import annotations

from orchestrata_ultimate.factory.amendment.coupling import coupling_verdict
from orchestrata_ultimate.factory.amendment.models import (
    ClauseChange,
    Direction,
    DirectionVerdict,
)
from orchestrata_ultimate.factory.amendment.syntactic import (
    classify_syntactic,
    diff_clauses,
)
from orchestrata_ultimate.factory.assumptions.models import fingerprint


def ledger_screen(
    old_text: str, new_text: str, accepted_fingerprints: list[str]
) -> Direction | None:
    """ADR-0020 step 2: the ledger differential, as a SCREEN only.

    Operates over an explicitly PASSED list of prior-accepted case
    fingerprints -- never a live query; a live governance-fact ledger is a
    future wiring (the caller injects the accepted set here). The
    fingerprint identity reuses
    :func:`orchestrata_ultimate.factory.assumptions.models.fingerprint`
    (normalize + sha256), applied to the WHOLE amended (``new_text``) rulebase
    text -- the granularity this module currently supports; a finer-grained,
    per-clause or per-fact fingerprint is future work, not attempted here.

    Returns :attr:`~...models.Direction.LOOSENING` when ``new_text``'s
    fingerprint is among ``accepted_fingerprints`` (this exact amendment
    matches a case the ledger already knows was newly accepted -- refuting
    any syntactic tightening claim). Returns ``None`` otherwise -- the screen
    has no evidence either way; it NEVER returns TIGHTENING, because an
    empirical subset test over sampled history can refute, never establish,
    a direction (ADR-0020's core distinction).
    """

    new_fingerprint = fingerprint(new_text)
    if new_fingerprint in accepted_fingerprints:
        return Direction.LOOSENING
    return None


def _non_tightening_syntactic_verdict(
    syntactic_direction: Direction, changes: list[ClauseChange]
) -> DirectionVerdict | None:
    """Step 1's early exit: a non-TIGHTENING syntactic verdict is final --
    ``None`` means "still provisionally tightening, keep going"."""
    if syntactic_direction is Direction.TIGHTENING:
        return None
    reason = (
        f"syntactic classifier found {syntactic_direction.value} "
        "(not provably tightening -- ADR-0020 step 1/3)"
    )
    return DirectionVerdict.build(syntactic_direction, reason, changes)


def _ledger_downgrade(
    old_text: str,
    new_text: str,
    accepted_fingerprints: list[str] | None,
    changes: list,
) -> DirectionVerdict | None:
    """Step 2: ``None`` means the screen did not refute (keep going)."""
    screen = ledger_screen(old_text, new_text, accepted_fingerprints or [])
    if screen is not Direction.LOOSENING:
        return None
    reason = (
        "syntactic classifier found tightening, but the ledger screen "
        "refutes it: this amendment newly accepts a known-accepted case "
        "(ADR-0020 step 2/3) -- downgraded to undetermined"
    )
    return DirectionVerdict.build(Direction.UNDETERMINED, reason, changes)


def _coupling_downgrade(
    old_edges: set[tuple[str, str]] | None,
    new_edges: set[tuple[str, str]] | None,
    boundary_changed: bool,
    changes: list,
) -> DirectionVerdict | None:
    """ADR-0021: ``None`` means the coupling signal is compatible (keep going)."""
    coupling_direction = coupling_verdict(
        old_edges or set(), new_edges or set(), boundary_changed
    )
    if coupling_direction is Direction.TIGHTENING:
        return None
    reason = (
        "syntactic classifier found tightening, but the coupling signal "
        f"is {coupling_direction.value} (ADR-0021) -- downgraded to "
        "undetermined"
    )
    return DirectionVerdict.build(Direction.UNDETERMINED, reason, changes)


def classify_amendment(
    old_text: str,
    new_text: str,
    *,
    old_edges: set[tuple[str, str]] | None = None,
    new_edges: set[tuple[str, str]] | None = None,
    boundary_changed: bool = False,
    accepted_fingerprints: list[str] | None = None,
) -> DirectionVerdict:
    """Classify one rulebase amendment, fail-closed (ADR-0020 step 3).

    ``old_edges``/``new_edges`` are cross-component import edges (see
    :mod:`~...coupling`); when omitted they default to empty sets, which
    (combined with ``boundary_changed=False``) is always compatible with
    tightening -- "no coupling signal available" is never treated as
    contradicting evidence. ``accepted_fingerprints`` is the explicitly
    passed ledger-screen input (see :func:`ledger_screen`); omitted means
    "no ledger evidence available" (the screen degrades to ``None``, never
    to a refutation).
    """
    changes = diff_clauses(old_text, new_text)
    syntactic_direction = classify_syntactic(changes)

    verdict = _non_tightening_syntactic_verdict(syntactic_direction, changes)
    if verdict is not None:
        return verdict

    verdict = _ledger_downgrade(old_text, new_text, accepted_fingerprints, changes)
    if verdict is not None:
        return verdict

    verdict = _coupling_downgrade(old_edges, new_edges, boundary_changed, changes)
    if verdict is not None:
        return verdict

    reason = (
        "every clause change is provably tightening (clause removed or "
        "conjunct added) and no ledger/coupling signal contradicts it"
    )
    return DirectionVerdict.build(Direction.TIGHTENING, reason, changes)