# Plan Diff & Apply Reference

This document describes the diff review and apply integration features
added in D0b.apply.

## CLI Commands

### `agents plan diff <plan_id>`

Show the ChangeSet produced during Execute as a unified diff, grouped
by resource path.

**Options:**

| Flag | Description |
|------|-------------|
| `--format`, `-f` | Output format: `rich` (default), `plain`, `json`, `yaml` |

**Examples:**

```bash
# Rich output with coloured operation labels
agents plan diff 01HXYZ...

# Plain unified-diff output
agents plan diff 01HXYZ... --format plain

# Machine-readable JSON
agents plan diff 01HXYZ... --format json
```

**Output fields (JSON/YAML):**

| Field | Type | Description |
|-------|------|-------------|
| `changeset_id` | string | ULID of the ChangeSet |
| `plan_id` | string | ULID of the plan |
| `total_changes` | int | Number of change entries |
| `summary` | object | `{creates, modifies, deletes, renames, paths_changed, resources_involved}` |
| `entries` | list | Per-file entries with `path`, `operation`, hashes, timestamps |

### `agents plan artifacts <plan_id>`

Show plan artifacts: ChangeSet metadata, sandbox references, file
change list, and validation results.

**Options:**

| Flag | Description |
|------|-------------|
| `--format`, `-f` | Output format: `rich` (default), `plain`, `json`, `yaml` |

**Output fields (JSON/YAML):**

| Field | Type | Description |
|-------|------|-------------|
| `plan_id` | string | Plan ULID |
| `phase` | string | Current lifecycle phase |
| `processing_state` | string | Current processing state |
| `changeset_id` | string | ChangeSet ULID (null if Execute not complete) |
| `sandbox_refs` | list | Active sandbox reference IDs |
| `changeset_summary` | object | Summary counts from SpecChangeSet |
| `files_changed` | list | `{path, operation}` per changed file |
| `validation_summary` | object | Validation results (if available) |
| `apply_summary` | object | Files changed count, validations run (after apply) |

## Apply Integration

### Empty ChangeSet Guard

The apply pipeline checks whether the ChangeSet has any entries before
proceeding.  If the ChangeSet is empty, apply is blocked with a clear
message:

```
Plan <id> has an empty ChangeSet. No changes to apply. Use --allow-empty to override.
```

Set `--allow-empty` to bypass this check for plans that intentionally
produce no file changes (e.g. validation-only plans).

### Apply Summary Persistence

When apply completes, the following metadata is stored in the plan:

- `apply_files_changed`: Number of files written/modified/deleted
- `apply_validations_run`: Number of validation checks executed
- `apply_completed_at`: ISO-8601 timestamp of apply completion

This metadata is visible via `agents plan status` and `agents plan artifacts`.

### Merge Failure Handling

When a sandbox merge fails during apply:

1. The plan transitions to `ERRORED` processing state
2. `error_message` is set to `"Merge failed: <details>"`
3. `error_details` includes:
   - `merge_conflict`: Description of the conflict
   - `sandbox_rollback`: Set to `"pending"` for cleanup

**Recovery steps:**

1. Review error details and recovery hints via `agents plan errors <id>`
2. Review conflict details via `agents plan status <id>`
3. Re-run execute phase after resolving conflicts: `agents plan execute <id>`
4. Or fix validation issues and retry apply

See [Error Recovery Reference](error_recovery.md) for the full error
classification, retry policy, and recovery hint system.

### Processing State Flow

```
Execute/COMPLETE
    |
    v
Apply/QUEUED  -->  Apply/PROCESSING  -->  Apply/APPLIED (terminal success)
                        |
                        +-->  Apply/ERRORED (merge failure)
                        +-->  Apply/CONSTRAINED (invariant violation)
```

### Validation-Gated Apply

The apply pipeline gates commits on validation results.  Before any
file changes are committed, `apply_with_validation_gate()` checks
the validation summary (either from the plan or supplied externally)
and blocks apply if any **required** validations have failed.

#### Outcomes

| Outcome | Description |
|---------|-------------|
| `applied` | All required validations passed; changes committed |
| `constrained` | Required validations failed; apply blocked |
| `already_applied` | Plan was already in a terminal applied state |
| `blocked_empty` | ChangeSet was empty and `allow_empty` not set |

#### Flow

```
apply_with_validation_gate(plan_id)
    |
    +-- Plan in terminal state? --> already_applied / constrained
    |
    +-- Empty ChangeSet? --> blocked_empty (unless allow_empty)
    |
    +-- required_failed > 0? --> constrained (with actionable message)
    |
    +-- All passed --> persist_apply_summary() --> complete_apply()
                       --> ApplyResult(outcome="applied")
```

#### `ApplyResult` Model

| Field | Type | Description |
|-------|------|-------------|
| `outcome` | `ApplyOutcome` | One of the outcomes above |
| `plan_id` | `str` | Plan ULID |
| `message` | `str` | Human-readable result message |
| `files_changed` | `int` | Number of files changed during apply |
| `validations_total` | `int` | Total validations evaluated |
| `validations_passed` | `int` | Required validations that passed |
| `validations_failed` | `int` | Required validations that failed |

#### External Validation Summary

You can supply a validation summary from an external source (e.g. a
CI pipeline) to override the plan's stored summary:

```python
result = service.apply_with_validation_gate(
    plan_id,
    validation_summary={"total": 3, "required_passed": 2, "required_failed": 1},
)
```

## Service API

### `PlanApplyService`

```python
from cleveragents.application.services.plan_apply_service import PlanApplyService

service = PlanApplyService(
    lifecycle_service=lifecycle,
    changeset_store=store,  # optional InMemoryChangeSetStore
)

# Generate diff
diff_text = service.diff(plan_id, fmt="json")

# Get artifacts
artifacts = service.artifacts(plan_id, fmt="json")

# Guard empty changeset
service.guard_empty_changeset(plan_id, allow_empty=False)

# Persist apply summary
service.persist_apply_summary(plan_id, files_changed=5, validations_run=3)

# Handle merge failure
service.handle_merge_failure(plan_id, conflict_details="...")

# Validation-gated apply
from cleveragents.application.services.plan_apply_service import ApplyOutcome, ApplyResult

result = service.apply_with_validation_gate(plan_id, allow_empty=False)
if result.outcome == ApplyOutcome.APPLIED:
    print(f"Applied: {result.files_changed} files changed")
elif result.outcome == ApplyOutcome.CONSTRAINED:
    print(f"Blocked: {result.message}")
```
