diff --git a/project/ticket-191/README.md b/project/ticket-191/README.md new file mode 100644 index 00000000..7f7b7535 --- /dev/null +++ b/project/ticket-191/README.md @@ -0,0 +1,55 @@ +# Ticket 191: Reduce cyclomatic complexity: get_agent_availability (CC=15) + +- **ID**: ticket-191 +- **Owner**: unresolved:human +- **Status**: IN_PROGRESS +- **Workflow state**: EDIT +- **Created**: 2026-09-20 + +## Goal and scope + +SESSION_EXECUTION_AUTHORIZATION: the planfile queue owner handed STARTER-609 +to this session with explicit completion instructions (checks, commit, +`planfile ticket done STARTER-609`). + +`project/analysis.toon.yaml` reports `get_agent_availability` defined in +`src/koru/agent_availability.py:137` with cyclomatic complexity 15 (limit 15). +Extract the environment-override lookup, the registry-entry parsing and the +temporary-block expiry reset into focused module-level helpers with unchanged +behaviour, bringing the function under the limit 15. + +## Acceptance criteria + +- [x] AC-01: `get_agent_availability` cyclomatic complexity is below 15 + (code2llm re-run on this checkout, output outside the repo tree). +- [x] AC-02: `tests/test_agent_availability.py` passes unmodified. +- [x] AC-03: `ruff check` passes on `src/koru/agent_availability.py`. +- [x] AC-04: `./project/governance-check.sh --base origin/main` passes. + +## Validation evidence + +Recorded 2026-09-20 in `.worktrees/ticket-191--agent-availability-complexity` +(branch `ticket/191-agent-availability-complexity`, base `origin/main` = `f171fe09`): + +- AC-01: `code2llm -f all -o /tmp/opencode/code2llm-ticket191 + --no-chunk --exclude *.md --exclude plugins` — the CC report no longer lists + `get_agent_availability` (module row `agent_availability CC=7`). Independent + AST recount: `get_agent_availability` CC 15 → 5; every extracted helper ≤ 7 + (`_availability_from_registry_entry` 7, `_with_expired_block_reset` 4, + `_environment_override` 3). Behaviour differential over 18 scenarios + (empty id, env precedence both-ways, registry miss/hit, invalid entries, + expiry boundary at equality, corrupt registry) is byte-identical to the + accepted base. +- AC-02: `PYTHONPATH=src python -m pytest tests/test_agent_availability.py -q` + — 7 passed, unmodified. +- AC-03: `ruff check src/koru/agent_availability.py` — All checks passed. + `ruff format --check` findings on pre-existing untouched spans are unchanged + from the accepted base (verified via `git stash` comparison); the refactor + keeps the module's existing formatting idiom verbatim. +- AC-04: `bash project/governance-check.sh --base origin/main` — + `GOV-PASS: passed (0 errors, 0 warnings)`. + +## Tracking boundary + +This directory contains the minimal reviewed intent. Optional participant prose +and raw command logs are not required delivery output. diff --git a/project/ticket-191/intent.json b/project/ticket-191/intent.json new file mode 100644 index 00000000..308ceb91 --- /dev/null +++ b/project/ticket-191/intent.json @@ -0,0 +1,97 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-191", + "summary": "Reduce cyclomatic complexity: get_agent_availability (CC=15)", + "workstream": "application", + "classification": { + "kind": "SERVICE", + "priority": "P2", + "origin": "health" + }, + "allowedPaths": [ + "project/ticket-191/**", + "TODO.md", + "project/TICKETS.md", + "src/koru/agent_availability.py", + "tests/test_agent_availability.py" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md" + ], + "stacks": [], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "f171fe09a90cd59315de70a8b02b87db1158ff89", + "targetBranch": "main", + "outcome": "get_agent_availability drops from cyclomatic complexity 15 to below the code2llm limit 15 by extracting behavior-preserving module-level helpers (_environment_override, _availability_from_registry_entry, _with_expired_block_reset). Env-override precedence (KORU_AGENT_AVAILABLE before KORU_AGENT_UNAVAILABLE), registry parsing, invalid-entry fallback, and temporary-block expiry semantics are byte-for-byte equivalent; tests/test_agent_availability.py passes unmodified.", + "nonGoals": [ + "Changing any observable status, reason, source, observed_at or retry_after value", + "Regenerating project/analysis.toon.yaml or project/planfile-tickets.yaml (integration-owned artifacts)", + "Renaming or removing any public name", + "Reformatting pre-existing spans of src/koru/agent_availability.py outside the refactored function" + ], + "complexity": "S", + "estimatedMinutes": 30, + "budgets": { + "maxImplementationFiles": 1, + "maxAffectedComponents": 1, + "maxPublicInterfaceChanges": 0, + "maxRuntimeDependencies": 0 + }, + "architecture": { + "status": "accepted", + "decision": "Decompose get_agent_availability into three single-responsibility private module functions mirroring the module's existing function-oriented style: environment override resolution, registry-entry parsing with the invalid-entry fallback, and expired-block reset. The public function keeps only normalization and dispatch; each helper returns an explicit AgentAvailability so behavior is equivalent.", + "components": [ + { + "name": "agent-availability", + "paths": [ + "src/koru/agent_availability.py", + "tests/test_agent_availability.py" + ] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [], + "dataChanges": [], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Revert the commit; the original single get_agent_availability body is restored verbatim." + }, + "runtimeDependencies": [], + "validation": [ + { + "criterion": "AC-01", + "commands": [ + "code2llm -f all -o /tmp/opencode/code2llm-ticket191 --no-chunk --exclude *.md --exclude plugins (complexity re-measured on get_agent_availability)" + ], + "evidence": "Re-run code2llm reports CC < 15 for get_agent_availability." + }, + { + "criterion": "AC-02", + "commands": [ + "PYTHONPATH=src python -m pytest tests/test_agent_availability.py -q" + ], + "evidence": "Existing agent-availability suite passes unmodified." + }, + { + "criterion": "AC-03", + "commands": [ + "ruff check src/koru/agent_availability.py" + ], + "evidence": "Lint passes on the refactored module; ruff format status of pre-existing untouched spans is unchanged from the accepted base." + }, + { + "criterion": "AC-04", + "commands": [ + "bash project/governance-check.sh --base origin/main" + ], + "evidence": "Governance gate passes with zero errors." + } + ] + } +} diff --git a/src/koru/agent_availability.py b/src/koru/agent_availability.py index da162f24..158c0f99 100644 --- a/src/koru/agent_availability.py +++ b/src/koru/agent_availability.py @@ -134,31 +134,27 @@ def _registry_write_lock() -> Iterator[None]: fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN) -def get_agent_availability(agent_id: str, *, now: float | None = None) -> AgentAvailability: - """Return effective availability, including environment overrides and expiry.""" - normalized = normalize_agent_id(agent_id) - if not normalized: - return AgentAvailability(agent_id="", reason="empty_agent_id") - if normalized in _env_ids("KORU_AGENT_AVAILABLE"): - return AgentAvailability( - agent_id=normalized, - status="available", - reason="environment override", - source="env:KORU_AGENT_AVAILABLE", - ) - if normalized in _env_ids("KORU_AGENT_UNAVAILABLE"): - return AgentAvailability( - agent_id=normalized, - status="unavailable", - reason="environment override", - source="env:KORU_AGENT_UNAVAILABLE", - ) +def _environment_override(normalized: str) -> AgentAvailability | None: + for env_name, status in ( + ("KORU_AGENT_AVAILABLE", "available"), + ("KORU_AGENT_UNAVAILABLE", "unavailable"), + ): + if normalized in _env_ids(env_name): + return AgentAvailability( + agent_id=normalized, + status=status, + reason="environment override", + source=f"env:{env_name}", + ) + return None - raw = _read_registry().get(normalized) - if raw is None: - return AgentAvailability(agent_id=normalized) + +def _availability_from_registry_entry( + normalized: str, + raw: Mapping[str, Any], +) -> AgentAvailability: try: - availability = AgentAvailability( + return AgentAvailability( agent_id=normalized, status=str(raw.get("status") or "unknown"), reason=str(raw.get("reason") or ""), @@ -172,11 +168,17 @@ def get_agent_availability(agent_id: str, *, now: float | None = None) -> AgentA ) except (TypeError, ValueError): return AgentAvailability(agent_id=normalized, reason="invalid_registry_entry") - current = time.time() if now is None else now + + +def _with_expired_block_reset( + availability: AgentAvailability, + *, + current: float, +) -> AgentAvailability: if availability.blocked and availability.retry_after is not None: if availability.retry_after <= current: return AgentAvailability( - agent_id=normalized, + agent_id=availability.agent_id, reason="temporary block expired", source=availability.source, observed_at=availability.observed_at, @@ -184,6 +186,25 @@ def get_agent_availability(agent_id: str, *, now: float | None = None) -> AgentA return availability +def get_agent_availability(agent_id: str, *, now: float | None = None) -> AgentAvailability: + """Return effective availability, including environment overrides and expiry.""" + normalized = normalize_agent_id(agent_id) + if not normalized: + return AgentAvailability(agent_id="", reason="empty_agent_id") + override = _environment_override(normalized) + if override is not None: + return override + + raw = _read_registry().get(normalized) + if raw is None: + return AgentAvailability(agent_id=normalized) + availability = _availability_from_registry_entry(normalized, raw) + return _with_expired_block_reset( + availability, + current=time.time() if now is None else now, + ) + + def set_agent_availability( agent_id: str, *,