docs: add InvariantReconciliationActor API docs, devcontainer discovery module guide, and mkdocs nav
CI / benchmark-regression (push) Has been skipped
CI / benchmark-publish (push) Failing after 41s
CI / quality (push) Failing after 1m4s
CI / security (push) Failing after 1m4s
CI / unit_tests (push) Failing after 1m3s
CI / build (push) Failing after 45s
CI / push-validation (push) Successful in 47s
CI / helm (push) Successful in 42s
CI / lint (push) Successful in 1m33s
CI / e2e_tests (push) Failing after 45s
CI / integration_tests (push) Failing after 47s
CI / typecheck (push) Successful in 1m43s
CI / docker (push) Has been skipped
CI / coverage (push) Has been skipped
CI / status-check (push) Failing after 6s
CI / benchmark-publish (pull_request) Has been skipped
CI / helm (pull_request) Successful in 50s
CI / build (pull_request) Successful in 57s
CI / lint (pull_request) Successful in 1m9s
CI / quality (pull_request) Successful in 1m18s
CI / benchmark-regression (pull_request) Failing after 1m16s
CI / typecheck (pull_request) Successful in 1m30s
CI / security (pull_request) Successful in 1m36s
CI / e2e_tests (pull_request) Successful in 3m52s
CI / integration_tests (pull_request) Successful in 4m30s
CI / push-validation (pull_request) Successful in 20s
CI / unit_tests (pull_request) Successful in 6m31s
CI / docker (pull_request) Successful in 1m36s
CI / coverage (pull_request) Successful in 12m19s
CI / status-check (pull_request) Successful in 4s
CI / benchmark-regression (push) Has been skipped
CI / benchmark-publish (push) Failing after 41s
CI / quality (push) Failing after 1m4s
CI / security (push) Failing after 1m4s
CI / unit_tests (push) Failing after 1m3s
CI / build (push) Failing after 45s
CI / push-validation (push) Successful in 47s
CI / helm (push) Successful in 42s
CI / lint (push) Successful in 1m33s
CI / e2e_tests (push) Failing after 45s
CI / integration_tests (push) Failing after 47s
CI / typecheck (push) Successful in 1m43s
CI / docker (push) Has been skipped
CI / coverage (push) Has been skipped
CI / status-check (push) Failing after 6s
CI / benchmark-publish (pull_request) Has been skipped
CI / helm (pull_request) Successful in 50s
CI / build (pull_request) Successful in 57s
CI / lint (pull_request) Successful in 1m9s
CI / quality (pull_request) Successful in 1m18s
CI / benchmark-regression (pull_request) Failing after 1m16s
CI / typecheck (pull_request) Successful in 1m30s
CI / security (pull_request) Successful in 1m36s
CI / e2e_tests (pull_request) Successful in 3m52s
CI / integration_tests (pull_request) Successful in 4m30s
CI / push-validation (pull_request) Successful in 20s
CI / unit_tests (pull_request) Successful in 6m31s
CI / docker (pull_request) Successful in 1m36s
CI / coverage (pull_request) Successful in 12m19s
CI / status-check (pull_request) Successful in 4s
- 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
This commit was merged in pull request #10840.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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/<name>/devcontainer.json` | `"<name>"` |
|
||||
|
||||
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/<name>/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
|
||||
Reference in New Issue
Block a user