Files
zesdex/docs/superpowers/plans/2026-07-16-cms-settings-appconfig-memory-editlog-wiring.md
T

738 lines
34 KiB
Markdown

# CMS Wiring: Settings, AppConfig, Memory, EditLog Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace `zesdex-backend`'s use of `zesdex_entities::seaorm::common::{settings,app_config,memory,edit_log}` (inherent-method I/O) with the previously-orphaned `zesdex-cms` crate's repository-based equivalents (`JsonSettingsRepository`, `JsonAppConfigRepository`, `MarkdownMemoryRepository`, `JsonlEditLogRepository`), which are on-disk-format-compatible drop-ins. Fix the one real defect found in `zesdex-cms::Settings` along the way (a missing `#[serde(default)]` that would hard-fail loading any pre-existing `settings.json`).
**Architecture:** `zesdex-backend` call sites move from `Type::static_method()` to `repository_instance.method(&base_dir, ...)`. Since `zesdex-cms`'s application-service layer (`SettingsServiceImpl`, `MemoryServiceImpl`) doesn't cover every read pattern the backend needs (no bare `AppConfig` getter, no single-memory read, no `EditLogService` at all), most call sites construct and use the concrete `Json*Repository`/`MarkdownMemoryRepository`/`JsonlEditLogRepository` types directly rather than going through the service traits — this matches the actual usage shape better than forcing everything through an ill-fitting service abstraction.
**Tech Stack:** Rust, Cargo workspace (`zesdex-cms`, `zesdex-backend`, `zesdex-entities`).
## Global Constraints
- On-disk formats are confirmed compatible for all four entities (same JSON/markdown/JSONL shape and file paths) — this plan is a call-site migration, **not** a data migration. No existing user `settings.json`/`app_config.json`/`*.md` memory files/`edits.jsonl` need to change.
- `EditLog`'s domain shape genuinely differs between the old (`entries` + `path`, disk I/O on construction) and new (`entries` only, disk I/O via `EditLogRepository::open`) versions — every call site becomes fallible (`Result`) where it was previously infallible.
- No new `#[allow(...)]` attributes. Existing ones in touched files are out of scope (handled by `2026-07-16-convention-cleanup-docs.md`).
- Tests are inline `#[cfg(test)] mod tests`, per CLAUDE.md.
- Run `cargo test --workspace` and `cargo clippy --workspace --all-targets -- -D warnings` before each commit.
- Base directory resolution: everywhere the old code called the zero-argument `Settings::load()`/`Settings::save()`/`AppConfig::load()` (which internally called `zesdex_entities::seaorm::common::store::Store::new()` to get `base_dir`), the replacement must call `zesdex_entities::seaorm::common::store::Store::new().base_dir` explicitly and pass it to the `zesdex-cms` repository — this reproduces the exact same directory, verified identical during planning.
---
### Task 1: Fix the `hive_mind_node_timeout_ms` serde-default gap in `zesdex-cms::Settings`
**Context:** `crates/zesdex-entities/src/seaorm/common/settings.rs:75` has `#[serde(default = "default_hive_mind_node_timeout_ms")]` on this field; `crates/zesdex-cms/src/domain/settings.rs`'s copy does not. Without this, any settings.json written before this field existed (or any settings.json missing it for any reason) will hard-fail `JsonSettingsRepository::load` with a parse error, whereas the old `Settings::load()` silently defaulted on **any** failure. Fix both the missing default and restore full parse-failure tolerance to avoid a behavior regression for existing users.
**Files:**
- Modify: `crates/zesdex-cms/src/domain/settings.rs`
- Modify: `crates/zesdex-cms/src/infrastructure/persistence/settings_repo.rs`
**Interfaces:**
- Produces: `Settings::default()` unchanged in value; `JsonSettingsRepository::load` becomes tolerant of parse failures (still `Result`-returning, but only returns `Err` for I/O errors other than "not found" or "malformed JSON" — matching the old infallible-except-I/O-permission-errors behavior as closely as a `Result`-based API can).
- [ ] **Step 1: Write the failing test**
Add to `crates/zesdex-cms/src/infrastructure/persistence/settings_repo.rs`'s `#[cfg(test)] mod tests` (create it if absent — check first: `grep -n "mod tests" crates/zesdex-cms/src/infrastructure/persistence/settings_repo.rs`):
```rust
#[test]
fn load_defaults_hive_mind_timeout_when_field_missing_from_old_settings_json() {
let dir = std::env::temp_dir().join(format!("zesdex-cms-settings-test-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).unwrap();
// Simulate a settings.json written before `hive_mind_node_timeout_ms` existed.
std::fs::write(
dir.join("settings.json"),
r#"{"internet_mode":"Off","provider":"zen","model":"m","api_keys":{},"max_tokens":null,"temperature":null,"review_max_lessons_per_run":5,"adaptive_review_max_skip":3,"verify_command":null,"verify_timeout_ms":30000,"workflow_max_concurrency":5,"review_enabled":true,"session_archive_enabled":true,"lsp_auto_provision":true,"lsp_languages":[]}"#,
).unwrap();
let repo = JsonSettingsRepository::new();
let settings = repo.load(&dir).expect("load must not fail on a pre-existing settings.json missing the new field");
assert_eq!(settings.hive_mind_node_timeout_ms, 600_000);
let _ = std::fs::remove_dir_all(&dir);
}
```
(Requires `uuid` as a dev-dependency of `zesdex-cms` — check first: `grep uuid crates/zesdex-cms/Cargo.toml`; add `uuid = { workspace = true }` under `[dev-dependencies]` if missing, creating that section if it doesn't exist.)
- [ ] **Step 2: Run the test to verify it fails**
Run: `cargo test -p zesdex-cms load_defaults_hive_mind -- --nocapture`
Expected: fails with a JSON parse/missing-field error.
- [ ] **Step 3: Add the serde default to the domain type**
In `crates/zesdex-cms/src/domain/settings.rs`, add above the `Settings` struct:
```rust
fn default_hive_mind_node_timeout_ms() -> u64 {
600_000
}
```
And annotate the field:
```rust
#[serde(default = "default_hive_mind_node_timeout_ms")]
pub hive_mind_node_timeout_ms: u64,
```
- [ ] **Step 4: Restore full parse-failure tolerance in the repository**
In `crates/zesdex-cms/src/infrastructure/persistence/settings_repo.rs`, update `load`:
```rust
fn load(&self, base_dir: &Path) -> Result<Settings> {
let path = base_dir.join("settings.json");
match std::fs::read_to_string(&path) {
Ok(s) => match serde_json::from_str(&s) {
Ok(settings) => Ok(settings),
Err(e) => {
tracing::warn!(
"settings.json at '{}' failed to parse ({e}); falling back to defaults",
path.display()
);
Ok(Settings::default())
}
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
tracing::info!("settings.json not found, using defaults");
Ok(Settings::default())
}
Err(e) => Err(anyhow::anyhow!("failed to read settings.json: {e}")),
}
}
```
- [ ] **Step 5: Run the test to verify it passes**
Run: `cargo test -p zesdex-cms load_defaults_hive_mind -- --nocapture`
Expected: pass.
- [ ] **Step 6: Run the crate's full test suite and clippy**
Run: `cargo test -p zesdex-cms && cargo clippy -p zesdex-cms -- -D warnings`
Expected: all pass, no new warnings.
- [ ] **Step 7: Commit**
```bash
git add crates/zesdex-cms/src/domain/settings.rs crates/zesdex-cms/src/infrastructure/persistence/settings_repo.rs
git commit -m "fix(cms): perbaiki serde default hive_mind_node_timeout_ms & toleransi parse gagal di Settings"
```
---
### Task 2: Swap `Settings` call sites to `JsonSettingsRepository`
**Files (every one confirmed by research — swap all):**
- `crates/zesdex-backend/src/app/state/rest.rs` (lines 20, 49, 89, 103, 170)
- `crates/zesdex-backend/src/bin/seed.rs` (line 12)
- `crates/zesdex-backend/src/app/subagent/engine.rs` (lines 61, 64-84, 353 doc comment)
- `crates/zesdex-backend/src/app/workflow/hive_mind.rs` (lines 255, 281-283, 367 doc comments)
- `crates/zesdex-backend/src/app/runtime/context/window.rs` (lines 9, 19-25, 46/56/79 test-only)
- `crates/zesdex-backend/src/app/mode/settings.rs` (lines 6, 16-22)
- `crates/zesdex-backend/src/controller/input.rs` (lines 339, 354, 357-361, 393-407)
- `crates/zesdex-backend/src/view/mod.rs` (lines 152-174, 701-713 — read-only, no method-call change needed beyond the type import)
- `crates/zesdex-backend/src/view/status.rs` (lines 68, 86-93 — read-only)
- `crates/zesdex-backend/src/app/review/mod.rs` (lines 79, 85, 405-406 — read-only)
- `crates/zesdex-backend/src/app/runtime/actions/mod.rs` (lines 549, 633-671, 1615, 1758 — read-only)
- `crates/zesdex-backend/src/main.rs` (lines 141, 474, 641 — `.save()` calls)
**Interfaces:**
- Consumes: `zesdex_cms::domain::settings::Settings`, `zesdex_cms::infrastructure::persistence::settings_repo::JsonSettingsRepository`, `zesdex_cms::domain::repository::SettingsRepository` (trait, for method resolution), `zesdex_entities::seaorm::common::store::Store` (for `base_dir` resolution).
- Produces: `AppStateRest.settings: zesdex_cms::domain::settings::Settings` (type changed from the old entities type — field-identical, so every **read-only** call site above needs only an import-path change, not a logic change).
- [ ] **Step 1: Update the type import everywhere it's read-only**
In each of `rest.rs`, `view/mod.rs`, `view/status.rs`, `app/review/mod.rs`, `app/runtime/actions/mod.rs`, replace:
```rust
use crate::model::settings::Settings;
```
with:
```rust
use zesdex_cms::domain::settings::Settings;
```
(For files that reference `Settings` only via `state.settings.<field>` without an explicit `use` for the type itself — confirm per-file with `grep -n "use.*settings::Settings\|model::settings" <file>` before editing — skip files where no explicit import exists, since `AppStateRest.settings`'s type change alone (Step 3 below) is what they actually depend on.)
- [ ] **Step 2: Update `app/mode/settings.rs` (mutates `Settings` in place, no I/O)**
Read the file first: `cat crates/zesdex-backend/src/app/mode/settings.rs`. Replace:
```rust
use crate::model::settings::{InternetMode, Settings};
```
with:
```rust
use zesdex_cms::domain::settings::{InternetMode, Settings};
```
`cycle_internet_mode(settings: &mut Settings)`'s body (mutating `settings.internet_mode` in a cycle) needs no logic change — `InternetMode` is field-identical between old and new.
- [ ] **Step 3: Update `AppStateRest` construction and field type**
In `crates/zesdex-backend/src/app/state/rest.rs`:
Replace the import (line 20):
```rust
use crate::model::settings::Settings;
```
with:
```rust
use zesdex_cms::domain::settings::Settings;
use zesdex_cms::domain::repository::SettingsRepository;
use zesdex_cms::infrastructure::persistence::settings_repo::JsonSettingsRepository;
```
Replace the field's type comment reference (line 49) — no change needed, `pub settings: Settings,` already resolves to the new import.
Replace construction (line 89):
```rust
let settings = Settings::load();
```
with:
```rust
let store_base_dir = zesdex_entities::seaorm::common::store::Store::new().base_dir;
let settings = JsonSettingsRepository::new()
.load(&store_base_dir)
.unwrap_or_default();
```
- [ ] **Step 4: Update `controller/input.rs`'s `.save()` call sites**
Read the file first: `grep -n -B3 "\.settings\.save()" crates/zesdex-backend/src/controller/input.rs`
Both call sites (previously lines 361 and 407) currently do `let _ = state.settings.save();`. Since `Settings` no longer carries an inherent `save()` method, replace each with:
```rust
let _ = zesdex_cms::infrastructure::persistence::settings_repo::JsonSettingsRepository::new()
.save(&state.store_base_dir(), &state.settings);
```
(Uses `state.store_base_dir()` — the existing helper on `AppStateRest`, confirmed to resolve to the same directory as `Store::new().base_dir` in normal operation — since these call sites already have `state: &mut AppStateRest` in scope, unlike `rest.rs::new()` which doesn't yet have a constructed `state` to call `.store_base_dir()` on.)
Add `use zesdex_cms::domain::repository::SettingsRepository;` to this file's imports if not already present after Step 1.
- [ ] **Step 5: Update `main.rs`'s three `.save()` call sites**
Read the file first: `grep -n -B2 "\.settings\.save()" crates/zesdex-backend/src/main.rs`
Replace each `let _ = state.settings.save();` (or `client_state.settings.save()`) with the same pattern as Step 4, substituting the correct state variable name at each site:
```rust
let _ = zesdex_cms::infrastructure::persistence::settings_repo::JsonSettingsRepository::new()
.save(&state.store_base_dir(), &state.settings);
```
- [ ] **Step 6: Update `app/subagent/engine.rs`'s `resolve_provider_config()`**
Read the function first: `grep -n -A 30 "fn resolve_provider_config" crates/zesdex-backend/src/app/subagent/engine.rs`
Replace:
```rust
let settings = crate::model::settings::Settings::load();
```
with:
```rust
let store_base_dir = zesdex_entities::seaorm::common::store::Store::new().base_dir;
let settings = zesdex_cms::infrastructure::persistence::settings_repo::JsonSettingsRepository::new()
.load(&store_base_dir)
.unwrap_or_default();
```
(Add `use zesdex_cms::domain::repository::SettingsRepository;` to this file's imports.) The subsequent field reads (`settings.api_keys`, `settings.provider`, `settings.model` at lines 64-84) need no change — same field names/types.
- [ ] **Step 7: Update `app/workflow/hive_mind.rs`'s `run_hive_mind()`**
Read the function first: `grep -n -A 5 "let settings = crate::model::settings::Settings::load" crates/zesdex-backend/src/app/workflow/hive_mind.rs`
Apply the identical substitution pattern from Step 6. Field reads `settings.hive_mind_node_timeout_ms`/`settings.workflow_max_concurrency` need no change.
- [ ] **Step 8: Update `app/runtime/context/window.rs`**
Read the file first: `cat crates/zesdex-backend/src/app/runtime/context/window.rs`
Replace the import (line 9):
```rust
use crate::model::settings::Settings;
```
with:
```rust
use zesdex_cms::domain::settings::Settings;
```
The `resolve(app_config: &AppConfig, settings: &Settings)` function signature/body and the three test-only `Settings::default()` constructions need no logic change — same type shape, `Default` still works identically after Task 1.
- [ ] **Step 9: Update `bin/seed.rs`**
Read the file first: `cat crates/zesdex-backend/src/bin/seed.rs`
Replace:
```rust
zesdex_entities::seaorm::common::settings::Settings::default()
```
with:
```rust
zesdex_cms::domain::settings::Settings::default()
```
- [ ] **Step 10: Remove the now-unused `model::settings` re-export**
In `crates/zesdex-backend/src/model/mod.rs`, remove:
```rust
pub mod settings {
pub use zesdex_entities::seaorm::common::settings::*;
}
```
- [ ] **Step 11: Verify no remaining references**
Run: `grep -rn "model::settings::" crates/zesdex-backend/src`
Expected: no output.
- [ ] **Step 12: Build and test**
Run: `cargo build --workspace && cargo test --workspace`
Expected: no errors, all tests pass.
- [ ] **Step 13: Manual smoke test**
Run: `cargo run -p zesdex-backend`. Confirm the TUI starts, the Settings overlay (per `view/mod.rs`) displays the current provider/model/flags correctly, and changing internet mode / API key / provider / model persists correctly across a restart (settings.json is written and re-read with the same values).
- [ ] **Step 14: Commit**
```bash
git add -A
git commit -m "refactor(backend): alihkan Settings ke zesdex-cms JsonSettingsRepository"
```
---
### Task 3: Swap `AppConfig` call sites to `JsonAppConfigRepository`
**Files:**
- `crates/zesdex-backend/src/app/state/rest.rs` (lines 18, 50, 90, 104)
- `crates/zesdex-backend/src/bin/seed.rs` (line 27)
- `crates/zesdex-backend/src/app/subagent/engine.rs` (lines 62, 69, 73)
- `crates/zesdex-backend/src/app/runtime/context/window.rs` (lines 8, 19-25, test sites)
- `crates/zesdex-backend/src/controller/input.rs` (lines 220, 252, 383, 385)
- `crates/zesdex-backend/src/view/mod.rs` (lines 710-711)
- `crates/zesdex-backend/src/view/status.rs` (line 68)
- `crates/zesdex-backend/src/app/runtime/actions/mod.rs` (lines 548, 551, 635-659, 1755-1758)
**Interfaces:**
- Consumes: `zesdex_cms::domain::app_config::{AppConfig, ProviderConfig, ModelRole}`, `zesdex_cms::infrastructure::persistence::app_config_repo::JsonAppConfigRepository`, `zesdex_cms::domain::repository::AppConfigRepository`.
- Produces: `AppStateRest.app_config: zesdex_cms::domain::app_config::AppConfig` (type changed, field-identical). `AppConfig` is never saved anywhere in `zesdex-backend` today (confirmed by research) — this task only needs `load`, no `save` call sites.
- [ ] **Step 1: Update read-only imports**
Same pattern as Task 2 Step 1: in each file that imports `crate::model::app_config::AppConfig`/`ProviderConfig`, replace with `zesdex_cms::domain::app_config::{AppConfig, ProviderConfig}` (add `ModelRole` too where `view/mod.rs`/`window.rs` need it).
- [ ] **Step 2: Update `AppStateRest` construction**
In `crates/zesdex-backend/src/app/state/rest.rs`, add to the import block from Task 2 Step 3:
```rust
use zesdex_cms::domain::app_config::AppConfig;
use zesdex_cms::domain::repository::AppConfigRepository;
use zesdex_cms::infrastructure::persistence::app_config_repo::JsonAppConfigRepository;
```
Replace construction (line 90):
```rust
let app_config = AppConfig::load();
```
with:
```rust
let app_config = JsonAppConfigRepository::new()
.load(&store_base_dir)
.unwrap_or_default();
```
(Reuses the `store_base_dir` local variable already introduced in Task 2 Step 3 — both `Settings` and `AppConfig` load from the same base directory.)
- [ ] **Step 3: Update `app/subagent/engine.rs`**
Apply the same substitution as Task 2 Step 6, adding the `AppConfig` load right after the `Settings` load using the same `store_base_dir`:
```rust
let app_config = zesdex_cms::infrastructure::persistence::app_config_repo::JsonAppConfigRepository::new()
.load(&store_base_dir)
.unwrap_or_default();
```
(Add `use zesdex_cms::domain::repository::AppConfigRepository;`.)
- [ ] **Step 4: Update `bin/seed.rs`**
Replace `zesdex_entities::seaorm::common::app_config::AppConfig::default()` with `zesdex_cms::domain::app_config::AppConfig::default()`.
- [ ] **Step 5: Remove the now-unused `model::app_config` re-export**
In `crates/zesdex-backend/src/model/mod.rs`, remove:
```rust
pub mod app_config {
pub use zesdex_entities::seaorm::common::app_config::*;
}
```
- [ ] **Step 6: Verify no remaining references, build, and test**
Run: `grep -rn "model::app_config::" crates/zesdex-backend/src` — expected no output.
Run: `cargo build --workspace && cargo test --workspace` — expected all pass.
- [ ] **Step 7: Manual smoke test**
Run the TUI, open the Model Selector overlay (Ctrl+P or equivalent per `resources.rs`), confirm the provider/model list still populates correctly from `app_config.json`, and that Claude-credential auto-detection (if `~/.claude/settings.json` exists on the test machine) still merges in correctly.
- [ ] **Step 8: Commit**
```bash
git add -A
git commit -m "refactor(backend): alihkan AppConfig ke zesdex-cms JsonAppConfigRepository"
```
---
### Task 4: Swap `Memory` call sites to `MarkdownMemoryRepository`
**Files:**
- `crates/zesdex-backend/src/tool/memory/recall.rs` (lines 4, 44, 63, 69)
- `crates/zesdex-backend/src/tool/memory/remember.rs` (lines 4, 74, 79-96)
- `crates/zesdex-backend/src/tool/memory/forget.rs` (lines 4, 45)
- `crates/zesdex-backend/src/app/mode/learning.rs` (lines 56, 58)
- `crates/zesdex-backend/src/app/workflow/docs.rs` (line 34 — `slugify` only, no I/O)
- `crates/zesdex-backend/src/app/review/mod.rs` (lines 487, 491, 493-494)
- `crates/zesdex-backend/src/app/runtime/actions/mod.rs` (lines 591, 797, 810, 833, 843)
**Interfaces:**
- Consumes: `zesdex_cms::domain::memory::Memory`, `zesdex_cms::infrastructure::persistence::memory_repo::MarkdownMemoryRepository`, `zesdex_cms::domain::repository::MemoryRepository`.
- Produces: nothing new for other tasks — `Memory` is a leaf entity with no state-struct field.
- [ ] **Step 1: `tool/memory/recall.rs`**
Read the file: `cat crates/zesdex-backend/src/tool/memory/recall.rs`
Replace:
```rust
use crate::model::memory::Memory;
```
with:
```rust
use zesdex_cms::domain::memory::Memory;
use zesdex_cms::domain::repository::MemoryRepository;
use zesdex_cms::infrastructure::persistence::memory_repo::MarkdownMemoryRepository;
```
Replace (line 44): `Memory::read(&ctx.memory_dir, name)``MarkdownMemoryRepository::new().load(&ctx.memory_dir, name)`
Replace (line 63): `Memory::list(&ctx.memory_dir)``MarkdownMemoryRepository::new().list(&ctx.memory_dir)`
Replace (line 69): `Memory::read(&ctx.memory_dir, name)``MarkdownMemoryRepository::new().load(&ctx.memory_dir, name)`
(Both old inherent methods and the new repository methods return `Result<Memory>`/`Result<Vec<String>>` respectively — signature shape at the call site is unchanged beyond the receiver.)
- [ ] **Step 2: `tool/memory/remember.rs`**
Read the file: `cat crates/zesdex-backend/src/tool/memory/remember.rs`
Same import swap as Step 1. Replace (line 74): `Memory::slugify(name)``Memory::slugify(name)` (unchanged — `slugify` remains an inherent method on the domain struct in `zesdex-cms`, per research). The struct-literal construction (lines 79-92) needs no change (field-identical). Replace (lines 94-96):
```rust
memory.write(&ctx.memory_dir)?;
```
with:
```rust
MarkdownMemoryRepository::new().save(&ctx.memory_dir, &memory)?;
```
- [ ] **Step 3: `tool/memory/forget.rs`**
Same import swap. Replace (line 45): `Memory::remove(&ctx.memory_dir, name)``MarkdownMemoryRepository::new().delete(&ctx.memory_dir, name)`.
- [ ] **Step 4: `app/mode/learning.rs`**
Read the file: `cat crates/zesdex-backend/src/app/mode/learning.rs`
Replace (line 56): `crate::model::memory::Memory::list(&state.memory_dir)``zesdex_cms::infrastructure::persistence::memory_repo::MarkdownMemoryRepository::new().list(&state.memory_dir)`
Replace (line 58): `crate::model::memory::Memory::read(&state.memory_dir, &name)``zesdex_cms::infrastructure::persistence::memory_repo::MarkdownMemoryRepository::new().load(&state.memory_dir, &name)`
(Add `use zesdex_cms::domain::repository::MemoryRepository;` at the top of the file.)
- [ ] **Step 5: `app/workflow/docs.rs`**
Replace (line 34): `crate::model::memory::Memory::slugify(user_request)``zesdex_cms::domain::memory::Memory::slugify(user_request)`. (Pure filename-generation helper, no repository/I/O involved — no other change needed.)
- [ ] **Step 6: `app/review/mod.rs`**
Read the surrounding code: `grep -n -B2 -A8 "model::memory::Memory::list(memory_dir)" crates/zesdex-backend/src/app/review/mod.rs`
Replace (line 487): `crate::model::memory::Memory::list(memory_dir)``zesdex_cms::infrastructure::persistence::memory_repo::MarkdownMemoryRepository::new().list(memory_dir)`
Replace (line 491): `crate::model::memory::Memory::read(memory_dir, &name)``zesdex_cms::infrastructure::persistence::memory_repo::MarkdownMemoryRepository::new().load(memory_dir, &name)`
Replace (lines 493-494):
```rust
mem.lifecycle = "stale".to_string();
mem.write(memory_dir)?;
```
with:
```rust
mem.lifecycle = "stale".to_string();
zesdex_cms::infrastructure::persistence::memory_repo::MarkdownMemoryRepository::new().save(memory_dir, &mem)?;
```
- [ ] **Step 7: `app/runtime/actions/mod.rs`**
Read each site first: `grep -n -B2 -A2 "model::memory::Memory::" crates/zesdex-backend/src/app/runtime/actions/mod.rs`
Apply the same `Memory::method(dir, ...)``MarkdownMemoryRepository::new().method(dir, ...)` substitution at all 5 remaining sites (lines 591, 797, 810, 833, 843), matching the method-name mapping: `remove``delete`, `list``list`, `read``load`.
- [ ] **Step 8: Add `use` statements**
Add `use zesdex_cms::domain::repository::MemoryRepository;` and `use zesdex_cms::infrastructure::persistence::memory_repo::MarkdownMemoryRepository;` to `app/review/mod.rs` and `app/runtime/actions/mod.rs` (both already import many things — add alongside existing `use` block).
- [ ] **Step 9: Remove the now-unused `model::memory` re-export**
In `crates/zesdex-backend/src/model/mod.rs`, remove:
```rust
pub mod memory {
pub use zesdex_entities::seaorm::common::memory::*;
}
```
- [ ] **Step 10: Verify no remaining references, build, test**
Run: `grep -rn "model::memory::" crates/zesdex-backend/src` — expected no output.
Run: `cargo build --workspace && cargo test --workspace` — expected all pass.
- [ ] **Step 11: Manual smoke test**
In the TUI, invoke the `remember` tool to create a memory, `recall` it back, confirm the Learning overlay lists it, then `forget` it and confirm it's gone. Also confirm existing `.md` memory files from before this change (if any test fixtures exist) still load correctly (format compatibility check).
- [ ] **Step 12: Commit**
```bash
git add -A
git commit -m "refactor(backend): alihkan Memory ke zesdex-cms MarkdownMemoryRepository"
```
---
### Task 5: Swap `EditLog` call sites to `JsonlEditLogRepository`
**Context:** This is the most invasive of the four because every call site currently does `EditLog::new(dir)` (infallible, eager full-file read) and the new equivalent `JsonlEditLogRepository::open(dir)` returns `Result`. There are 7 distinct call sites plus one persistent field on `AppStateRest`.
**Files:**
- `crates/zesdex-backend/src/app/state/rest.rs` (lines 19, 58, 115)
- `crates/zesdex-backend/src/main.rs` (line 236 — reads `state.edit_log.len()`, no change needed beyond the type)
- `crates/zesdex-backend/src/app/subagent/engine.rs` (lines 571-580)
- `crates/zesdex-backend/src/app/mode/rewind.rs` (lines 103-114, 128-134)
- `crates/zesdex-backend/src/app/runtime/actions/mod.rs` (lines 921, 1473-1475, 1487-1489, 1586-1597)
- `crates/zesdex-backend/src/model/mod.rs` (remove re-export)
**Interfaces:**
- Consumes: `zesdex_cms::domain::edit_log::{EditLog, EditLogEntry}`, `zesdex_cms::infrastructure::persistence::edit_log_repo::JsonlEditLogRepository`, `zesdex_cms::domain::repository::EditLogRepository`.
- Produces: `AppStateRest.edit_log: zesdex_cms::domain::edit_log::EditLog` (type changed — no `path` field this time, so anything reading `state.edit_log.path` would break; confirmed by research that no call site does this).
- [ ] **Step 1: Update `AppStateRest`**
In `crates/zesdex-backend/src/app/state/rest.rs`, replace the import (line 19):
```rust
use crate::model::editlog::EditLog;
```
with:
```rust
use zesdex_cms::domain::edit_log::EditLog;
use zesdex_cms::domain::repository::EditLogRepository;
use zesdex_cms::infrastructure::persistence::edit_log_repo::JsonlEditLogRepository;
```
Replace construction (line 115):
```rust
edit_log: EditLog::new(session_dir),
```
with:
```rust
edit_log: JsonlEditLogRepository::new().open(session_dir).unwrap_or_else(|e| {
tracing::warn!("[state] failed to open edit log at '{}': {e}", session_dir.display());
EditLog::new()
}),
```
- [ ] **Step 2: `app/subagent/engine.rs`**
Read the site: `grep -n -B3 -A3 "EditLog::new(&ctx.session_dir)" crates/zesdex-backend/src/app/subagent/engine.rs`
Replace:
```rust
let mut el = crate::model::editlog::EditLog::new(&ctx.session_dir);
el.append(entry).ok();
```
with:
```rust
let repo = zesdex_cms::infrastructure::persistence::edit_log_repo::JsonlEditLogRepository::new();
if let Ok(mut el) = repo.open(&ctx.session_dir) {
let _ = repo.append(&ctx.session_dir, &mut el, entry);
}
```
- [ ] **Step 3: `app/mode/rewind.rs` — logging the rewind operation**
Read the site: `grep -n -B3 -A10 "EditLog::new(&state.session_dir)" crates/zesdex-backend/src/app/mode/rewind.rs`
Replace the first site (previously lines 103-114):
```rust
let mut el = crate::model::editlog::EditLog::new(&state.session_dir);
let entry = crate::model::editlog::EditLogEntry { /* ... */ };
let _ = el.append(entry);
```
with:
```rust
let repo = zesdex_cms::infrastructure::persistence::edit_log_repo::JsonlEditLogRepository::new();
if let Ok(mut el) = repo.open(&state.session_dir) {
let entry = zesdex_cms::domain::edit_log::EditLogEntry { /* same field values as before */ };
let _ = repo.append(&state.session_dir, &mut el, entry);
}
```
(Keep the exact same `EditLogEntry` field values from the original code — only the type path and the append mechanism change.)
- [ ] **Step 4: `app/mode/rewind.rs` — `find_edit_path`**
Read the site: `grep -n -B3 -A8 "fn find_edit_path" crates/zesdex-backend/src/app/mode/rewind.rs`
Replace:
```rust
let el = crate::model::editlog::EditLog::new(&state.session_dir);
```
with:
```rust
let el = zesdex_cms::infrastructure::persistence::edit_log_repo::JsonlEditLogRepository::new()
.open(&state.session_dir)
.unwrap_or_else(|_| zesdex_cms::domain::edit_log::EditLog::new());
```
The subsequent `el.entries.iter().rev().find(...)` (unchanged — `entries` is a public field on both old and new `EditLog`) needs no further change.
- [ ] **Step 5: `app/runtime/actions/mod.rs` — turn-start snapshot**
Read the site: `grep -n -B2 -A2 "let initial_edits" crates/zesdex-backend/src/app/runtime/actions/mod.rs`
Replace:
```rust
let initial_edits = crate::model::editlog::EditLog::new(&tc.edit_log_session_dir).len();
```
with:
```rust
let initial_edits = zesdex_cms::infrastructure::persistence::edit_log_repo::JsonlEditLogRepository::new()
.open(&tc.edit_log_session_dir)
.map(|el| el.entries.len())
.unwrap_or(0);
```
(`EditLog` in `zesdex-cms` has no `.len()` inherent method — check first: `grep -n "fn len\|impl EditLog" crates/zesdex-cms/src/domain/edit_log.rs`. If `.len()` doesn't exist, use `.entries.len()` as shown; if it does exist, use `el.len()` instead for consistency with the rest of the codebase's naming.)
- [ ] **Step 6: `app/runtime/actions/mod.rs` — turn-end diff**
Read the site: `grep -n -B2 -A10 "let final_edits" crates/zesdex-backend/src/app/runtime/actions/mod.rs`
Replace:
```rust
let el = crate::model::editlog::EditLog::new(&tc.edit_log_session_dir);
let final_edits = el.len();
let total_edits_this_turn = final_edits.saturating_sub(initial_edits);
```
with:
```rust
let el = zesdex_cms::infrastructure::persistence::edit_log_repo::JsonlEditLogRepository::new()
.open(&tc.edit_log_session_dir)
.unwrap_or_else(|_| zesdex_cms::domain::edit_log::EditLog::new());
let final_edits = el.entries.len();
let total_edits_this_turn = final_edits.saturating_sub(initial_edits);
```
And immediately after (previously lines 1487-1489), the `.entries.iter().skip(initial_edits)` loop needs no change — same field access.
- [ ] **Step 7: `app/runtime/actions/mod.rs` — `execute_one_tool`'s primary write path**
Read the site: `grep -n -B3 -A5 "EditLog::new(session_dir)" crates/zesdex-backend/src/app/runtime/actions/mod.rs`
Apply the same pattern as Step 2 (open + conditional append via the repository).
- [ ] **Step 8: Remove the now-unused `model::editlog` re-export**
In `crates/zesdex-backend/src/model/mod.rs`, remove:
```rust
pub mod editlog {
pub use zesdex_entities::seaorm::common::edit_log::*;
}
```
- [ ] **Step 9: Verify no remaining references**
Run: `grep -rn "model::editlog::" crates/zesdex-backend/src`
Expected: no output.
- [ ] **Step 10: Build and test**
Run: `cargo build --workspace && cargo test --workspace`
Expected: no errors, all tests pass.
- [ ] **Step 11: Manual smoke test**
In the TUI, make a file edit via the `edit`/`write` tool, confirm `state.edit_log` grows and `main.rs`'s IPC `edit_count` payload reflects it, then use the rewind overlay to confirm `find_edit_path` still correctly recovers the edited file's path and the rewind itself works end-to-end.
- [ ] **Step 12: Commit**
```bash
git add -A
git commit -m "refactor(backend): alihkan EditLog ke zesdex-cms JsonlEditLogRepository"
```
---
### Task 6: Delete the now-dead `zesdex_entities::seaorm::common::{settings,app_config,memory,edit_log}` modules
**Files:**
- Delete: `crates/zesdex-entities/src/seaorm/common/settings.rs`, `app_config.rs`, `memory.rs`, `edit_log.rs`
- Modify: `crates/zesdex-entities/src/seaorm/common/mod.rs` (remove their module declarations)
**Interfaces:** none — pure deletion after Tasks 2-5 have removed every reference.
- [ ] **Step 1: Verify zero remaining references across the whole workspace**
Run: `grep -rln "seaorm::common::settings\|seaorm::common::app_config\|seaorm::common::memory\b\|seaorm::common::edit_log" crates --include='*.rs'`
Expected: no output. (If anything other than the `mod.rs` declaration itself shows up, stop and investigate before deleting — it means a call site was missed in Tasks 2-5.)
- [ ] **Step 2: Delete the files**
```bash
git rm crates/zesdex-entities/src/seaorm/common/settings.rs
git rm crates/zesdex-entities/src/seaorm/common/app_config.rs
git rm crates/zesdex-entities/src/seaorm/common/memory.rs
git rm crates/zesdex-entities/src/seaorm/common/edit_log.rs
```
- [ ] **Step 3: Remove their module declarations**
In `crates/zesdex-entities/src/seaorm/common/mod.rs`, remove the corresponding `pub mod settings;`, `pub mod app_config;`, `pub mod memory;`, `pub mod edit_log;` lines (check first: `cat crates/zesdex-entities/src/seaorm/common/mod.rs`).
- [ ] **Step 4: Build the whole workspace**
Run: `cargo build --workspace`
Expected: no errors.
- [ ] **Step 5: Run the full test suite and clippy**
Run: `cargo test --workspace && cargo clippy --workspace --all-targets -- -D warnings`
Expected: all pass, no new warnings.
- [ ] **Step 6: Commit**
```bash
git add -A
git commit -m "chore: hapus entitas settings/app_config/memory/edit_log lama di zesdex-entities yang sudah digantikan zesdex-cms"
```