# Change Tracking (C5.model)

This document describes the ChangeSet domain models, ToolInvocation
tracking, and the SkillInvocationTracker used for auditability of
tool executions during the Execute phase.

## Overview

CleverAgents uses a **tool-based change model**: LLMs call tools
directly, tools modify sandbox state, and a `ChangeSet` is built from
those tool invocations.  Every resource modification is explicit and
tracked.

## Models

### ChangeOperation (enum)

| Value    | Description               |
|----------|---------------------------|
| `create` | New file created          |
| `modify` | Existing file changed     |
| `delete` | File removed              |
| `rename` | File moved or renamed     |

`ChangeType` is an alias for `ChangeOperation`.

### ChangeEntry

A single recorded change from a tool execution.

| Field         | Type               | Required | Description                       |
|---------------|--------------------|----------|-----------------------------------|
| `entry_id`    | `str` (ULID)       | auto     | Unique identifier                 |
| `plan_id`     | `str` (ULID)       | yes      | Owning plan                       |
| `resource_id` | `str` (ULID)       | yes      | Affected resource                 |
| `tool_name`   | `str`              | yes      | Namespaced tool name              |
| `operation`   | `ChangeOperation`  | yes      | Type of change                    |
| `path`        | `str`              | yes      | Repo-relative file path           |
| `before_hash` | `str \| None`      | no       | SHA-256 before change             |
| `after_hash`  | `str \| None`      | no       | SHA-256 after change              |
| `before_mode` | `int \| None`      | no       | File mode before change           |
| `after_mode`  | `int \| None`      | no       | File mode after change            |
| `timestamp`   | `datetime`         | auto     | UTC timestamp                     |

**Validation rules:**
- CREATE must not have `before_hash` (no prior content).
- DELETE must not have `after_hash` (no resulting content).

**Integrity check:** `has_integrity_hashes` property returns `True` when
the operation-appropriate hashes are all present.

### SpecChangeSet

Accumulated set of ChangeEntry records for a plan.

| Field          | Type                | Description                   |
|----------------|---------------------|-------------------------------|
| `changeset_id` | `str` (ULID)       | Unique identifier             |
| `plan_id`      | `str` (ULID)       | Owning plan                   |
| `entries`      | `list[ChangeEntry]` | Ordered change entries        |
| `created_at`   | `datetime`          | UTC creation timestamp        |

**Methods:**
- `summary()` -- flat counts (total, creates, modifies, deletes, renames)
- `sorted_entries()` -- deterministic order: `(resource_id, path, timestamp)`
- `grouped_by_resource()` -- dict grouped by resource_id, sorted within
- `add_change(entry)` -- append a change entry

### ToolInvocation

Record of a single tool execution for auditability.

| Field              | Type                    | Description                        |
|--------------------|-------------------------|------------------------------------|
| `invocation_id`    | `str` (ULID)            | Unique identifier                  |
| `plan_id`          | `str` (ULID)            | Plan that triggered the call       |
| `tool_name`        | `str`                   | Namespaced tool name               |
| `skill_name`       | `str \| None`           | Skill that provided the tool       |
| `arguments`        | `dict`                  | Input parameters                   |
| `result`           | `dict \| None`          | Output payload                     |
| `error`            | `str \| None`           | Error message on failure           |
| `success`          | `bool`                  | Whether execution succeeded        |
| `duration_ms`      | `float`                 | Wall-clock time in milliseconds    |
| `started_at`       | `datetime`              | UTC start timestamp                |
| `completed_at`     | `datetime \| None`      | UTC end timestamp                  |
| `change_ids`       | `list[str]`             | ChangeEntry IDs produced           |
| `sequence_number`  | `int`                   | Ordering within a plan             |
| `sandbox_path`     | `str \| None`           | Sandbox root used                  |
| `resource_refs`    | `list[str]`             | Resource IDs involved              |
| `provider_metadata`| `dict \| None`          | Provider info (model, latency)     |

### InvocationTracker (Protocol)

| Method                          | Returns                | Description                     |
|---------------------------------|------------------------|---------------------------------|
| `track(invocation)`             | `None`                 | Record a tool invocation        |
| `get_invocations(plan_id)`      | `list[ToolInvocation]` | All invocations for a plan      |
| `get_invocations_for_skill(...)`| `list[ToolInvocation]` | Filter by skill within a plan   |
| `get_changes(plan_id)`          | `list[str]`            | All change IDs for a plan       |

### InMemoryInvocationTracker

In-memory implementation of the `InvocationTracker` protocol.
Returns invocations sorted by `sequence_number`.

## Path Normalization

`normalize_change_path(path, repo_root)` converts absolute paths to
repo-relative POSIX paths by stripping the repo root prefix.

## Usage

```python
from cleveragents.domain.models.core.change import (
    ChangeEntry, ChangeOperation, SpecChangeSet,
    ToolInvocation, InMemoryInvocationTracker,
    normalize_change_path,
)

# Create and populate a changeset
cs = SpecChangeSet(plan_id="plan-123")
cs.add_change(ChangeEntry(
    plan_id="plan-123",
    resource_id="res-1",
    tool_name="builtin/file-write",
    operation=ChangeOperation.CREATE,
    path=normalize_change_path("/sandbox/src/new.py", "/sandbox"),
    after_hash="abc123",
))

# Track a tool invocation
tracker = InMemoryInvocationTracker()
tracker.track(ToolInvocation(
    plan_id="plan-123",
    tool_name="builtin/file-write",
    skill_name="local/file-ops",
    change_ids=[cs.entries[0].entry_id],
))

# Get grouped view for plan diff
for resource_id, entries in cs.grouped_by_resource().items():
    print(f"Resource {resource_id}: {len(entries)} changes")
```
