Skip to content
Open
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
88 changes: 88 additions & 0 deletions docs/decisions/anti-entropy-benchmark-decisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Architecture Decision Record: Anti-Entropy Governance Benchmark & Evaluation Harness

- **Status**: Accepted (Design Grill Complete)
- **Date**: 2026-10-29
- **Scope**: Out-of-band evaluation harness for quantifying anti-entropy (Aegis delete-first) impact
- **Target Constraint**: 100% zero-intrusion to LoopX core control plane (`loopx/control_plane/`), default-off opt-in capability

---

## 1. Context & Problem Statement

When autonomous coding agents execute refactoring, feature replacement, or protocol migration tasks, they suffer from systematic "entropy growth":
1. **Fallback/Wrapper Addiction**: Instead of cleanly deleting old classes/functions, agents keep them as backwards-compatibility shims or wrap them inside new logic.
2. **Ghost Code Residue**: Agents frequently comment out superseded blocks (`# old_logic()`) rather than deleting them.
3. **Zombie Reintroduction**: When tests fail, agents often recreate the deprecated symbol in the newly modified files.

LoopX possesses an existing deterministic AST-based anti-entropy engine (`loopx/capabilities/anti_entropy/`). The core question was:
- Should test/benchmark scenarios overlay a full real-world development task?
- How should the anti-entropy capability be injected into commands to measure the delta between having vs. not having anti-entropy governance?
- What task scenario is most appropriate to evaluate this without compromising LoopX core stability?

---

## 2. Decision Points & Trade-Off Analysis

### Decision 1: Loading Mechanism & Interception Timing
- **Considered Alternatives**:
- *Option 1*: Pure prompt guidance (soft constraint). Zero tooling overhead, but models consistently regress under multi-file pressure.
- *Option 2*: Post-hoc terminal gate only. Verifies diff at submission; fails late when the agent has already exhausted budget, leading to blind rollback loops.
- *Option 3 (Selected, partially delivered)*: **Two-Tier Closed Loop (Execution Tool + Submission Gate)**.
- Delivered now: the execution tier as an explicitly callable entry (`anti_entropy_inspect` tool descriptor plus `loopx anti-entropy inspect`) returning actionable progress-fact feedback, and the gate tier as a named delivery route — final blocking stays owned by the existing change-quality `prepare/record/verify` receipt, not a parallel authority.
- Not yet proven: that models actually adopt the tool and prefer deletion in long-horizon runs. That claim requires actual recorded runs and is intentionally not asserted by this ADR or the comparator.
- **Trade-off**: Increases tool-call rounds and slight token overhead per run, in exchange for deterministic first-pass cleanliness and eliminating expensive blind rollbacks.

---

### Decision 2: Zero-Intrusion Packaging & Retirement Basis Injection
- **Considered Alternatives**:
- *Option 1*: Hardwire anti-entropy check into `loopx/control_plane/turn_driver/`. Causes direct intrusion into generic mainline scheduling, violating domain-neutrality.
- *Option 2*: Dynamic runtime AST inference without declaration. Prone to false positives when agents refactor internal helper functions.
- *Option 3 (Selected)*: **Declarative Sidecar / Task-Envelope Hook Pattern**.
- The task specification explicitly provides an `anti_entropy_basis` (target symbols and target paths to retire).
- Delivered now: the typed turn-context projection (`build_anti_entropy_turn_context`) a host can inject when the capability is enabled for the task, and the explicit CLI/tool entries agents call.
- Named follow-up (not delivered here): a runner that conditionally injects the inspect tool into a host agent envelope and appends the verify command to a test command. Owner: the host integration that owns agent envelopes; post-writeback typed intent stays with the provider-neutral post-writeback capability-hooks RFC, including failure isolation and idempotence.
- `loopx/control_plane/` remains completely unmodified (0-line diff).
- **Trade-off**: Requires test cases to declare their target retirement list in advance, rather than inferring intent magically.

---

### Decision 3: Evaluation Benchmark Scenario (Testbed Task) Selection
- **Considered Alternatives**:
- *Option 1*: In-repo dogfooding on live LoopX modules. High risk of breaking the developer active branch during failed baseline runs; violates mainline safety.
- *Option 2*: External SWE-bench Docker benchmark. Requires heavyweight containers and environment setups; feedback loop too slow for local iterative testing.
- *Option 3 (Selected)*: **Self-Contained Realistic Engineering Playground in `examples/anti_entropy/`**.
- A multi-module Python service (protocol parser + streaming receiver + client caller + unit test suite).
- Real task: Migrate all callers from `LegacyProtocolParser` to `StreamMessageParser` and physically delete `legacy_parser.py`.
- The comparator is a deterministic, fixed-input sandbox demonstration: per arm it seeds a disposable git sandbox, applies the fixed post-state, captures the real diff with git, executes the case-study functional tests with pytest, and evaluates the oracle against the bound sandbox workspace. It measures oracle semantics only — it launches no model runs and measures no agent-runtime behavior, so its output must not be read as an agent-effectiveness result.
- **Trade-off**: Tests on a standalone engineering playground rather than mutating production LoopX code directly.

---

## 3. First-Principles Minimality Justification (Aegis Integration)

1. **Change Necessity**: Reuses the existing `loopx/capabilities/anti_entropy/` AST engine without creating redundant verification abstractions.
2. **Fallback Ceiling**: Refuses to add fallback adapters; enforces physical deletion of deprecated symbols (`/dev/null` diff + AST symbol absence).
3. **Delete-First & Anti-Entropy**: The evaluation suite directly measures net negative lines of code, absence of zombie wrappers, and 0 ghost code blocks.
4. **Mainline Purity**: All new harness scripts live in `examples/anti_entropy/` and `docs/decisions/`. No files under `loopx/control_plane/` are touched.

---

## 4. Delivered Boundary (This PR)

- **Delivered**: default-off capability with explicit typed entries — `loopx anti-entropy verify` (delivery-gate semantics, exit 1 on rejection), `loopx anti-entropy inspect` (in-flight progress-fact feedback, exit 0), the `anti_entropy_inspect` tool descriptor registered in the capability catalog, the `build_anti_entropy_turn_context` projection, and the single-task feedback cycle classifying findings as actionable progress facts. Symbol retirement certification requires an explicitly bound workspace (`--workspace`); diff-only receipts may reject but never certify, and evidence provenance (`basis_hash`, `source_root`, `source_evidence`) is recorded on every receipt.
- **Delivery authority**: unchanged and owned by change-quality (`prepare/record/verify`, exact diff scope, receipt invalidation). This capability supplies evidence and names the delivery route; it writes no delivery receipt.
- **Not claimed**: model adoption of the loop in real agent runs, measured entropy reduction on live workloads, or any comparator-derived effectiveness number. Those require the recorded-run evaluation program (Decision 3) executed for real, and the envelope-injection runner from Decision 2.
- **Source binding invariants**: complete post-change sources are AST-parsed; documentation files never contribute symbol evidence; string literals, comments, and docstring examples are never counted as references or definitions; a missing bound source is evidence-insufficient (AE004), never silently skipped.

---

## 5. Impacted System Components & Retired Paths

- **Impacted Components**:
- `docs/decisions/anti-entropy-benchmark-decisions.md`: Canonical ADR record.
- `examples/anti_entropy/case_study/`: Standalone realistic multi-file testbed.
- `examples/anti_entropy/compare_arms.py`: Out-of-band evaluation comparator for measuring differential.
- **Retired / Disallowed Paths**:
- Disallowed: Modifying `loopx/control_plane/turn_driver/executor.py` for benchmark gates.
- Disallowed: Ad-hoc heuristic AST diff guesswork without a declared basis.
138 changes: 138 additions & 0 deletions examples/anti-entropy-governance-smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""Smoke test for anti-entropy governance: AST-based delete-first verification and diff hygiene.

Exercises the source-binding contract: symbol retirement certification requires
an explicitly bound workspace with the complete post-change sources; diff-only
evidence can reject but never certifies.
"""

from __future__ import annotations

import sys
import tempfile
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))

from loopx.capabilities.anti_entropy.cli import run_anti_entropy_verify # noqa: E402
from loopx.capabilities.anti_entropy.contract import ( # noqa: E402
ANTI_ENTROPY_CAPABILITY_ID,
RetirementBasis,
)
from loopx.capabilities.anti_entropy.oracles import evaluate_diff_hygiene # noqa: E402


def _bound_workspace(content: str) -> Path:
workspace = Path(tempfile.mkdtemp(prefix="ae_smoke_ws_"))
(workspace / "calculator.py").write_text(content, encoding="utf-8")
return workspace


def test_clean_delete_first() -> None:
diff = """
--- a/calculator.py
+++ b/calculator.py
@@ -1,5 +1,5 @@
-def old_add(a: int, b: int) -> int:
- return a + b
+def new_add(a: int, b: int) -> int:
+ return a + b
"""
workspace = _bound_workspace("def new_add(a: int, b: int) -> int:\n return a + b\n")
basis = RetirementBasis(
target_symbols=("old_add",),
rationale="Replaced by new_add",
)
receipt = evaluate_diff_hygiene(diff, basis, workspace=workspace)
assert receipt.verdict == "passed", f"expected passed, got {receipt.verdict}"
assert "old_add" in receipt.retired_symbols
assert receipt.source_evidence == "workspace_sources"


def test_diff_only_evidence_cannot_certify() -> None:
diff = """
--- a/calculator.py
+++ b/calculator.py
@@ -1,5 +1,5 @@
-def old_add(a: int, b: int) -> int:
- return a + b
+def new_add(a: int, b: int) -> int:
+ return a + b
"""
basis = RetirementBasis(target_symbols=("old_add",))
receipt = evaluate_diff_hygiene(diff, basis)
assert receipt.verdict == "rejected", f"expected rejected, got {receipt.verdict}"
assert any(f.rule_id == "AE004-INSUFFICIENT_SOURCE_EVIDENCE" for f in receipt.findings)
assert receipt.retired_symbols == ()


def test_delete_first_violation() -> None:
diff = """
--- a/calculator.py
+++ b/calculator.py
@@ -1,3 +1,5 @@
+def new_add(a: int, b: int) -> int:
+ return a + b
def old_add(a: int, b: int) -> int:
return a + b
"""
workspace = _bound_workspace("def new_add(a: int, b: int) -> int:\n return a + b\ndef old_add(a: int, b: int) -> int:\n return a + b\n")
basis = RetirementBasis(
target_symbols=("old_add",),
allow_compat_seam=False,
)
receipt = evaluate_diff_hygiene(diff, basis, workspace=workspace)
assert receipt.verdict == "rejected", f"expected rejected, got {receipt.verdict}"
assert any(f.rule_id == "AE001-DELETE_FIRST_VIOLATION" for f in receipt.findings)


def test_zombie_reintroduced() -> None:
diff = """
--- a/calculator.py
+++ b/calculator.py
@@ -1,3 +1,3 @@
-def legacy_sink():
+def legacy_sink(): # wrapper
pass
"""
basis = RetirementBasis(target_symbols=("legacy_sink",))
receipt = evaluate_diff_hygiene(diff, basis)
assert receipt.verdict == "rejected", f"expected rejected, got {receipt.verdict}"
assert any(f.rule_id == "AE002-ZOMBIE_SYMBOL_REINTRODUCED" for f in receipt.findings)


def test_cli_runner() -> None:
diff = """
--- a/module.py
+++ b/module.py
@@ -1,2 +1,2 @@
-class OldService:
+class NewService:
pass
"""
workspace = Path(tempfile.mkdtemp(prefix="ae_smoke_ws_"))
(workspace / "module.py").write_text("class NewService:\n pass\n", encoding="utf-8")
payload = run_anti_entropy_verify(
diff_text=diff,
basis_data={"target_symbols": ["OldService"]},
goal_id="test-goal-1",
workspace=workspace,
)
assert payload["ok"] is True
assert payload["capability_id"] == ANTI_ENTROPY_CAPABILITY_ID
assert "OldService" in payload["retired_symbols"]


def main() -> int:
test_clean_delete_first()
test_diff_only_evidence_cannot_certify()
test_delete_first_violation()
test_zombie_reintroduced()
test_cli_runner()
print("anti-entropy-governance-smoke: ok")
return 0


if __name__ == "__main__":
sys.exit(main())
19 changes: 19 additions & 0 deletions examples/anti_entropy/case_study/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Consumer client currently bound to legacy parser."""

from typing import Any
from .legacy_parser import LegacyMessageParser


class DataConsumer:
"""High-level data consumer."""

def __init__(self) -> None:
self._parser = LegacyMessageParser()

def process_message(self, raw_input: str) -> dict[str, Any]:
result = self._parser.decode(raw_input)
return {
"status": "ok",
"content": result["payload"],
"length": result["size"],
}
28 changes: 28 additions & 0 deletions examples/anti_entropy/case_study/legacy_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Legacy message parser slated for retirement."""

from typing import Any


class LegacyMessageParser:
"""Old-style frame decoder with delimiter separation."""

def __init__(self, delimiter: str = "|") -> None:
self.delimiter = delimiter

def decode(self, raw_data: str) -> dict[str, Any]:
parts = raw_data.strip().split(self.delimiter)
if len(parts) < 3:
raise ValueError("Malformed legacy frame")
header, payload, checksum = parts[0], parts[1], parts[2]
return {
"version": "v1",
"header": header,
"payload": payload,
"checksum": checksum,
"size": len(payload),
}


def parse_legacy_frame(raw_data: str) -> dict[str, Any]:
"""Standalone legacy helper."""
return LegacyMessageParser().decode(raw_data)
20 changes: 20 additions & 0 deletions examples/anti_entropy/case_study/stream_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Modern stream message parser replacing legacy parser."""

from typing import Any


class StreamMessageParser:
"""Modern stream parser parsing structured payload with validation."""

def parse(self, raw_data: str) -> dict[str, Any]:
# Accepts both pipe format and key-value formats
parts = raw_data.strip().split("|")
if len(parts) >= 3:
return {
"version": "v2",
"header": parts[0],
"payload": parts[1],
"checksum": parts[2],
"size": len(parts[1]),
}
raise ValueError("Invalid stream payload")
12 changes: 12 additions & 0 deletions examples/anti_entropy/case_study/task_spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"task_id": "case_study_parser_retirement",
"objective": "Migrate DataConsumer in client.py from LegacyMessageParser to StreamMessageParser. Fully retire LegacyMessageParser and delete legacy_parser.py cleanly without leaving backward-compatibility shims or dead code.",
"functional_test_command": "pytest examples/anti_entropy/case_study/tests/test_client.py -q",
"retirement_basis": {
"schema_version": "loopx_anti_entropy_retirement_basis_v0",
"target_symbols": ["LegacyMessageParser", "parse_legacy_frame"],
"target_paths": ["examples/anti_entropy/case_study/legacy_parser.py"],
"allow_compat_seam": false,
"rationale": "Superseded by StreamMessageParser in stream_parser.py"
}
}
11 changes: 11 additions & 0 deletions examples/anti_entropy/case_study/tests/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Tests for DataConsumer."""

from examples.anti_entropy.case_study.client import DataConsumer


def test_consumer_normal_flow() -> None:
consumer = DataConsumer()
res = consumer.process_message("HDR|hello world|CRC32")
assert res["status"] == "ok"
assert res["content"] == "hello world"
assert res["length"] == 11
Loading
Loading