#!/usr/bin/env python3 """Detect and publish the Kanban API boundary required by CLI lanes.""" from __future__ import annotations import inspect import sys from dataclasses import asdict, dataclass from pathlib import Path from typing import Any, Callable from cli_lane_config import STATE_ROOT, utc_now from cli_lane_files import atomic_json, load_json _CAPABILITIES: KanbanCapabilities | None = None _CAPABILITY_SOURCE: tuple[int, int] | None = None @dataclass(frozen=True) class KanbanCapabilities: """Safety-relevant keyword support exposed by the image's Kanban DB.""" exact_run_completion: bool ended_run_replay: bool exact_run_reclaim: bool @property def ready(self) -> bool: """Return whether dispatch and every recovery transition are safe.""" return ( self.exact_run_completion and self.ended_run_replay and self.exact_run_reclaim ) @property def deferred_features(self) -> tuple[str, ...]: """List fixed, bounded capability names absent from the image API.""" missing = [] if not self.exact_run_completion: missing.append("exact-run-completion") if not self.ended_run_replay: missing.append("ended-run-replay") if not self.exact_run_reclaim: missing.append("exact-run-reclaim") return tuple(missing) def _explicit_keyword(function: Callable[..., Any], keyword: str) -> bool: """Require an explicit keyword parameter rather than trusting ``**kwargs``.""" try: parameter = inspect.signature(function).parameters.get(keyword) except (TypeError, ValueError): return False return parameter is not None and parameter.kind in { inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY, } def detect_kanban_capabilities(kanban_db: Any) -> KanbanCapabilities: """Inspect the loaded image API without invoking a mutating DB operation.""" complete_task = getattr(kanban_db, "complete_task", None) reclaim_task = getattr(kanban_db, "reclaim_task", None) return KanbanCapabilities( exact_run_completion=callable(complete_task) and _explicit_keyword(complete_task, "expected_run_id"), ended_run_replay=callable(complete_task) and _explicit_keyword(complete_task, "replay_ended_run_id"), exact_run_reclaim=callable(reclaim_task) and _explicit_keyword(reclaim_task, "expected_run_id"), ) def _source_identity(kanban_db: Any) -> tuple[int, int]: """Bind cached capability evidence to the two inspected callables.""" return ( id(getattr(kanban_db, "complete_task", None)), id(getattr(kanban_db, "reclaim_task", None)), ) def _health_document(capabilities: KanbanCapabilities) -> dict[str, Any]: """Return a bounded operator-readable compatibility health document.""" return { "component": "cli-lane-runner", "state": "ready" if capabilities.ready else "deferred", "ready": capabilities.ready, "active_run_completion_safe": capabilities.exact_run_completion, "deferred_features": list(capabilities.deferred_features), "capabilities": asdict(capabilities), "observed_at": utc_now(), } def initialize_kanban_capabilities( kanban_db: Any, *, health_path: Path | None = None, ) -> KanbanCapabilities: """Detect startup compatibility and durably expose ready/deferred health.""" global _CAPABILITIES, _CAPABILITY_SOURCE capabilities = detect_kanban_capabilities(kanban_db) _CAPABILITIES = capabilities _CAPABILITY_SOURCE = _source_identity(kanban_db) destination = health_path or STATE_ROOT / "runtime-health.json" destination.parent.mkdir(parents=True, exist_ok=True) atomic_json(destination, _health_document(capabilities), 0o600) if not capabilities.ready: print( "CLI lane safety compatibility deferred; missing image APIs: " + ", ".join(capabilities.deferred_features), file=sys.stderr, flush=True, ) return capabilities def kanban_capabilities(kanban_db: Any) -> KanbanCapabilities: """Return startup evidence, redetecting only when the loaded API changes.""" if _CAPABILITIES is None or _source_identity(kanban_db) != _CAPABILITY_SOURCE: return initialize_kanban_capabilities(kanban_db) return _CAPABILITIES def runtime_health(path: Path | None = None) -> dict[str, Any]: """Read the bounded health document for probes and operator diagnostics.""" document = load_json(path or STATE_ROOT / "runtime-health.json") return document if isinstance(document, dict) else {}