Domain: retry orchestration across a dispatch facade

Issue:
The attempts option on execute() does nothing. An operation that fails once with a transient error fails the whole call immediately even with attempts=3, and our instrumentation shows the operation was only invoked a single time. The default behavior without the option is correct — fail fast on the first transient error — and the option value is validated, it just has no effect. Expected behavior: execute() retries an operation that raises a transient error up to the configured total number of attempts, as the API documents, re-raising only after the final attempt fails.

Repository files:
content: # Present so pytest adds the repository root to sys.path, making `import src`
# resolve from the test suite.

path: conftest.py

content: # Pure standard library unless the environment image pre-bakes wheels.

path: requirements.lock

path: src/__init__.py

content: """Public facade for running operations."""

from . import service


def execute(name, op, attempts=1):
    """Run an operation and return its Execution.

    attempts: retry the operation up to this many total tries when it
    raises TransientError; the final failure is re-raised if every attempt
    fails. The default is a single attempt (fail fast).
    """
    if attempts < 1:
        raise ValueError("attempts must be >= 1")
    return service.dispatch(name, op)

path: src/api.py

content: """Execution engine."""


def run(op):
    """Invoke op once and return its result."""
    return op()

path: src/engine.py

content: """Exceptions and results for operation execution."""

from dataclasses import dataclass


class TransientError(Exception):
    """A failure that may succeed if the operation is tried again."""


@dataclass(frozen=True)
class Execution:
    """Outcome of a completed operation."""

    name: str
    value: object

path: src/models.py

content: """Plain-text rendering of execution outcomes."""


def to_lines(executions):
    """Format executions as 'name=value' lines."""
    return ["{}={}".format(execution.name, execution.value) for execution in executions]


def to_text(executions):
    """Render executions as a newline-joined document."""
    return "\n".join(to_lines(executions))

path: src/report.py

content: """Dispatch layer: validates, runs, and wraps operations."""

from . import engine
from .models import Execution


def dispatch(name, op):
    """Run op through the engine and wrap the result."""
    if not name:
        raise ValueError("operation name must be non-empty")
    value = engine.run(op)
    return Execution(name, value)

path: src/service.py

content: """Tests for the execution facade, dispatch, engine, and reporting."""

import pytest

from src.api import execute
from src.engine import run
from src.models import Execution, TransientError
from src.report import to_lines, to_text
from src.service import dispatch


def counting(value):
    state = {"calls": 0}

    def op():
        state["calls"] += 1
        return value

    return op, state


def test_execute_success_first_try():
    op, state = counting(42)
    assert execute("job", op) == Execution("job", 42)
    assert state["calls"] == 1


def test_execute_fails_fast_by_default():
    state = {"calls": 0}

    def op():
        state["calls"] += 1
        raise TransientError("try again")

    with pytest.raises(TransientError):
        execute("job", op)
    assert state["calls"] == 1


def test_execute_validates_attempts():
    with pytest.raises(ValueError):
        execute("job", lambda: 1, attempts=0)


def test_dispatch_wraps_and_validates():
    assert dispatch("fetch", lambda: "v") == Execution("fetch", "v")
    with pytest.raises(ValueError):
        dispatch("", lambda: "v")


def test_engine_runs_op():
    assert run(lambda: 7) == 7


def test_report_formats_executions():
    executions = [Execution("a", 1), Execution("b", "x")]
    assert to_lines(executions) == ["a=1", "b=x"]
    assert to_text(executions) == "a=1\nb=x"

path: tests/public/test_public.py

Reference patch:
--- a/src/api.py
+++ b/src/api.py
@@ -12,4 +12,4 @@ def execute(name, op, attempts=1):
     """
     if attempts < 1:
         raise ValueError("attempts must be >= 1")
-    return service.dispatch(name, op)
+    return service.dispatch(name, op, attempts)
--- a/src/engine.py
+++ b/src/engine.py
@@ -1,6 +1,13 @@
 """Execution engine."""
 
+from .models import TransientError
 
-def run(op):
-    """Invoke op once and return its result."""
-    return op()
+
+def run(op, attempts=1):
+    """Invoke op, retrying TransientError up to attempts total tries."""
+    for remaining in range(attempts - 1, -1, -1):
+        try:
+            return op()
+        except TransientError:
+            if remaining == 0:
+                raise
--- a/src/service.py
+++ b/src/service.py
@@ -4,9 +4,9 @@ from . import engine
 from .models import Execution
 
 
-def dispatch(name, op):
+def dispatch(name, op, attempts=1):
     """Run op through the engine and wrap the result."""
     if not name:
         raise ValueError("operation name must be non-empty")
-    value = engine.run(op)
+    value = engine.run(op, attempts)
     return Execution(name, value)


Hidden acceptance tests (diff):
diff --git a/tests/hidden/test_hidden.py b/tests/hidden/test_hidden.py
new file mode 100644
--- /dev/null
+++ b/tests/hidden/test_hidden.py
@@ -0,0 +1,46 @@
+"""Hidden evaluation tests. Never agent-visible (DEC-015).
+
+FAIL_TO_PASS: the facade's attempts option is documented but never reaches
+the engine, so any transient failure fails the call on the first try; all
+three tests fail at baseline. They pass once attempts is threaded through
+dispatch to the engine's retry loop, with exact invocation counts pinning
+the loop's behavior.
+"""
+
+import pytest
+
+from src.api import execute
+from src.models import TransientError
+
+
+def flaky(fail_times, value):
+    state = {"calls": 0}
+
+    def op():
+        state["calls"] += 1
+        if state["calls"] <= fail_times:
+            raise TransientError("try again")
+        return value
+
+    return op, state
+
+
+def test_attempts_retry_until_success():
+    op, state = flaky(2, "ok")
+    execution = execute("job", op, attempts=3)
+    assert execution.value == "ok"
+    assert state["calls"] == 3
+
+
+def test_attempts_stop_as_soon_as_one_succeeds():
+    op, state = flaky(1, 5)
+    execution = execute("job", op, attempts=4)
+    assert execution.value == 5
+    assert state["calls"] == 2
+
+
+def test_attempts_exhausted_reraises_after_final_try():
+    op, state = flaky(5, "never")
+    with pytest.raises(TransientError):
+        execute("job", op, attempts=3)
+    assert state["calls"] == 3