From 988a169831b9f2b2c24138dc7c4d2fa528610141 Mon Sep 17 00:00:00 2001 From: HAL9000 Date: Thu, 23 Apr 2026 12:20:59 +0000 Subject: [PATCH] docs: add InvariantReconciliationActor API docs, devcontainer discovery module guide, and mkdocs nav - docs/api/actor.md: Add InvariantReconciliationActor section documenting the built-in reconciliation actor introduced in v3.8.0, including the reconciliation algorithm, ReconciliationResult/ConflictRecord/ScopeInvariants data classes, standalone reconcile_invariants() function, DI registration, and failure behaviour. - docs/modules/devcontainer-discovery.md: New module guide for the devcontainer auto-discovery system (v3.8.0 fix #2615), covering DevcontainerDiscoveryResult, discover_devcontainers(), is_trigger_type(), monorepo named-config support, and gotchas. - mkdocs.yml: Add 'Devcontainer Auto-Discovery' to the Modules nav section, alongside shell-safety, uko-provenance, invariant-reconciliation, context-hydration, and git-worktree-sandbox module docs. - robot/coverage_threshold.robot: Add tdd_issue and tdd_issue_4305 tags to the Noxfile Contains Coverage Threshold Constant test case. ISSUES CLOSED: #4485 --- docs/api/actor.md | 124 ++++++++++++++++++ docs/modules/devcontainer-discovery.md | 175 +++++++++++++++++++++++++ mkdocs.yml | 1 + robot/coverage_threshold.robot | 2 +- 4 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 docs/modules/devcontainer-discovery.md diff --git a/docs/api/actor.md b/docs/api/actor.md index 329d47311..74569d73a 100644 --- a/docs/api/actor.md +++ b/docs/api/actor.md @@ -100,6 +100,130 @@ nodes, and any warnings. --- +## `InvariantReconciliationActor` + +**Introduced:** v3.8.0 | **Module:** `cleveragents.actor.reconciliation` + +The built-in `InvariantReconciliationActor` (`builtin/invariant-reconciliation`) +is invoked by `PlanLifecycleService` when a plan enters the Strategize phase. +This gate must succeed before Strategize work begins; the lifecycle service +persists the reconciled invariant view and reuses it for later Execute or Apply +transitions instead of re-running the actor. The actor collects invariants +from four scopes, resolves conflicts using specificity-based precedence, +records `invariant_enforced` decisions, and returns a reconciled +`InvariantSet`. + +See [ADR-016 — Invariant System](../adr/ADR-016-invariant-system.md) for the +design rationale behind invariant reconciliation. + +```python +from cleveragents.actor.reconciliation import InvariantReconciliationActor + +actor = InvariantReconciliationActor( + invariant_service=container.invariant_service(), + decision_service=container.decision_service(), +) +result = actor.run(plan_id="01HXYZ...", project_name="local/my-app") +# result.reconciled_set — effective InvariantSet +# result.conflicts — list[ConflictRecord] with resolution details +# result.enforced_decision_ids — list of ULID strings +``` + +### Reconciliation Algorithm + +1. Collect invariants from four scopes: **global**, **project**, **action**, **plan**. +2. Group by normalised text (case-insensitive, stripped). +3. Detect conflicts between invariants at different scopes. +4. Resolve using specificity: `plan > action > project > global`. + Exception: `non_overridable` global invariants always win. +5. Record an `invariant_enforced` decision for each active invariant. +6. Return a reconciled `InvariantSet`. + +### `InvariantReconciliationActor` Methods + +| Method signature | Description | +|------------------|-------------| +| `collect_invariants(*, plan_id: str \| None = None, project_name: str \| None = None, action_name: str \| None = None) -> ScopeInvariants` | Collect invariants from all four scopes and return them as a `ScopeInvariants` container | +| `run(*, plan_id: str, project_name: str \| None = None, action_name: str \| None = None, parent_decision_id: str \| None = None) -> ReconciliationResult` | Execute the reconciliation lifecycle (collect → reconcile → record) and return a `ReconciliationResult` | + +### `ReconciliationResult` + +```python +from cleveragents.actor.reconciliation import ReconciliationResult + +@dataclass(frozen=True) +class ReconciliationResult: + reconciled_set: InvariantSet # effective invariants + conflicts: list[ConflictRecord] # detected conflicts with resolution details + enforced_decision_ids: list[str] # ULIDs of invariant_enforced decisions +``` + +### `ConflictRecord` + +```python +from cleveragents.actor.reconciliation import ConflictRecord + +@dataclass(frozen=True) +class ConflictRecord: + key: str # normalised invariant text used for grouping + winner: Invariant # invariant that prevailed + losers: list[Invariant] # invariants that were overridden + reason: str # human-readable explanation of the resolution +``` + +### `ScopeInvariants` + +Convenience container grouping invariants by scope tier: + +```python +from cleveragents.actor.reconciliation import ScopeInvariants + +scope_invs = ScopeInvariants( + global_invariants=[...], + project_invariants=[...], + action_invariants=[...], + plan_invariants=[...], +) +all_invs = scope_invs.all_invariants() # flat list across all scopes +``` + +### `reconcile_invariants` (standalone function) + +```python +from cleveragents.actor.reconciliation import reconcile_invariants + +reconciled, conflicts = reconcile_invariants(scope_invariants) +# reconciled — list[Invariant] (winners only) +# conflicts — list[ConflictRecord] +``` + +### Constructing the actor via dependency injection + +`InvariantReconciliationActor` is instantiated directly with application +services resolved from the DI container: + +```python +from cleveragents.application.container import Container +from cleveragents.actor.reconciliation import InvariantReconciliationActor + +container = Container() +actor = InvariantReconciliationActor( + invariant_service=container.invariant_service(), + decision_service=container.decision_service(), +) +``` + +### Failure Behaviour + +Reconciliation failures block the phase transition with +`ReconciliationBlockedError` and emit an `INVARIANT_VIOLATED` event. The +exception is raised by `PlanLifecycleService`, importable via +`from cleveragents.application.services.plan_lifecycle_service import ReconciliationBlockedError`. +Post-correction reconciliation runs via `CORRECTION_APPLIED` event subscription +(best-effort; does not block correction completion). + +--- + ## Example: Loading and Compiling an Actor ```python diff --git a/docs/modules/devcontainer-discovery.md b/docs/modules/devcontainer-discovery.md new file mode 100644 index 000000000..e63f9a079 --- /dev/null +++ b/docs/modules/devcontainer-discovery.md @@ -0,0 +1,175 @@ +# Devcontainer Auto-Discovery + +**Package:** `cleveragents.resource.handlers.discovery` +**Introduced:** v3.8.0 (issue #2615) + +This module guide covers the devcontainer auto-discovery system, which +automatically detects `devcontainer.json` configurations when a +`git-checkout` or `fs-directory` resource is linked to a project. +It supports both root-level configurations and named configurations +for monorepo projects. + +--- + +## Purpose + +When a `git-checkout` or `fs-directory` resource is registered, the +`discover_devcontainers()` function scans the resource location for +`devcontainer.json` files. Each valid configuration is returned as a +`DevcontainerDiscoveryResult`, which the resource registry uses to +create child `devcontainer-instance` resources automatically. + +--- + +## Scan Paths + +The scanner checks three categories of paths: + +| Category | Path | `config_name` | +|----------|------|---------------| +| Root-level (default) | `.devcontainer/devcontainer.json` | `None` | +| Root-level (flat) | `.devcontainer.json` | `None` | +| Named configuration | `.devcontainer//devcontainer.json` | `""` | + +Named configurations are discovered by iterating one subdirectory level +inside `.devcontainer/`. Each subdirectory that contains a +`devcontainer.json` produces a separate result with `config_name` set to +the subdirectory name (e.g. `"api"`, `"frontend"`). + +--- + +## Key Classes and Functions + +### `DevcontainerDiscoveryResult` + +```python +from cleveragents.resource.handlers.discovery import DevcontainerDiscoveryResult + +result = DevcontainerDiscoveryResult( + config_path=Path("/workspace/.devcontainer/api/devcontainer.json"), + config_data={"name": "API Dev Container", "image": "mcr.microsoft.com/devcontainers/python:3.12"}, + parent_location="/workspace", + config_name="api", +) +``` + +| Attribute | Type | Description | +|-----------|------|-------------| +| `config_path` | `Path` | Absolute path to the `devcontainer.json` file | +| `config_data` | `dict[str, object]` | Parsed JSON content of the devcontainer config | +| `parent_location` | `str` | Filesystem path of the parent resource | +| `config_name` | `str \| None` | Named configuration identifier, or `None` for root-level configs | + +--- + +### `discover_devcontainers` + +```python +from cleveragents.resource.handlers.discovery import discover_devcontainers + +results = discover_devcontainers( + resource_location="/workspace", + resource_type="git-checkout", +) +for result in results: + print(result.config_name, result.config_path) +``` + +Scans a resource location for all valid devcontainer configurations. +Invalid JSON files are skipped with a warning log rather than raising. + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `resource_location` | `str` | Filesystem path of the parent resource | +| `resource_type` | `str` | Type name of the parent resource | + +**Returns:** `list[DevcontainerDiscoveryResult]` — one entry per valid config. + +**Raises:** +- `ValueError` — if `resource_location` is empty or `resource_type` is empty/blank. + +--- + +### `is_trigger_type` + +```python +from cleveragents.resource.handlers.discovery import is_trigger_type + +is_trigger_type("git-checkout") # True +is_trigger_type("sqlite") # False +``` + +Returns `True` if the given resource type triggers devcontainer auto-discovery. +Currently only `"git-checkout"` and `"fs-directory"` are trigger types. + +--- + +## Monorepo Example + +Given a monorepo with the following layout: + +``` +/workspace/ +├── .devcontainer/ +│ ├── api/ +│ │ └── devcontainer.json → config_name="api" +│ └── frontend/ +│ └── devcontainer.json → config_name="frontend" +└── src/ +``` + +`discover_devcontainers("/workspace", "git-checkout")` returns two results: + +```python +results = discover_devcontainers("/workspace", "git-checkout") +# results[0].config_name == "api" +# results[1].config_name == "frontend" +``` + +Each result produces a distinct `devcontainer-instance` resource in the +registry, allowing plans to target individual containers by name. + +--- + +## Root-Level Example + +For a single-container project: + +``` +/workspace/ +├── .devcontainer/ +│ └── devcontainer.json → config_name=None +└── src/ +``` + +```python +results = discover_devcontainers("/workspace", "git-checkout") +# results[0].config_name is None +``` + +Root-level configs retain `config_name=None` for full backward compatibility. + +--- + +## Gotchas + +- **Non-trigger types return an empty list.** Calling `discover_devcontainers()` + with a resource type that is not `"git-checkout"` or `"fs-directory"` returns + `[]` immediately without scanning the filesystem. +- **Invalid JSON is silently skipped.** Files that cannot be parsed as JSON + objects are logged at `WARNING` level and excluded from results. +- **Only one subdirectory level is scanned.** Named configurations must be + directly inside `.devcontainer//devcontainer.json`. Deeper nesting + is not supported. +- **Sorted order.** Named configurations are returned in lexicographic order + of their subdirectory names (via `sorted(dc_dir.iterdir())`). + +--- + +## Related Documentation + +- [Resource System API](../api/resource.md) — `ResourceHandler` protocol and built-in handlers +- [ADR-043 Devcontainer Integration](../adr/ADR-043-devcontainer-integration.md) — design rationale +- [Resource System](../api/resource.md#devcontainerhandler) — `DevcontainerHandler` CRUD operations diff --git a/mkdocs.yml b/mkdocs.yml index 5d0e312b0..a00676f38 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,6 +30,7 @@ nav: - Invariant Reconciliation: modules/invariant-reconciliation.md - ACMS Context Hydration: modules/context-hydration.md - Git Worktree Sandbox: modules/git-worktree-sandbox.md + - Devcontainer Auto-Discovery: modules/devcontainer-discovery.md - Showcase: - Overview: showcase/index.md - CLI Tools: showcase/cli-tools/ diff --git a/robot/coverage_threshold.robot b/robot/coverage_threshold.robot index 92babb4af..2d4180f9f 100644 --- a/robot/coverage_threshold.robot +++ b/robot/coverage_threshold.robot @@ -9,7 +9,7 @@ Suite Teardown Cleanup Test Environment *** Test Cases *** Noxfile Contains Coverage Threshold Constant [Documentation] Verify COVERAGE_THRESHOLD = 96.5 is defined in noxfile.py - [Tags] coverage config + [Tags] coverage config tdd_issue tdd_issue_4305 ${content}= Get File ${WORKSPACE}/noxfile.py Should Contain ${content} COVERAGE_THRESHOLD = 96.5