---
phase: 03-memory-layer
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
- src/skill_retriever/memory/component_memory.py
- src/skill_retriever/memory/__init__.py
- tests/test_component_memory.py
autonomous: true
must_haves:
truths:
- "Component memory tracks recommendation counts per component"
- "Component memory tracks selection counts per component"
- "Component memory tracks co-selection patterns between component pairs"
- "Component memory persists to JSON and reloads correctly"
artifacts:
- path: "src/skill_retriever/memory/component_memory.py"
provides: "ComponentUsageStats, CoSelectionEntry, ComponentMemory models with persistence"
exports: ["ComponentMemory", "ComponentUsageStats", "CoSelectionEntry"]
min_lines: 80
- path: "tests/test_component_memory.py"
provides: "Component memory unit tests"
min_lines: 60
key_links:
- from: "src/skill_retriever/memory/component_memory.py"
to: "pydantic"
via: "BaseModel with model_dump_json/model_validate_json"
pattern: "model_dump_json|model_validate_json"
---
<objective>
Build the component memory subsystem: a Pydantic-based tracker that records recommendation counts, selection counts, and co-selection patterns (DeepAgent-style), with JSON file persistence.
Purpose: Component memory provides a feedback signal for the retrieval system. Components that are frequently selected together should be recommended together. Components that are recommended but never selected should be deprioritized. This data feeds into Phase 4's score fusion to improve recommendation quality over time.
Output: `component_memory.py` with Pydantic models and persistence, tests
</objective>
<execution_context>
@C:\Users\33641\.claude/get-shit-done/workflows/execute-plan.md
@C:\Users\33641\.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-memory-layer/03-RESEARCH.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Create ComponentMemory models with tracking and persistence</name>
<files>
src/skill_retriever/memory/component_memory.py
src/skill_retriever/memory/__init__.py
</files>
<action>
Create `src/skill_retriever/memory/component_memory.py` with:
1. `ComponentUsageStats(BaseModel)`:
- `component_id: str`
- `recommendation_count: int = 0`
- `selection_count: int = 0`
- `last_recommended: datetime | None = None`
- `last_selected: datetime | None = None`
2. `CoSelectionEntry(BaseModel)`:
- `component_a: str`
- `component_b: str` (invariant: a < b lexicographically)
- `count: int = 0`
3. `ComponentMemory(BaseModel)`:
- `usage_stats: dict[str, ComponentUsageStats] = Field(default_factory=dict)`
- `co_selections: dict[str, CoSelectionEntry] = Field(default_factory=dict)`
Methods:
a) `record_recommendation(self, component_id: str) -> None`:
- Get or create ComponentUsageStats for this ID
- Increment recommendation_count
- Set last_recommended to datetime.now(tz=UTC)
b) `record_selection(self, component_ids: list[str]) -> None`:
- For each component: get or create stats, increment selection_count, set last_selected
- For each PAIR (a, b) where a < b: get or create CoSelectionEntry with key `"{a}|{b}"`, increment count
c) `get_co_selected(self, component_id: str, top_k: int = 5) -> list[tuple[str, int]]`:
- Scan co_selections for entries containing this component_id
- Return the other component in each pair, sorted by count descending, limited to top_k
d) `get_selection_rate(self, component_id: str) -> float`:
- Return selection_count / recommendation_count for the component
- Return 0.0 if recommendation_count is 0
e) `save(self, path: str) -> None`:
- Write `self.model_dump_json(indent=2)` to file at path
f) `load(cls, path: str) -> ComponentMemory` (classmethod):
- Read JSON from file, return `ComponentMemory.model_validate_json(text)`
- If file doesn't exist, return `ComponentMemory()` (empty)
Use `from __future__ import annotations`. Use `from datetime import UTC, datetime` with `# noqa: TC003` for the datetime runtime import (following project convention).
AVOID:
- Do NOT use mutable default arguments
- Do NOT forget to sort (a, b) pairs so the key is deterministic regardless of input order
- Do NOT use pickle -- JSON via Pydantic model_dump_json is the standard
</action>
<verify>
Run `uv run python -c "from skill_retriever.memory.component_memory import ComponentMemory; m = ComponentMemory(); m.record_recommendation('test'); print(m.usage_stats['test'].recommendation_count)"` prints 1.
Run `uv run pyright src/skill_retriever/memory/component_memory.py` passes.
Run `uv run ruff check src/skill_retriever/memory/component_memory.py` passes.
</verify>
<done>
ComponentMemory, ComponentUsageStats, CoSelectionEntry models exist with all tracking methods and JSON persistence. Type checks and linting pass.
</done>
</task>
<task type="auto">
<name>Task 2: Write component memory tests</name>
<files>
tests/test_component_memory.py
</files>
<action>
Create `tests/test_component_memory.py` with pytest tests:
1. **test_record_recommendation** -- Record recommendation for a component. Verify recommendation_count == 1, last_recommended is set, selection_count remains 0.
2. **test_record_multiple_recommendations** -- Record 3 recommendations for same component. Verify recommendation_count == 3.
3. **test_record_selection** -- Record selection of [A, B, C]. Verify each has selection_count == 1. Verify co_selections has 3 entries (A|B, A|C, B|C) each with count == 1.
4. **test_co_selection_ordering** -- Record selection of [B, A] (reverse order). Verify the key is "A|B" (sorted), not "B|A".
5. **test_get_co_selected** -- Record selection of [A, B] twice and [A, C] once. Call get_co_selected(A). Verify B is returned first (count=2), then C (count=1).
6. **test_get_co_selected_top_k** -- Record selections creating 6 co-selection partners for one component. Call get_co_selected with top_k=3. Verify only 3 returned.
7. **test_get_selection_rate** -- Record 10 recommendations and 3 selections for a component. Verify get_selection_rate returns 0.3.
8. **test_get_selection_rate_zero_recommendations** -- Verify returns 0.0 for component with no recommendations.
9. **test_save_and_load** -- Create memory, record some data, save to tmp_path. Load from same path. Verify all counts and co-selections match.
10. **test_load_nonexistent_returns_empty** -- Call load() on nonexistent path. Verify returns empty ComponentMemory.
11. **test_selection_updates_last_selected** -- Record selection, verify last_selected is set and is recent (within 1 second of now).
</action>
<verify>
Run `uv run pytest tests/test_component_memory.py -v` -- all tests pass.
Run `uv run ruff check tests/test_component_memory.py` passes.
</verify>
<done>
11 tests covering recommendation tracking, selection tracking, co-selection patterns (including sort invariant), selection rate, save/load persistence, and edge cases all pass.
</done>
</task>
</tasks>
<verification>
- `uv run pytest tests/test_component_memory.py -v` -- all tests green
- `uv run pyright src/skill_retriever/memory/component_memory.py` -- zero errors
- `uv run ruff check src/skill_retriever/memory/` -- zero warnings
- Save/load round-trip preserves all usage stats and co-selection data
</verification>
<success_criteria>
1. ComponentMemory tracks per-component recommendation and selection counts
2. Co-selection patterns are tracked for all component pairs with deterministic key ordering
3. get_co_selected returns partners ranked by frequency
4. get_selection_rate computes recommendation-to-selection ratio
5. JSON persistence via Pydantic model_dump_json/model_validate_json round-trips correctly
6. All linting and type checks pass
</success_criteria>
<output>
After completion, create `.planning/phases/03-memory-layer/03-03-SUMMARY.md`
</output>