chore: add rule 58 — hierarchical state tree architecture (mandatory)
This commit is contained in:
parent
aaeee808c3
commit
ae4c07c160
1 changed files with 104 additions and 0 deletions
104
.claude/rules/58-state-tree.md
Normal file
104
.claude/rules/58-state-tree.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# State Tree Architecture (MANDATORY)
|
||||
|
||||
All application state lives in a single hierarchical state tree. Components are pure renderers — they read from the tree via getter functions and dispatch actions. No component-local `$state` except DOM refs (`bind:this`).
|
||||
|
||||
## Tree Structure
|
||||
|
||||
```
|
||||
AppState (root) → app-state.svelte.ts
|
||||
├── theme, locale, appReady
|
||||
├── ui: settings, palette, search, wizard, notifications
|
||||
├── workspace
|
||||
│ ├── groups[], activeGroupId
|
||||
│ └── projects[] (per project ↓)
|
||||
│ ├── agent: session, messages, status, cost
|
||||
│ ├── terminals: tabs[], collapsed, activeTabId
|
||||
│ ├── files: tree, selectedPath, content, dirty
|
||||
│ ├── comms: channels[], messages[], mode
|
||||
│ ├── tasks: items[], dragTarget
|
||||
│ └── tab: activeTab, activatedTabs
|
||||
└── health: per-project trackers, attention queue
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
### 1. State ownership follows the tree
|
||||
|
||||
Each node owns its children. A node NEVER reads siblings or ancestors directly — it receives what it needs via getter functions scoped to its level.
|
||||
|
||||
```typescript
|
||||
// RIGHT — scoped access through parent
|
||||
function getProjectAgent(projectId: string) {
|
||||
return getProjectState(projectId).agent;
|
||||
}
|
||||
|
||||
// WRONG — cross-branch reach
|
||||
function getAgentForComms() {
|
||||
return agentStore.getSession(commsStore.activeAgentId);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Actions bubble up, state flows down
|
||||
|
||||
Components dispatch actions to the store that owns the state. State changes propagate downward via Svelte's reactive reads.
|
||||
|
||||
```typescript
|
||||
// Component dispatches action
|
||||
onclick={() => projectActions.setActiveTab(projectId, 'files')}
|
||||
|
||||
// Store mutates owned state
|
||||
function setActiveTab(projectId: string, tab: ProjectTab) {
|
||||
getProjectState(projectId).tab.activeTab = tab;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. One store file per tree level
|
||||
|
||||
| Level | File | Owns |
|
||||
|-------|------|------|
|
||||
| Root | `app-state.svelte.ts` | theme, locale, appReady |
|
||||
| UI | `ui-store.svelte.ts` | drawers, overlays, modals |
|
||||
| Workspace | `workspace-store.svelte.ts` | groups, projects list |
|
||||
| Project | `project-state.svelte.ts` | per-project sub-states |
|
||||
| Health | `health-store.svelte.ts` | trackers, attention |
|
||||
|
||||
### 4. Per-project state is a typed object, not scattered stores
|
||||
|
||||
```typescript
|
||||
interface ProjectState {
|
||||
agent: AgentState;
|
||||
terminals: TerminalState;
|
||||
files: FileState;
|
||||
comms: CommsState;
|
||||
tasks: TaskState;
|
||||
tab: TabState;
|
||||
}
|
||||
|
||||
// Accessed via:
|
||||
function getProjectState(id: string): ProjectState
|
||||
```
|
||||
|
||||
### 5. No `$state` in components
|
||||
|
||||
Components use ONLY:
|
||||
- `$props()` for parent-passed values
|
||||
- Getter functions from stores (e.g., `getProjectAgent(id)`)
|
||||
- Action functions from stores (e.g., `projectActions.sendMessage(id, text)`)
|
||||
- `bind:this` for DOM element references (the sole exception)
|
||||
- Ephemeral interaction state (hover, drag coordinates) via plain `let` — NOT `$state`
|
||||
|
||||
### 6. No `$derived` with new objects (Rule 57)
|
||||
|
||||
All computed values are plain functions. See Rule 57 for details.
|
||||
|
||||
### 7. Initialization in `onMount`, never `$effect`
|
||||
|
||||
All async data loading, timer setup, and event listener registration goes in `onMount`. See Rule 57.
|
||||
|
||||
## Adding New State
|
||||
|
||||
1. Identify which tree node owns it
|
||||
2. Add the field to that node's interface
|
||||
3. Add getter + action functions in the store file
|
||||
4. Components read via getter, mutate via action
|
||||
5. NEVER add `$state` to a `.svelte` component file
|
||||
Loading…
Add table
Add a link
Reference in a new issue