---
phase: 09-storage-abstraction-layer
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - crates/pmcp-tasks/src/store/backend.rs
  - crates/pmcp-tasks/src/domain/record.rs
  - crates/pmcp-tasks/src/error.rs
  - crates/pmcp-tasks/src/store/mod.rs
autonomous: true
requirements:
  - ABST-01
  - ABST-03

must_haves:
  truths:
    - "StorageBackend trait defines 6 async KV methods (get, put, put_if_version, delete, list_by_prefix, cleanup_expired)"
    - "StorageError has domain-aware variants (NotFound, VersionConflict, CapacityExceeded, Backend with source())"
    - "VersionedRecord carries data bytes + monotonic u64 version"
    - "TaskRecord derives Serialize and Deserialize with camelCase rename"
    - "TaskError has ConcurrentModification and StorageFull variants"
    - "Variable schema validation rejects nested depth bombs and extremely long strings"
  artifacts:
    - path: "crates/pmcp-tasks/src/store/backend.rs"
      provides: "StorageBackend trait, StorageError enum, VersionedRecord struct, key helpers"
      exports: ["StorageBackend", "StorageError", "VersionedRecord", "make_key", "make_prefix"]
    - path: "crates/pmcp-tasks/src/domain/record.rs"
      provides: "TaskRecord with Serialize/Deserialize derives"
      contains: "Serialize, Deserialize"
    - path: "crates/pmcp-tasks/src/error.rs"
      provides: "ConcurrentModification and StorageFull error variants"
      contains: "ConcurrentModification"
  key_links:
    - from: "crates/pmcp-tasks/src/store/backend.rs"
      to: "crates/pmcp-tasks/src/store/mod.rs"
      via: "pub mod backend declaration and re-export"
      pattern: "pub mod backend"
    - from: "crates/pmcp-tasks/src/store/backend.rs"
      to: "StorageError"
      via: "std::error::Error impl with source() for Backend variant"
      pattern: "impl.*Error.*for StorageError"
---

<objective>
Define the StorageBackend KV trait contract, StorageError/VersionedRecord types, and prepare TaskRecord for canonical JSON serialization.

Purpose: Establish the foundation types that GenericTaskStore (Plan 02) will build on. The StorageBackend trait defines what any backend must implement (~6 methods), and TaskRecord gains serialization so GenericTaskStore can serialize/deserialize at the storage boundary.

Output: `backend.rs` with complete trait + error types, updated `record.rs` with serde derives, updated `error.rs` with new variants, updated `mod.rs` with module declaration.
</objective>

<execution_context>
@./.claude/get-shit-done/workflows/execute-plan.md
@./.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-storage-abstraction-layer/09-CONTEXT.md
@.planning/phases/09-storage-abstraction-layer/09-RESEARCH.md
@crates/pmcp-tasks/src/store/mod.rs
@crates/pmcp-tasks/src/domain/record.rs
@crates/pmcp-tasks/src/error.rs
@crates/pmcp-tasks/src/lib.rs
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create StorageBackend trait, StorageError, and VersionedRecord in backend.rs</name>
  <files>crates/pmcp-tasks/src/store/backend.rs, crates/pmcp-tasks/src/store/mod.rs</files>
  <action>
Create `crates/pmcp-tasks/src/store/backend.rs` with:

1. **VersionedRecord struct** — holds `data: Vec<u8>` (serialized task record bytes, canonical JSON) and `version: u64` (monotonic version number starting at 1, incremented on each put). Derive Debug, Clone.

2. **StorageError enum** with 4 variants (per locked error model decision):
   - `NotFound { key: String }` — key not found in storage
   - `VersionConflict { key: String, expected: u64, actual: u64 }` — CAS failure
   - `CapacityExceeded { message: String }` — backend at capacity limit
   - `Backend { message: String, source: Option<Box<dyn std::error::Error + Send + Sync>> }` — I/O or backend-specific error

   Implement `std::fmt::Display` for StorageError with meaningful messages. Implement `std::error::Error` for StorageError with `source()` returning the inner error for the `Backend` variant only (per locked decision: "Backend errors include underlying cause via std::error::Error::source()").

3. **StorageBackend async trait** (using `async_trait`) with 6 methods (per locked trait method design decision):
   - `async fn get(&self, key: &str) -> Result<VersionedRecord, StorageError>`
   - `async fn put(&self, key: &str, data: &[u8]) -> Result<u64, StorageError>` — returns assigned version
   - `async fn put_if_version(&self, key: &str, data: &[u8], expected_version: u64) -> Result<u64, StorageError>` — CAS primitive, returns new version
   - `async fn delete(&self, key: &str) -> Result<bool, StorageError>` — returns true if key existed
   - `async fn list_by_prefix(&self, prefix: &str) -> Result<Vec<(String, VersionedRecord)>, StorageError>`
   - `async fn cleanup_expired(&self) -> Result<usize, StorageError>` — best-effort, returns count removed

   Trait must be `Send + Sync`. Add comprehensive rustdoc on the trait (explain keys are composite `{owner_id}:{task_id}`, prefix queries use `{owner_id}:`, backends are dumb KV stores — domain logic lives in GenericTaskStore) and on each method (explain semantics, return values, error conditions).

4. **Key helper functions** (free functions, not on the trait):
   - `pub fn make_key(owner_id: &str, task_id: &str) -> String` — format `"{owner_id}:{task_id}"`
   - `pub fn parse_key(key: &str) -> Option<(&str, &str)>` — split on first colon
   - `pub fn make_prefix(owner_id: &str) -> String` — format `"{owner_id}:"`

   Add doctests on all three helpers.

5. **Update `crates/pmcp-tasks/src/store/mod.rs`**: Add `pub mod backend;` declaration near the top (after `pub mod memory;`). Re-export key types: `pub use backend::{StorageBackend, StorageError, VersionedRecord};`.

6. **Unit tests** in `backend.rs`:
   - Test StorageError Display formatting for all 4 variants
   - Test StorageError source() returns Some for Backend, None for others
   - Test make_key, parse_key, make_prefix with normal and edge cases (empty owner, empty task_id)
   - Test VersionedRecord construction and clone

Do NOT put any domain logic in StorageBackend — it is a dumb KV interface. Do NOT add any retry logic. Do NOT add serialization logic here — that belongs in GenericTaskStore.
  </action>
  <verify>
Run `cargo check -p pmcp-tasks` to verify the new module compiles. Run `cargo test -p pmcp-tasks -- backend` to verify unit tests pass. Run `cargo clippy -p pmcp-tasks -- -D warnings` for zero clippy warnings. Run `cargo test -p pmcp-tasks --doc` for doctest verification.
  </verify>
  <done>
`backend.rs` exists with StorageBackend trait (6 methods), StorageError (4 variants with source()), VersionedRecord, and 3 key helper functions. All unit tests and doctests pass. `mod.rs` re-exports the new types. Zero clippy warnings.
  </done>
</task>

<task type="auto">
  <name>Task 2: Add Serialize/Deserialize to TaskRecord, new error variants, and variable validation</name>
  <files>crates/pmcp-tasks/src/domain/record.rs, crates/pmcp-tasks/src/error.rs, crates/pmcp-tasks/src/lib.rs</files>
  <action>
1. **Update `crates/pmcp-tasks/src/domain/record.rs`** — Add `Serialize` and `Deserialize` derives to `TaskRecord`:

   ```rust
   #[derive(Debug, Clone, Serialize, Deserialize)]
   #[serde(rename_all = "camelCase")]
   pub struct TaskRecord { ... }
   ```

   Add `use serde::{Serialize, Deserialize};` import.

   The `expires_at` field is `Option<DateTime<Utc>>` — keep it as-is. Chrono's serde support (already enabled via `chrono/serde` feature in Cargo.toml) serializes `DateTime<Utc>` to ISO 8601 by default. This is the canonical format per research recommendation. Verify chrono/serde feature is enabled in Cargo.toml — if not, add it.

   Add a `version` field to TaskRecord:
   ```rust
   /// Monotonic version for CAS operations. Not part of the serialized record —
   /// managed by the storage backend.
   #[serde(skip)]
   pub version: u64,
   ```
   Initialize `version: 0` in `TaskRecord::new()`. Update any Clone or construction sites that directly construct TaskRecord (check tests).

   Add **variable schema validation** functions as associated functions or free functions in record.rs (per locked decision: "size limit enforcement PLUS schema validation — no nested depth bombs, no extremely long strings"):
   - `pub fn validate_variable_depth(value: &Value, max_depth: usize) -> Result<(), String>` — recursively walk the JSON value, return error if depth exceeds max_depth. Arrays and objects increment depth.
   - `pub fn validate_variable_string_lengths(value: &Value, max_length: usize) -> Result<(), String>` — recursively walk the JSON value, return error if any string value exceeds max_length bytes.
   - `pub fn validate_variables(variables: &HashMap<String, Value>, max_depth: usize, max_string_length: usize) -> Result<(), String>` — calls both validators for each value.

   Add unit tests for validation functions:
   - Normal values pass
   - Depth bomb (deeply nested object) at depth 11 fails with max_depth=10
   - Long string at 65537 bytes fails with max_string_length=65536
   - Mixed valid values pass
   - Empty map passes
   - Null values pass

   Add a **serialization round-trip test**: create a TaskRecord, serialize to JSON via `serde_json::to_vec`, deserialize back, verify fields match. Verify `version` field is NOT present in the serialized JSON (serde(skip)).

2. **Update `crates/pmcp-tasks/src/error.rs`** — Add two new variants to `TaskError` (per locked error model decision):

   ```rust
   /// Concurrent modification detected via CAS failure.
   ConcurrentModification {
       /// The task ID.
       task_id: String,
       /// The version that was expected.
       expected_version: u64,
       /// The actual version found in storage.
       actual_version: u64,
   },

   /// Storage backend is full or at capacity.
   StorageFull {
       /// Description of the capacity issue.
       message: String,
   },
   ```

   Update the `Display` impl:
   - `ConcurrentModification` -> `"concurrent modification on task {task_id}: expected version {expected}, found {actual}"`
   - `StorageFull` -> `"storage full: {message}"`

   Update the `error_code` method:
   - `ConcurrentModification` -> `-32603` (internal error — caller should retry)
   - `StorageFull` -> `-32603` (internal error)

   Update existing tests in error.rs to cover the new variants (Display, error_code).

3. **Update `crates/pmcp-tasks/src/lib.rs`** — Add re-export for `StorageBackend`, `StorageError`, `VersionedRecord` from `store::backend`:
   ```rust
   pub use store::{StorageBackend, StorageError, VersionedRecord};
   ```
   This makes the types accessible at the crate root for ergonomic imports.

4. **Update `StoreConfig`** in `crates/pmcp-tasks/src/store/mod.rs` — Add two new fields for variable validation (per research recommendation):
   ```rust
   /// Maximum nesting depth for variable JSON values.
   /// Prevents depth bombs. Default: 10.
   pub max_variable_depth: usize,

   /// Maximum length in bytes for any single string value within variables.
   /// Prevents extremely long strings. Default: 65,536 (64 KB).
   pub max_string_length: usize,
   ```
   Update `Default` impl to set `max_variable_depth: 10` and `max_string_length: 65_536`. Update the docstring table.

CRITICAL: Do NOT break any existing tests. The `TaskRecord::new()` change (adding `version: 0`) should be the only behavioral change — all existing fields remain. Run full test suite to confirm. If any test directly constructs `TaskRecord` with struct literal syntax (not `TaskRecord::new()`), update those sites to include `version: 0`.
  </action>
  <verify>
Run `cargo test -p pmcp-tasks` to verify ALL existing tests still pass (200+ tests). Run `cargo test -p pmcp-tasks -- record::tests` and `cargo test -p pmcp-tasks -- error::tests` for the new tests. Run `cargo clippy -p pmcp-tasks -- -D warnings` for zero warnings. Run `cargo test -p pmcp-tasks --doc` for doctests.
  </verify>
  <done>
TaskRecord derives Serialize/Deserialize with camelCase rename and has a version field (serde(skip)). Variable validation functions exist and are tested. TaskError has ConcurrentModification and StorageFull variants with correct Display and error_code. StoreConfig has max_variable_depth and max_string_length fields. All 200+ existing tests pass unchanged. Zero clippy warnings.
  </done>
</task>

</tasks>

<verification>
1. `cargo check -p pmcp-tasks` — compiles with new types
2. `cargo test -p pmcp-tasks` — ALL existing tests pass (regression check)
3. `cargo test -p pmcp-tasks -- backend` — StorageBackend unit tests pass
4. `cargo test -p pmcp-tasks -- record::tests` — serialization round-trip and validation tests pass
5. `cargo test -p pmcp-tasks -- error::tests` — new error variant tests pass
6. `cargo clippy -p pmcp-tasks -- -D warnings` — zero warnings
7. `cargo test -p pmcp-tasks --doc` — all doctests pass
</verification>

<success_criteria>
- StorageBackend trait exists with 6 async methods matching the locked design
- StorageError has 4 domain-aware variants with source() on Backend
- VersionedRecord holds data bytes + u64 version
- TaskRecord serializes to/from canonical JSON (Serialize + Deserialize)
- TaskRecord.version is #[serde(skip)]
- Variable validation catches depth bombs and long strings
- TaskError has ConcurrentModification (with expected/actual versions) and StorageFull variants
- StoreConfig has max_variable_depth and max_string_length fields
- All 200+ existing tests pass unchanged
- Zero clippy warnings
</success_criteria>

<output>
After completion, create `.planning/phases/09-storage-abstraction-layer/09-01-SUMMARY.md`
</output>
