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