diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e76cc11..2f13eeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,29 +1,35 @@ name: CI - on: push: branches: [main] pull_request: branches: [main] - jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.10", "3.11", "3.12"] - steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies run: pip install pytest - + - name: Drift alarm - no StopMachine class in primitives + run: | + if grep -rn "class StopMachine" primitives/; then + echo "DRIFT DETECTED: StopMachine class found in primitives/" + exit 1 + fi + - name: Drift alarm - no Gate implementation in primitives + run: | + if grep -rn "class.*Gate" primitives/authority-gate-v0/ primitives/stop-machine-v0/; then + echo "DRIFT DETECTED: Gate class found in v0 folders" + exit 1 + fi - name: Run primitive tests run: python -m pytest primitives -v - name: Run root tests diff --git a/examples/demo_authority_gate.py b/examples/demo_authority_gate.py deleted file mode 100644 index f8b221f..0000000 --- a/examples/demo_authority_gate.py +++ /dev/null @@ -1,17 +0,0 @@ -# examples/demo_authority_gate.py - -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "primitives" / "authority-gate-v0")) - -from gate import Authority, AuthorityGate # noqa: E402 - -g = AuthorityGate(required=Authority.OWNER_CONFIRMED) -print("required:", g.required.name) -for a in [Authority.NONE, Authority.USER_CONFIRMED, Authority.OWNER_CONFIRMED]: - try: - print(a.name, "->", g.call(lambda: "OK", authority=a)) - except PermissionError as e: - print(a.name, "-> DENY", str(e)) -print("history:", g.history) diff --git a/examples/demo_stop_machine.py b/examples/demo_stop_machine.py deleted file mode 100644 index c3a5690..0000000 --- a/examples/demo_stop_machine.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Demo: shows the full StopMachine lifecycle in ~15 lines.""" -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "primitives" / "stop-machine-v0")) - -from stop_machine import Event, StopMachine # noqa: E402 - -m = StopMachine() -print(m) # StopMachine(state=GREEN) - -m.send(Event.TICK) -print(m) # StopMachine(state=GREEN) - -m.send(Event.WARN) -print(m) # StopMachine(state=AMBER) - -m.send(Event.STOP) -print(m) # StopMachine(state=RED) - -m.send(Event.RESET) # attempt escape -print(m) # StopMachine(state=RED) <- absorbed - -print(f"\nTerminal: {m.is_terminal()}") -print(f"History: {m.history}") diff --git a/primitives/authority-gate-v0/README.md b/primitives/authority-gate-v0/README.md index 9a15192..2aeaa4f 100644 --- a/primitives/authority-gate-v0/README.md +++ b/primitives/authority-gate-v0/README.md @@ -1,37 +1,8 @@ -# AuthorityGate +NON-CANONICAL LEGACY (V0). Canonical: [constraint-workshop](https://github.com/LalaSkye/constraint-workshop) @ `70ed2c9`. -A tiny, deterministic wrapper that makes **execution require explicit authority**. +This folder contains a **non-functional stub** that raises `RuntimeError` on import. +The canonical `AuthorityGate` implementation lives in +[constraint-workshop/authority_gate.py](https://github.com/LalaSkye/constraint-workshop/blob/main/authority_gate.py). -## Invariants (all tested) - -| Invariant | Meaning | Tested | -|---|---|:--:| -| Determinism | Same inputs => same allow/deny + same history | Yes | -| Monotonicity | Higher authority never loses permissions | Yes | -| Auditability | Every call records {required, provided, allowed} | Yes | - -## Authority levels - -Ordered (weak to strong): - -- `NONE` -- `USER_CONFIRMED` -- `OWNER_CONFIRMED` -- `ADMIN_APPROVED` - -## Why this matters - -Most "governance" documents talk about approval, but runtime systems still execute on vibes. -This primitive forces the missing mechanical step: **no explicit authority, no execution**. - -## Quickstart - -```bash -python -m pytest primitives/authority-gate -v -``` - -## Scope - -- No policy engine. -- No orchestration logic. -- No opinions. +Do not modify this folder. Update the canonical source instead. +See [CANONICAL.md](../../CANONICAL.md) for details. diff --git a/primitives/authority-gate-v0/gate.py b/primitives/authority-gate-v0/gate.py index cc76ad8..c5ac7e5 100644 --- a/primitives/authority-gate-v0/gate.py +++ b/primitives/authority-gate-v0/gate.py @@ -1,47 +1,11 @@ -"""AuthorityGate -- deterministic authority wrapper. +"""NON-CANONICAL STUB — do not use. -Execution requires explicit authority. No implicit permissions. -Authority levels are ordered: NONE < USER_CONFIRMED < OWNER_CONFIRMED < ADMIN_APPROVED. +Canonical source: https://github.com/LalaSkye/constraint-workshop +Canonical file: authority_gate.py +Pinned commit: 70ed2c9 """ -from __future__ import annotations - -from dataclasses import dataclass, field -from enum import IntEnum -from typing import Any, Callable, List - - -class Authority(IntEnum): - NONE = 0 - USER_CONFIRMED = 1 - OWNER_CONFIRMED = 2 - ADMIN_APPROVED = 3 - - -@dataclass(frozen=True) -class Decision: - required: Authority - provided: Authority - allowed: bool - - -@dataclass -class AuthorityGate: - """Deterministic authority gate. No implicit permissions.""" - - required: Authority = Authority.USER_CONFIRMED - _history: List[Decision] = field(default_factory=list) - - def call(self, fn: Callable[..., Any], *args: Any, authority: Authority, **kwargs: Any) -> Any: - """Execute *fn* only if authority >= required. Pure comparison.""" - allowed = authority >= self.required - self._history.append(Decision(self.required, authority, allowed)) - if not allowed: - raise PermissionError(f"authority {authority.name} < required {self.required.name}") - return fn(*args, **kwargs) - - @property - def history(self) -> List[Decision]: - return list(self._history) - - def is_satisfied(self, authority: Authority) -> bool: - return authority >= self.required +raise RuntimeError( + "This is a non-canonical legacy stub (v0). " + "The canonical AuthorityGate lives in constraint-workshop @ commit 70ed2c9. " + "See: https://github.com/LalaSkye/constraint-workshop/blob/main/authority_gate.py" +) diff --git a/primitives/authority-gate-v0/test_gate.py b/primitives/authority-gate-v0/test_gate.py index 31bcc86..ea95db8 100644 --- a/primitives/authority-gate-v0/test_gate.py +++ b/primitives/authority-gate-v0/test_gate.py @@ -1,51 +1,14 @@ -# primitives/authority-gate/test_gate.py - +"""Stub test: confirms the v0 authority_gate raises RuntimeError on import.""" import pytest -from gate import Authority, AuthorityGate - -ALL = list(Authority) - - -def add(a, b): - return a + b - - -def test_determinism_replay_history_and_result(): - g1 = AuthorityGate(required=Authority.OWNER_CONFIRMED) - g2 = AuthorityGate(required=Authority.OWNER_CONFIRMED) - seq = [Authority.NONE, Authority.USER_CONFIRMED, Authority.OWNER_CONFIRMED, Authority.ADMIN_APPROVED] - out1, out2 = [], [] - for a in seq: - try: - out1.append(g1.call(add, 1, 2, authority=a)) - except PermissionError: - out1.append("DENY") - try: - out2.append(g2.call(add, 1, 2, authority=a)) - except PermissionError: - out2.append("DENY") - assert out1 == out2 - assert g1.history == g2.history - - -@pytest.mark.parametrize("required", ALL) -@pytest.mark.parametrize("provided", ALL) -def test_monotonicity_authority_required_is_threshold(required, provided): - g = AuthorityGate(required=required) - ok = provided >= required - assert g.is_satisfied(provided) is ok - if ok: - assert g.call(add, 2, 3, authority=provided) == 5 - else: - with pytest.raises(PermissionError): - g.call(add, 2, 3, authority=provided) - - -def test_history_records_decisions_in_order(): - g = AuthorityGate(required=Authority.USER_CONFIRMED) - with pytest.raises(PermissionError): - g.call(add, 1, 1, authority=Authority.NONE) - assert g.call(add, 1, 1, authority=Authority.USER_CONFIRMED) == 2 - assert len(g.history) == 2 - assert g.history[0].allowed is False - assert g.history[1].allowed is True +import importlib.util +import sys +from pathlib import Path + + +def test_import_authority_gate_v0_raises(): + """Importing the v0 stub must raise RuntimeError.""" + stub = Path(__file__).resolve().parent / "gate.py" + spec = importlib.util.spec_from_file_location("gate_v0_stub", stub) + mod = importlib.util.module_from_spec(spec) + with pytest.raises(RuntimeError, match="non-canonical legacy stub"): + spec.loader.exec_module(mod) diff --git a/primitives/stop-machine-v0/README.md b/primitives/stop-machine-v0/README.md index 8f174e9..8f1763d 100644 --- a/primitives/stop-machine-v0/README.md +++ b/primitives/stop-machine-v0/README.md @@ -1,60 +1,8 @@ -# StopMachine +NON-CANONICAL LEGACY (V0). Canonical: [constraint-workshop](https://github.com/LalaSkye/constraint-workshop) @ `3780882`. -Deterministic finite-state stop controller. +This folder contains a **non-functional stub** that raises `RuntimeError` on import. +The canonical `StopMachine` implementation lives in +[constraint-workshop/stop_machine.py](https://github.com/LalaSkye/constraint-workshop/blob/main/stop_machine.py). -``` -Inputs (events) ──▶ [ StopMachine ] ──▶ Output state - - GREEN - │ - ▼ - AMBER - │ - ▼ - RED (terminal, absorbing) -``` - -- **RED is terminal** (cannot be bypassed) -- **Transition table is explicit** (no hidden behaviour) -- **Determinism is tested** (replay stable) - -## Invariants (all tested) - -| Invariant | Meaning | Tested | -|---|---|:--:| -| Determinism | Same state + same event => same next state; replay is stable | ✅ | -| Absorption | RED is terminal: (RED, *) -> RED | ✅ | -| Completeness | Every (State, Event) pair exists in the table | ✅ | -| Monotonicity | WARN and STOP never decrease severity (GREEN < AMBER < RED) | ✅ | - -## Full transition table - -| Current | TICK | WARN | STOP | RESET | -|---|---|---|---|---| -| GREEN | GREEN | AMBER | RED | GREEN | -| AMBER | AMBER | AMBER | RED | GREEN | -| RED | RED | RED | RED | RED | - -**The table is the implementation. There is no branching logic.** - -## Why this matters - -In real systems, "optimisation" often means adding behaviour without tightening failure modes. This primitive does the opposite: it makes **stop-rights** explicit, deterministic, and testable. - -- You can replay decisions and get identical results. -- You can prove terminal behaviour (absorption) rather than hoping it holds. -- You can inspect the entire behavioural surface area in one table. - -## Quickstart - -```bash -cd primitives/stop-machine -pip install pytest -pytest test_stop_machine.py -v -``` - -## Scope - -- No orchestration logic. -- No selection logic. -- No opinions. +Do not modify this folder. Update the canonical source instead. +See [CANONICAL.md](../../CANONICAL.md) for details. diff --git a/primitives/stop-machine-v0/stop_machine.py b/primitives/stop-machine-v0/stop_machine.py index 9737b8d..8678dc2 100644 --- a/primitives/stop-machine-v0/stop_machine.py +++ b/primitives/stop-machine-v0/stop_machine.py @@ -1,80 +1,11 @@ -"""StopMachine -- deterministic finite-state stop controller. +"""NON-CANONICAL STUB — do not use. -States: GREEN -> AMBER -> RED -RED is terminal and absorbing: once entered, no event can leave it. -The transition table is the single source of truth. -There is no implicit behaviour. +Canonical source: https://github.com/LalaSkye/constraint-workshop +Canonical file: stop_machine.py +Pinned commit: 3780882 """ -from __future__ import annotations - -from dataclasses import dataclass, field -from enum import Enum -from typing import Dict, List, Tuple - - -class State(Enum): - GREEN = "GREEN" - AMBER = "AMBER" - RED = "RED" - - -class Event(Enum): - TICK = "TICK" - WARN = "WARN" - STOP = "STOP" - RESET = "RESET" - - -# -- Transition table -------------------------------------------------------- -# Key: (current_state, event) -# Value: next_state -# Every (State, Event) pair is listed. Nothing is implicit. - -TRANSITIONS: Dict[Tuple[State, Event], State] = { - # GREEN - (State.GREEN, Event.TICK): State.GREEN, - (State.GREEN, Event.WARN): State.AMBER, - (State.GREEN, Event.STOP): State.RED, - (State.GREEN, Event.RESET): State.GREEN, - # AMBER - (State.AMBER, Event.TICK): State.AMBER, - (State.AMBER, Event.WARN): State.AMBER, - (State.AMBER, Event.STOP): State.RED, - (State.AMBER, Event.RESET): State.GREEN, - # RED (absorbing) - (State.RED, Event.TICK): State.RED, - (State.RED, Event.WARN): State.RED, - (State.RED, Event.STOP): State.RED, - (State.RED, Event.RESET): State.RED, -} - - -@dataclass -class StopMachine: - """Finite-state stop controller. Deterministic. No side-effects.""" - - _state: State = State.GREEN - _history: List[Tuple[State, Event, State]] = field(default_factory=list) - - def send(self, event: Event) -> State: - """Apply *event*, return the new state. Pure lookup -- no branching.""" - prev = self._state - nxt = TRANSITIONS[(prev, event)] - self._state = nxt - self._history.append((prev, event, nxt)) - return nxt - - @property - def state(self) -> State: - return self._state - - @property - def history(self) -> List[Tuple[State, Event, State]]: - """Immutable copy of transition log.""" - return list(self._history) - - def is_terminal(self) -> bool: - return self._state is State.RED - - def __repr__(self) -> str: - return f"StopMachine(state={self._state.value})" +raise RuntimeError( + "This is a non-canonical legacy stub (v0). " + "The canonical StopMachine lives in constraint-workshop @ commit 3780882. " + "See: https://github.com/LalaSkye/constraint-workshop/blob/main/stop_machine.py" +) diff --git a/primitives/stop-machine-v0/test_stop_machine.py b/primitives/stop-machine-v0/test_stop_machine.py index 1655f5d..a717f26 100644 --- a/primitives/stop-machine-v0/test_stop_machine.py +++ b/primitives/stop-machine-v0/test_stop_machine.py @@ -1,91 +1,13 @@ -"""Property tests for StopMachine. - -Three guarantees, tested exhaustively over the finite domain: - 1. Determinism -- same (state, event) always yields same next_state. - 2. Absorption -- RED is terminal; no event can leave it. - 3. Completeness -- every (state, event) pair has an entry. -""" +"""Stub test: confirms the v0 stop_machine raises RuntimeError on import.""" import pytest +import importlib.util +from pathlib import Path -from stop_machine import Event, State, StopMachine, TRANSITIONS - -ALL_STATES = list(State) -ALL_EVENTS = list(Event) -ALLOWED = set(ALL_STATES) -SEVERITY = {State.GREEN: 0, State.AMBER: 1, State.RED: 2} - - -# -- Determinism ------------------------------------------------------------- - -def test_determinism_replay_identical_history(): - """Same input sequence -> same output sequence. Always.""" - seq = [Event.TICK, Event.WARN, Event.TICK, Event.STOP, Event.RESET, Event.TICK] - m1 = StopMachine() - m2 = StopMachine() - for ev in seq: - m1.send(ev) - m2.send(ev) - assert m1.state == m2.state - assert m1.history == m2.history - - -@pytest.mark.parametrize("state", ALL_STATES) -@pytest.mark.parametrize("event", ALL_EVENTS) -def test_determinism_table_lookup_same_value_twice(state, event): - """The table returns the same value on every read.""" - a = TRANSITIONS[(state, event)] - b = TRANSITIONS[(state, event)] - assert a is b - - -# -- Absorption (RED is terminal) -------------------------------------------- - -@pytest.mark.parametrize("event", ALL_EVENTS) -def test_absorption_red_to_red_for_each_event(event): - assert TRANSITIONS[(State.RED, event)] is State.RED - - -def test_absorption_enter_red_then_fire_every_event_stays_red(): - sm = StopMachine() - sm.send(Event.STOP) - assert sm.is_terminal() - for event in ALL_EVENTS: - sm.send(event) - assert sm.state is State.RED - assert sm.is_terminal() - - -# -- Completeness ------------------------------------------------------------ - -def test_completeness_every_state_event_pair_exists(): - for s in ALL_STATES: - for e in ALL_EVENTS: - assert (s, e) in TRANSITIONS - - -# -- Exhaustive transition closure ------------------------------------------- - -def test_exhaustive_transition_closure(): - """ - For every (state, event) pair: - - next_state is in {GREEN, AMBER, RED} - - RED is absorbing - - determinism holds - """ - for s in ALL_STATES: - for e in ALL_EVENTS: - a = TRANSITIONS[(s, e)] - b = TRANSITIONS[(s, e)] - assert a in ALLOWED, f"({s}, {e}) -> {a} not in allowed set" - assert a is b # determinism - if s is State.RED: - assert a is State.RED, f"RED escaped via {e} -> {a}" - - -# -- Monotonicity ------------------------------------------------------------ -@pytest.mark.parametrize("state", ALL_STATES) -@pytest.mark.parametrize("event", [Event.WARN, Event.STOP]) -def test_monotonicity_warn_and_stop_never_decrease_severity(state, event): - nxt = TRANSITIONS[(state, event)] - assert SEVERITY[nxt] >= SEVERITY[state] +def test_import_stop_machine_v0_raises(): + """Importing the v0 stub must raise RuntimeError.""" + stub = Path(__file__).resolve().parent / "stop_machine.py" + spec = importlib.util.spec_from_file_location("stop_machine_v0_stub", stub) + mod = importlib.util.module_from_spec(spec) + with pytest.raises(RuntimeError, match="non-canonical legacy stub"): + spec.loader.exec_module(mod)