Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions executive/watchdog.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,27 @@ def run_checks() -> tuple[bool, list[str]]:

for a in actions:
rid = a.get("ruling_id", "")
if rid and rid not in approved and "override" not in a.get("action_type", "").lower():
# Action references ruling not in judicial log
proof = a.get("override_proof")
action_type = a.get("action_type", "").lower()
proof = a.get("override_proof")

if "override" in action_type:
if not proof:
alerts.append(f"Override action without proof: ruling_id={rid}")
continue
try:
from executive.proof import OverrideProof

reason = a.get("description", "")
if OverrideProof.verify(proof, rid, reason) is None:
alerts.append(f"Invalid override proof: ruling_id={rid}")
except FileNotFoundError:
alerts.append(
f"Action without judicial approval and no override proof: ruling_id={rid}"
f"Cannot verify override proof (secret missing): ruling_id={rid}"
)
elif rid and rid not in approved:
alerts.append(
f"Action without judicial approval and no override proof: ruling_id={rid}"
)

# (c) Override frequency
overrides = [a for a in actions if a.get("override_proof")]
Expand Down
76 changes: 61 additions & 15 deletions litigation/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
from datetime import datetime
from pathlib import Path

try:
import fcntl
except ImportError: # Windows — no cross-process locking
fcntl = None # type: ignore[assignment,misc]

# Add project root for imports
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT))
Expand Down Expand Up @@ -62,30 +67,71 @@ def slugify(text: str) -> str:
REGISTRY_PATH = REPO_ROOT / "courtroom" / "case-registry.yaml"


def _lock_registry_file(file_obj) -> None:
if fcntl is not None:
fcntl.flock(file_obj.fileno(), fcntl.LOCK_EX)


def _unlock_registry_file(file_obj) -> None:
if fcntl is not None:
fcntl.flock(file_obj.fileno(), fcntl.LOCK_UN)


def allocate_case_no(category: str = "DEL", *, deliberation: int = 1) -> str:
"""
Allocate the next Case No. from courtroom/case-registry.yaml.

Per core/case-format.md: registry holds next available NNN per category.
Uses an exclusive file lock so concurrent saves cannot corrupt the registry
or assign duplicate Case Nos. Creates the registry when missing and resets
per-category counters when the calendar year advances past the registry year.
"""
import yaml

year = datetime.now().strftime("%Y")
calendar_year = datetime.now().strftime("%Y")
REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
if not REGISTRY_PATH.exists():
return f"{year}-{category}-001-{deliberation:03d}"

data = yaml.safe_load(REGISTRY_PATH.read_text(encoding="utf-8")) or {}
year = str(data.get("year", year))
categories = data.setdefault("categories", {})
nnn = int(categories.get(category, 1))
case_no = f"{year}-{category}-{nnn:03d}-{deliberation:03d}"
categories[category] = nnn + 1
data["categories"] = categories
data["last_updated"] = datetime.now().strftime("%Y-%m-%d")
REGISTRY_PATH.write_text(
yaml.dump(data, default_flow_style=False, sort_keys=False),
encoding="utf-8",
)
REGISTRY_PATH.write_text(
yaml.dump(
{
"year": int(calendar_year),
"categories": {},
"last_updated": datetime.now().strftime("%Y-%m-%d"),
},
default_flow_style=False,
sort_keys=False,
),
encoding="utf-8",
)

with open(REGISTRY_PATH, "r+", encoding="utf-8") as registry_file:
_lock_registry_file(registry_file)
try:
registry_file.seek(0)
raw = registry_file.read()
data = yaml.safe_load(raw) or {}
year = str(data.get("year", calendar_year))
if year != calendar_year:
data["year"] = int(calendar_year)
data["categories"] = {cat: 1 for cat in (data.get("categories") or {})}
year = calendar_year
categories = data.setdefault("categories", {})
# Registry may seed unused categories at 0; NNN must be 001-999 per case-format.md
nnn = max(int(categories.get(category, 1)), 1)
case_no = f"{year}-{category}-{nnn:03d}-{deliberation:03d}"
categories[category] = nnn + 1
data["categories"] = categories
data["year"] = int(data.get("year", calendar_year))
data["last_updated"] = datetime.now().strftime("%Y-%m-%d")
serialized = yaml.dump(data, default_flow_style=False, sort_keys=False)
registry_file.seek(0)
registry_file.write(serialized)
registry_file.truncate()
registry_file.flush()
os.fsync(registry_file.fileno())
finally:
_unlock_registry_file(registry_file)

return case_no


Expand Down
54 changes: 54 additions & 0 deletions tests/test_executive_watchdog.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,60 @@ def test_run_checks_alerts_on_tampered_judicial_log(
assert any("Log integrity" in a for a in alerts)


def test_run_checks_alerts_on_override_without_proof(
tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
log_path = tmp_path / "judicial_decisions.log"
actions_path = tmp_path / "executive_actions.log"

JudicialLog(log_path=log_path) # empty approvals

_write_action(
actions_path,
action_type="override",
ruling_id="2026-UNKNOWN-999",
description="rogue override",
)

monkeypatch.setattr(watchdog, "JudicialLog", lambda: JudicialLog(log_path=log_path))
monkeypatch.setattr(watchdog, "_actions_log_path", lambda: actions_path)

ok, alerts = watchdog.run_checks()
assert ok is False
assert any("Override action without proof" in a for a in alerts)


def test_run_checks_alerts_on_invalid_override_proof(
tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
log_path = tmp_path / "judicial_decisions.log"
actions_path = tmp_path / "executive_actions.log"
secret_path = tmp_path / ".executive_secret"
secret_path.write_bytes(bytes.fromhex("99" * 32))

proof = OverrideProof.generate(
"2026-DEL-001",
"emergency",
timestamp="2026-05-25T12:00:00+00:00",
secret_path=secret_path,
)
_write_action(
actions_path,
action_type="override",
ruling_id="2026-DEL-002",
description="emergency",
override_proof=proof,
)

monkeypatch.setattr(watchdog, "JudicialLog", lambda: JudicialLog(log_path=log_path))
monkeypatch.setattr(watchdog, "_actions_log_path", lambda: actions_path)
monkeypatch.setenv("EXECUTIVE_PROOF_SECRET", "99" * 64)

ok, alerts = watchdog.run_checks()
assert ok is False
assert any("Invalid override proof" in a for a in alerts)


def test_run_checks_override_frequency_threshold(
tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand Down
104 changes: 104 additions & 0 deletions tests/test_litigation_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from __future__ import annotations

import concurrent.futures
import datetime as dt_module
import re
from pathlib import Path

import pytest
Expand All @@ -10,6 +13,79 @@
from litigation import run as litigation_run


def _patch_calendar_year(monkeypatch: pytest.MonkeyPatch, year: int) -> None:
class FixedDatetime:
@staticmethod
def now(tz=None):
return dt_module.datetime(year, 6, 14)

@staticmethod
def strftime(fmt: str) -> str:
return dt_module.datetime(year, 6, 14).strftime(fmt)

monkeypatch.setattr(litigation_run, "datetime", FixedDatetime)


def _case_no(content: str) -> str:
match = re.search(r"\*\*Case No\.:\*\*\s*(.+)", content)
assert match is not None, content
return match.group(1).strip()


def test_allocate_case_no_creates_registry_when_missing(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
_patch_calendar_year(monkeypatch, 2026)
registry = tmp_path / "case-registry.yaml"
monkeypatch.setattr(litigation_run, "REGISTRY_PATH", registry)

first = litigation_run.allocate_case_no("DEL")
second = litigation_run.allocate_case_no("DEL")

assert first == "2026-DEL-001-001"
assert second == "2026-DEL-002-001"
assert registry.exists()
data = yaml.safe_load(registry.read_text(encoding="utf-8"))
assert data["categories"]["DEL"] == 3


def test_allocate_case_no_resets_sequences_on_year_rollover(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
_patch_calendar_year(monkeypatch, 2026)
registry = tmp_path / "case-registry.yaml"
registry.write_text(
yaml.dump({"year": 2025, "categories": {"DEL": 9, "ARCH": 4}}),
encoding="utf-8",
)
monkeypatch.setattr(litigation_run, "REGISTRY_PATH", registry)

case_no = litigation_run.allocate_case_no("DEL")

assert case_no == "2026-DEL-001-001"
data = yaml.safe_load(registry.read_text(encoding="utf-8"))
assert data["year"] == 2026
assert data["categories"]["DEL"] == 2
assert data["categories"]["ARCH"] == 1


def test_allocate_case_no_treats_zero_category_as_first_case(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
registry = tmp_path / "case-registry.yaml"
registry.write_text(
yaml.dump({"year": 2026, "categories": {"FEAT": 0}}),
encoding="utf-8",
)
monkeypatch.setattr(litigation_run, "REGISTRY_PATH", registry)

case_no = litigation_run.allocate_case_no("FEAT")

assert case_no == "2026-FEAT-001-001"
data = yaml.safe_load(registry.read_text(encoding="utf-8"))
assert data["categories"]["FEAT"] == 2


def test_allocate_case_no_increments_registry(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
registry = tmp_path / "case-registry.yaml"
registry.write_text(
Expand Down Expand Up @@ -76,3 +152,31 @@ def test_save_transcript_filename_suffix_independent_of_case_no(

assert path.name.endswith("-1.md")
assert "**Case No.:** 2026-DEL-020-001" in content


def test_allocate_case_no_concurrent_assignments_are_unique(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
registry = tmp_path / "case-registry.yaml"
registry.write_text(
yaml.dump({"year": 2026, "categories": {"DEL": 40}}),
encoding="utf-8",
)
monkeypatch.setattr(litigation_run, "REGISTRY_PATH", registry)

fake_module = tmp_path / "litigation_pkg"
fake_module.mkdir()
(fake_module / "run.py").write_text("", encoding="utf-8")
transcripts_dir = fake_module / "transcripts"
transcripts_dir.mkdir()
monkeypatch.setattr(litigation_run, "__file__", str(fake_module / "run.py"))

def save_one(i: int) -> str:
path = litigation_run.save_transcript(f"matter {i}", f"body {i}")
return _case_no(path.read_text(encoding="utf-8"))

with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
case_numbers = list(executor.map(save_one, range(8)))

assert len(set(case_numbers)) == len(case_numbers)
assert yaml.safe_load(registry.read_text(encoding="utf-8"))["categories"]["DEL"] == 48