compound-workflow 1.7.3 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -69
- package/package.json +2 -8
- package/scripts/check-pack-readme.mjs +1 -16
- package/scripts/check-workflow-contracts.mjs +34 -44
- package/scripts/install-cli.mjs +273 -417
- package/src/AGENTS.md +59 -192
- package/src/{.agents/agents → agents}/research/best-practices-researcher.md +2 -2
- package/src/{.agents/commands → commands}/assess.md +4 -4
- package/src/commands/install.md +43 -0
- package/src/{.agents/commands → commands}/metrics.md +1 -1
- package/src/commands/workflow-agents.md +101 -0
- package/src/{.agents/commands → commands}/workflow-compound.md +1 -1
- package/src/{.agents/commands → commands}/workflow-plan.md +24 -33
- package/src/commands/workflow-work.md +836 -0
- package/src/{.agents/references → references}/README.md +1 -1
- package/src/{.agents/skills → skills}/capture-skill/SKILL.md +1 -1
- package/src/{.agents/skills → skills}/compound-docs/SKILL.md +6 -6
- package/src/{.agents/skills → skills}/compound-docs/references/yaml-schema.md +2 -2
- package/src/skills/setup-agents/SKILL.md +250 -0
- package/src/skills/standards/SKILL.md +79 -0
- package/src/skills/standards/references/architecture.md +228 -0
- package/src/skills/standards/references/code-quality.md +192 -0
- package/src/skills/standards/references/presentation.md +515 -0
- package/src/skills/standards/references/services.md +172 -0
- package/src/skills/standards/references/state-management.md +936 -0
- package/.claude-plugin/plugin.json +0 -7
- package/.cursor-plugin/plugin.json +0 -20
- package/.cursor-plugin/registration.json +0 -5
- package/scripts/check-version-parity.mjs +0 -36
- package/scripts/generate-platform-artifacts.mjs +0 -230
- package/src/.agents/commands/install.md +0 -51
- package/src/.agents/commands/workflow-work.md +0 -690
- package/src/.agents/registry.json +0 -48
- package/src/.agents/scripts/self-check.mjs +0 -227
- package/src/.agents/scripts/sync-opencode.mjs +0 -362
- package/src/.agents/skills/presentation-composability/SKILL.md +0 -72
- package/src/.agents/skills/react-ddd-mvc-frontend/SKILL.md +0 -51
- package/src/.agents/skills/react-ddd-mvc-frontend/references/feature-structure.md +0 -25
- package/src/.agents/skills/react-ddd-mvc-frontend/references/implementation-principles.md +0 -21
- package/src/.agents/skills/react-ddd-mvc-frontend/references/responsibility-gates.md +0 -41
- package/src/.agents/skills/react-ddd-mvc-frontend/references/source-map.md +0 -11
- package/src/.agents/skills/standards/SKILL.md +0 -747
- package/src/.agents/skills/xstate-actor-orchestration/SKILL.md +0 -197
- package/src/.agents/skills/xstate-actor-orchestration/agents/openai.yaml +0 -4
- package/src/.agents/skills/xstate-actor-orchestration/assets/statecharts/.gitkeep +0 -0
- package/src/.agents/skills/xstate-actor-orchestration/references/actor-system-patterns.md +0 -71
- package/src/.agents/skills/xstate-actor-orchestration/references/event-contracts.md +0 -73
- package/src/.agents/skills/xstate-actor-orchestration/references/functional-domain-patterns.md +0 -53
- package/src/.agents/skills/xstate-actor-orchestration/references/machine-structure-and-tags.md +0 -36
- package/src/.agents/skills/xstate-actor-orchestration/references/react-container-pattern.md +0 -45
- package/src/.agents/skills/xstate-actor-orchestration/references/reliability-observability.md +0 -39
- package/src/.agents/skills/xstate-actor-orchestration/references/skill-validation.md +0 -33
- package/src/.agents/skills/xstate-actor-orchestration/references/source-map.md +0 -44
- package/src/.agents/skills/xstate-actor-orchestration/references/statechart-review-and-signoff.md +0 -59
- package/src/.agents/skills/xstate-actor-orchestration/references/testing-strategy.md +0 -35
- package/src/.agents/skills/xstate-actor-orchestration/scripts/create-statechart-artifact.sh +0 -71
- package/src/.agents/skills/xstate-actor-orchestration/scripts/validate-skill.sh +0 -138
- package/src/generated/opencode.managed.json +0 -115
- /package/src/{.agents/agents → agents}/research/framework-docs-researcher.md +0 -0
- /package/src/{.agents/agents → agents}/research/git-history-analyzer.md +0 -0
- /package/src/{.agents/agents → agents}/research/learnings-researcher.md +0 -0
- /package/src/{.agents/agents → agents}/research/repo-research-analyst.md +0 -0
- /package/src/{.agents/agents → agents}/review/agent-native-reviewer.md +0 -0
- /package/src/{.agents/agents → agents}/review/planning-technical-reviewer.md +0 -0
- /package/src/{.agents/agents → agents}/workflow/bug-reproduction-validator.md +0 -0
- /package/src/{.agents/agents → agents}/workflow/lint.md +0 -0
- /package/src/{.agents/agents → agents}/workflow/spec-flow-analyzer.md +0 -0
- /package/src/{.agents/commands → commands}/test-browser.md +0 -0
- /package/src/{.agents/commands → commands}/workflow-brainstorm.md +0 -0
- /package/src/{.agents/commands → commands}/workflow-review.md +0 -0
- /package/src/{.agents/commands → commands}/workflow-tech-review.md +0 -0
- /package/src/{.agents/commands → commands}/workflow-triage.md +0 -0
- /package/src/{.agents/references → references}/standards/README.md +0 -0
- /package/src/{.agents/skills → skills}/agent-browser/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/audit-traceability/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/brainstorming/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/compound-docs/assets/critical-pattern-template.md +0 -0
- /package/src/{.agents/skills → skills}/compound-docs/assets/resolution-template.md +0 -0
- /package/src/{.agents/skills → skills}/compound-docs/schema.project.yaml +0 -0
- /package/src/{.agents/skills → skills}/compound-docs/schema.yaml +0 -0
- /package/src/{.agents/skills → skills}/data-foundations/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/document-review/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/file-todos/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/file-todos/assets/todo-template.md +0 -0
- /package/src/{.agents/skills → skills}/financial-workflow-integrity/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/git-worktree/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/pii-protection-prisma/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/process-metrics/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/process-metrics/assets/daily-template.md +0 -0
- /package/src/{.agents/skills → skills}/process-metrics/assets/monthly-template.md +0 -0
- /package/src/{.agents/skills → skills}/process-metrics/assets/weekly-template.md +0 -0
- /package/src/{.agents/skills → skills}/technical-review/SKILL.md +0 -0
|
@@ -1,747 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: standards
|
|
3
|
-
description: General coding practices, implementation styles, and patterns for the Altai application. Covers domain entities, XState patterns, type usage, and code organization. Use when implementing features, writing new code, or refactoring existing code in the Altai codebase.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Altai Code Standards
|
|
7
|
-
|
|
8
|
-
## Mandatory Baseline (Declarative, Immutable, Maintainable)
|
|
9
|
-
|
|
10
|
-
These rules are mandatory for code/config implementation work. They are pass/fail gates, not advisory guidance.
|
|
11
|
-
|
|
12
|
-
**Declarative over imperative:** Prefer declarative solutions by requirement. Describe *what* should hold (state, transitions, invariants) and let the runtime or framework determine *how*; avoid imperative step-by-step control that mutates shared state in place.
|
|
13
|
-
|
|
14
|
-
### MUST
|
|
15
|
-
|
|
16
|
-
- **MUST prefer declarative over imperative:** Orchestration and data flow MUST be expressed declaratively (state/events/transitions, pure transforms) rather than imperative sequences (mutating variables, step-by-step handlers). When both are feasible, choose the declarative option.
|
|
17
|
-
- **MUST keep orchestration declarative** in containers/controllers: describe state transitions and event flow explicitly instead of imperative step-by-step control logic.
|
|
18
|
-
- **MUST use immutable transforms** for domain and controller data operations (`input -> output`), returning new values instead of mutating existing objects/arrays.
|
|
19
|
-
- **MUST keep domain logic in pure entity functions** (no side effects, no hidden mutable module state, no IO in entity transforms/predicates).
|
|
20
|
-
- **MUST keep maintainability boundaries clear**:
|
|
21
|
-
- containers wire/select/send and avoid business rules
|
|
22
|
-
- controllers manage state transitions and delegate reusable logic to entities
|
|
23
|
-
- presentation components remain UI-focused
|
|
24
|
-
- **MUST keep branching complexity controlled** with early exits and extracted helpers when conditional logic grows.
|
|
25
|
-
|
|
26
|
-
### MUST NOT
|
|
27
|
-
|
|
28
|
-
- **MUST NOT choose imperative over declarative** when a declarative solution is feasible (e.g. state machine + events over manual flags and step counters; pure transforms over in-place mutation).
|
|
29
|
-
- **MUST NOT implement mutation-heavy imperative handlers** that modify shared state in-place across multiple steps.
|
|
30
|
-
- **MUST NOT use hidden mutable accumulators** (`let` variables mutated through control flow) when a pure transform is feasible.
|
|
31
|
-
- **MUST NOT mix business decision logic into containers/presentation** when it belongs in domain entities/controllers.
|
|
32
|
-
- **MUST NOT allow branching sprawl** in controllers/containers (deep nesting, chained `else-if`, or nested ternaries for workflow logic).
|
|
33
|
-
- **MUST NOT complete code/config work without standards evidence** recorded in work/review outputs.
|
|
34
|
-
|
|
35
|
-
### Required Evidence Format (Work and Review)
|
|
36
|
-
|
|
37
|
-
Use this format when reporting standards compliance in execution or review evidence:
|
|
38
|
-
|
|
39
|
-
```markdown
|
|
40
|
-
standards_compliance:
|
|
41
|
-
- declarative_over_imperative: pass|fail (evidence: file:line) # Declarative solutions required; no imperative choice when declarative feasible
|
|
42
|
-
- declarative_flow: pass|fail (evidence: file:line)
|
|
43
|
-
- immutable_transforms: pass|fail (evidence: file:line)
|
|
44
|
-
- maintainability_boundaries: pass|fail (evidence: file:line)
|
|
45
|
-
- hidden_mutable_state: pass|fail (evidence: file:line)
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
If any mandatory line is `fail`, code/config work is not complete.
|
|
49
|
-
|
|
50
|
-
## Core Principles
|
|
51
|
-
|
|
52
|
-
1. **Simplicity over cleverness** - Prefer straightforward solutions
|
|
53
|
-
2. **Maintainability over flexibility** - Avoid premature abstraction
|
|
54
|
-
3. **YAGNI** - Add complexity when needed, not before
|
|
55
|
-
4. **Domain entities for logic** - Use pure functions in entities for transforms and predicates
|
|
56
|
-
5. **Keep controllers simple** - Delegate complexity to domain entities
|
|
57
|
-
6. **Early exits over else/else-if** - Return early for special cases, avoid nested conditionals
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Domain Entities
|
|
62
|
-
|
|
63
|
-
Location: `src/features/{feature}/domain/entities/`
|
|
64
|
-
|
|
65
|
-
Domain entities contain **types** and **pure functions** that encapsulate business logic. Use them for transforms, predicates, and any complex logic - keeping controllers/machines simple.
|
|
66
|
-
|
|
67
|
-
### Structure
|
|
68
|
-
|
|
69
|
-
- Pure functions, no classes
|
|
70
|
-
- One file per concern (e.g., `PlaylistSelectionEntity.ts` vs `PlaylistReorderEntity.ts`)
|
|
71
|
-
- Private helpers stay internal (no `export`)
|
|
72
|
-
|
|
73
|
-
### Function Signatures
|
|
74
|
-
|
|
75
|
-
**Use named params for 2+ arguments:**
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
// Good - clear at call site
|
|
79
|
-
export const hasAddedPlaylists = ({
|
|
80
|
-
current,
|
|
81
|
-
incoming,
|
|
82
|
-
}: PlaylistCompareParams): boolean => {
|
|
83
|
-
// ...
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
// Usage
|
|
87
|
-
hasAddedPlaylists({ current: ctx.playlists, incoming: evt.payload.playlists });
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**Benefits**: Self-documenting, easier to refactor, better IDE support.
|
|
91
|
-
|
|
92
|
-
### Naming Conventions
|
|
93
|
-
|
|
94
|
-
| Prefix | Purpose | Example |
|
|
95
|
-
| ------ | --------------------- | ------------------------------------------ |
|
|
96
|
-
| `hasX` | Boolean predicate | `hasAddedPlaylists`, `hasRemovedPlaylists` |
|
|
97
|
-
| `getX` | Retrieve/extract data | `getAddedPlaylists`, `getRemovedPlaylists` |
|
|
98
|
-
| `toX` | Transform data | `toPlaylistIds`, `toSortedPlaylistIds` |
|
|
99
|
-
|
|
100
|
-
### Early Exit Pattern
|
|
101
|
-
|
|
102
|
-
**Always prefer early exits over else/else-if** - this is a code standard, not a suggestion.
|
|
103
|
-
|
|
104
|
-
```typescript
|
|
105
|
-
// ❌ Bad - nested conditionals with else-if
|
|
106
|
-
export const createAction = (params) => {
|
|
107
|
-
if (params.pendingFolder) {
|
|
108
|
-
return {
|
|
109
|
-
metadata: {
|
|
110
|
-
createFolder: params.pendingFolder,
|
|
111
|
-
mode: "create",
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
} else if (params.existingFolder) {
|
|
115
|
-
return {
|
|
116
|
-
metadata: {
|
|
117
|
-
targetFolder: params.existingFolder,
|
|
118
|
-
mode: "existing",
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
} else {
|
|
122
|
-
throw new Error("Invalid params");
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// ✅ Good - early exits, flat structure
|
|
127
|
-
export const createAction = (params) => {
|
|
128
|
-
// Handle special case first
|
|
129
|
-
if (params.pendingFolder) {
|
|
130
|
-
return {
|
|
131
|
-
metadata: {
|
|
132
|
-
createFolder: params.pendingFolder,
|
|
133
|
-
mode: "create",
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Handle next case
|
|
139
|
-
if (params.existingFolder) {
|
|
140
|
-
return {
|
|
141
|
-
metadata: {
|
|
142
|
-
targetFolder: params.existingFolder,
|
|
143
|
-
mode: "existing",
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Invalid state
|
|
149
|
-
throw new Error("Invalid params");
|
|
150
|
-
};
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
**Benefits**:
|
|
154
|
-
|
|
155
|
-
- **Flat code structure** - No nesting, easier to read
|
|
156
|
-
- **Clear intent** - Each case is independent and complete
|
|
157
|
-
- **Easier to modify** - Add/remove cases without touching others
|
|
158
|
-
- **No else-if** - Else-if is a code smell indicating missed early exit opportunities
|
|
159
|
-
|
|
160
|
-
**When extracting action creation to entities**:
|
|
161
|
-
|
|
162
|
-
```typescript
|
|
163
|
-
// ✅ Good - early exits for different contexts
|
|
164
|
-
export const createSelectFolderRootAction = ({
|
|
165
|
-
folder,
|
|
166
|
-
activeTabId,
|
|
167
|
-
activeFolderId,
|
|
168
|
-
pendingFolderName,
|
|
169
|
-
}: Params): PanelActionEntity => {
|
|
170
|
-
// Early exit - pending folder
|
|
171
|
-
if (pendingFolderName && activeTabId) {
|
|
172
|
-
return panelActionEntity.toActionItem(ActionTypes.SHARE, {
|
|
173
|
-
label: pendingFolderName,
|
|
174
|
-
metadata: {
|
|
175
|
-
targetTabId: activeTabId,
|
|
176
|
-
createFolderName: pendingFolderName,
|
|
177
|
-
mode: "folderRoot",
|
|
178
|
-
},
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Early exit - existing folder in share context
|
|
183
|
-
if (activeTabId && activeFolderId) {
|
|
184
|
-
return panelActionEntity.toActionItem(ActionTypes.SHARE, {
|
|
185
|
-
label: folder?.label ?? "Folder",
|
|
186
|
-
metadata: {
|
|
187
|
-
targetTabId: activeTabId,
|
|
188
|
-
targetFolderId: activeFolderId,
|
|
189
|
-
mode: "folderRoot",
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Early exit - move to folder context
|
|
195
|
-
if (activeFolderId) {
|
|
196
|
-
return panelActionEntity.toActionItem(ActionTypes.MOVE, {
|
|
197
|
-
label: folder?.label ?? "Folder",
|
|
198
|
-
metadata: {
|
|
199
|
-
targetFolderId: activeFolderId,
|
|
200
|
-
mode: "folderRoot",
|
|
201
|
-
},
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
throw new Error("Invalid parameters");
|
|
206
|
-
};
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
**Anti-patterns to avoid**:
|
|
210
|
-
|
|
211
|
-
- `else` and `else-if` blocks
|
|
212
|
-
- Conditional spreading: `...(condition && { prop: value })`
|
|
213
|
-
- Let variables modified in conditionals
|
|
214
|
-
- Nested ternaries for complex logic
|
|
215
|
-
|
|
216
|
-
### When to Use
|
|
217
|
-
|
|
218
|
-
- **Predicates**: `hasAddedPlaylists`, `isValidState`
|
|
219
|
-
- **Transforms**: `toPlaylistIds`, `toSortedItems`
|
|
220
|
-
- **Comparisons**: Diffing arrays, checking membership
|
|
221
|
-
- **Complex conditionals**: Extract to helper functions
|
|
222
|
-
|
|
223
|
-
```typescript
|
|
224
|
-
// Extract complex logic to helpers
|
|
225
|
-
const getFolderLabel = (
|
|
226
|
-
folder: InputOption | ClipsPanelFolderEntity
|
|
227
|
-
): string => {
|
|
228
|
-
return "label" in folder ? folder.label : (folder.folderName ?? "Folder");
|
|
229
|
-
};
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
This keeps controllers/machines simple - they delegate logic to entities.
|
|
233
|
-
|
|
234
|
-
---
|
|
235
|
-
|
|
236
|
-
## XState Patterns
|
|
237
|
-
|
|
238
|
-
### Guards (Conditions)
|
|
239
|
-
|
|
240
|
-
Extract complex guards to domain entity predicates. Inline the entity call in `cond`:
|
|
241
|
-
|
|
242
|
-
```typescript
|
|
243
|
-
import * as playlistSelectionEntity from "src/features/tapesv3/domain/entities/PlaylistSelectionEntity";
|
|
244
|
-
|
|
245
|
-
[EventType.PLAYLIST_CHANGED]: [
|
|
246
|
-
{
|
|
247
|
-
cond: (ctx, evt) =>
|
|
248
|
-
playlistSelectionEntity.hasAddedPlaylists({
|
|
249
|
-
current: ctx.playlists,
|
|
250
|
-
incoming: evt.payload.playlists,
|
|
251
|
-
}),
|
|
252
|
-
actions: [/* ... */],
|
|
253
|
-
},
|
|
254
|
-
],
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
Avoid unnecessary wrapper functions - inline entity calls directly.
|
|
258
|
-
|
|
259
|
-
### State vs Context: Model UI Modes as State
|
|
260
|
-
|
|
261
|
-
**Use state machines to model UI modes** - don't store mode flags in context:
|
|
262
|
-
|
|
263
|
-
```typescript
|
|
264
|
-
// Bad - storing mode in context
|
|
265
|
-
type Context = {
|
|
266
|
-
viewMode: "folder" | "search"; // Don't store what state can represent
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
// Good - model modes as parallel states
|
|
270
|
-
viewModeManagement: {
|
|
271
|
-
initial: "folderView",
|
|
272
|
-
states: {
|
|
273
|
-
folderView: {
|
|
274
|
-
on: {
|
|
275
|
-
[EventType.SYNC_VIEW_MODE]: {
|
|
276
|
-
cond: (_, evt) => evt.payload.viewMode === "search",
|
|
277
|
-
target: "searchView",
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
|
-
},
|
|
281
|
-
searchView: { /* ... */ },
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
**Rationale**: State machines semantically represent modes. Context holds data, not state.
|
|
287
|
-
|
|
288
|
-
**Exception**: Storing previous values in context for business logic comparison is acceptable:
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
// Acceptable - storing previous value for change detection
|
|
292
|
-
type Context = {
|
|
293
|
-
oldHierarchy: Hierarchy | null; // For comparing with newHierarchy
|
|
294
|
-
viewMode: "folder" | "search"; // For comparing previous vs current in change detection
|
|
295
|
-
};
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
The distinction: Don't store current UI state in context when it can be modeled as machine state. Do store previous values when needed for comparison logic (change detection, diffing, etc.).
|
|
299
|
-
|
|
300
|
-
### Event & Type Patterns
|
|
301
|
-
|
|
302
|
-
**Consolidate similar events with properties:**
|
|
303
|
-
|
|
304
|
-
```typescript
|
|
305
|
-
// Bad - event proliferation
|
|
306
|
-
BROADCAST_FOLDER_VIEW_MODE;
|
|
307
|
-
BROADCAST_SEARCH_VIEW_MODE;
|
|
308
|
-
|
|
309
|
-
// Good - single event with property
|
|
310
|
-
BROADCAST_VIEW_MODE: {
|
|
311
|
-
payload: {
|
|
312
|
-
viewMode: "folder" | "search";
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
**Move reusable types to domain entities:**
|
|
318
|
-
|
|
319
|
-
```typescript
|
|
320
|
-
// Domain entity: FolderSelectorEntity.ts
|
|
321
|
-
export type FolderSelectorViewMode = "folder" | "search";
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
Single source of truth, consistent types across controllers/containers.
|
|
325
|
-
|
|
326
|
-
### assertEvent Usage
|
|
327
|
-
|
|
328
|
-
**Only use `assertEvent` when TypeScript can't narrow the event type:**
|
|
329
|
-
|
|
330
|
-
```typescript
|
|
331
|
-
// Bad - redundant, TypeScript already knows the type from transition
|
|
332
|
-
[EventType.SYNC_VIEW_MODE]: {
|
|
333
|
-
actions: assign({
|
|
334
|
-
viewMode: (_, evt) => {
|
|
335
|
-
assertEvent(evt, EventType.SYNC_VIEW_MODE); // Unnecessary
|
|
336
|
-
return evt.payload.viewMode;
|
|
337
|
-
},
|
|
338
|
-
}),
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Good - no assertEvent needed in assign actions
|
|
342
|
-
[EventType.SYNC_VIEW_MODE]: {
|
|
343
|
-
actions: assign({
|
|
344
|
-
viewMode: (_, evt) => evt.payload.viewMode,
|
|
345
|
-
}),
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Good - assertEvent needed in broadcast payload functions
|
|
349
|
-
broadcast({
|
|
350
|
-
type: PublicEventType.BROADCAST_VIEW_MODE,
|
|
351
|
-
payload: (ctx, evt) => {
|
|
352
|
-
assertEvent(evt, EventType.SYNC_VIEW_MODE); // Needed - type not narrowed
|
|
353
|
-
return { viewMode: evt.payload.viewMode };
|
|
354
|
-
},
|
|
355
|
-
})
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
**Rationale**: In `assign` actions, the event type is already narrowed by the transition. In `broadcast` payload functions, the event type isn't narrowed, so `assertEvent` provides runtime type safety.
|
|
359
|
-
|
|
360
|
-
### Handler Placement
|
|
361
|
-
|
|
362
|
-
**Event handlers live in the state that manages that concern** - not in parallel states or background sync:
|
|
363
|
-
|
|
364
|
-
```typescript
|
|
365
|
-
// Good - handler in the state managing search view
|
|
366
|
-
viewModeManagement: {
|
|
367
|
-
states: {
|
|
368
|
-
searchView: {
|
|
369
|
-
on: {
|
|
370
|
-
[EventType.SYNC_BATCH_SEARCH_RESULTS]: {
|
|
371
|
-
actions: [/* Handle search results here */],
|
|
372
|
-
},
|
|
373
|
-
},
|
|
374
|
-
},
|
|
375
|
-
},
|
|
376
|
-
},
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
Keeps concerns separated, avoids "half jobs" split across states.
|
|
380
|
-
|
|
381
|
-
### Container State Management
|
|
382
|
-
|
|
383
|
-
**When moving state to parent, remove all fallback logic** - make props required:
|
|
384
|
-
|
|
385
|
-
```typescript
|
|
386
|
-
// Good - fully controlled by parent
|
|
387
|
-
type ContainerProps = {
|
|
388
|
-
viewMode: ViewMode; // Required
|
|
389
|
-
onViewModeChange: (viewMode: ViewMode) => void; // Required
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
// Parent manages state
|
|
393
|
-
const [viewMode, setViewMode] = useState<ViewMode>("folder");
|
|
394
|
-
<Container viewMode={viewMode} onViewModeChange={setViewMode} />
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
Eliminates dual sources of truth, makes data flow explicit.
|
|
398
|
-
|
|
399
|
-
### Entity Imports
|
|
400
|
-
|
|
401
|
-
Import as namespace for clarity:
|
|
402
|
-
|
|
403
|
-
```typescript
|
|
404
|
-
import * as playlistSelectionEntity from "...";
|
|
405
|
-
playlistSelectionEntity.hasAddedPlaylists({ ... });
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
---
|
|
409
|
-
|
|
410
|
-
## Import Paths
|
|
411
|
-
|
|
412
|
-
Use absolute paths from `src/`:
|
|
413
|
-
|
|
414
|
-
```typescript
|
|
415
|
-
import { InputOption } from "src/features/common/domain/entities/SelectionInputEntity";
|
|
416
|
-
import * as playlistSelectionEntity from "src/features/tapesv3/domain/entities/PlaylistSelectionEntity";
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
Relative paths acceptable within same feature for deeply nested files.
|
|
420
|
-
|
|
421
|
-
---
|
|
422
|
-
|
|
423
|
-
## File Organization
|
|
424
|
-
|
|
425
|
-
### Feature Structure
|
|
426
|
-
|
|
427
|
-
```
|
|
428
|
-
src/features/{feature}/
|
|
429
|
-
├── application/
|
|
430
|
-
│ ├── containers/ # React containers (connect to XState)
|
|
431
|
-
│ └── controllers/ # XState machines
|
|
432
|
-
├── domain/
|
|
433
|
-
│ └── entities/ # Pure functions, types, business logic
|
|
434
|
-
├── presentation/ # React components (UI only)
|
|
435
|
-
└── types.ts # Shared types, event enums
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
### When to Create New Files
|
|
439
|
-
|
|
440
|
-
- **New entity file**: When the concern is distinct (e.g., selection vs reordering)
|
|
441
|
-
- **Same file**: When functions are tightly related and small
|
|
442
|
-
|
|
443
|
-
---
|
|
444
|
-
|
|
445
|
-
## Container / Controller Separation
|
|
446
|
-
|
|
447
|
-
### Containers
|
|
448
|
-
|
|
449
|
-
Location: `src/features/{feature}/application/containers/`
|
|
450
|
-
|
|
451
|
-
Containers wire controllers to presentation components. They do NOT contain business logic.
|
|
452
|
-
|
|
453
|
-
**Responsibilities:**
|
|
454
|
-
|
|
455
|
-
- Get actor ref via `useActorRefById`
|
|
456
|
-
- Select UI-relevant data via `useSelector`
|
|
457
|
-
- Create callbacks that send events to controller
|
|
458
|
-
- Compose presentation components
|
|
459
|
-
- Apply `useMemo` for derived UI values if needed
|
|
460
|
-
|
|
461
|
-
**Do NOT:**
|
|
462
|
-
|
|
463
|
-
- Transform data (controller's job)
|
|
464
|
-
- Contain business logic
|
|
465
|
-
- Make decisions about state
|
|
466
|
-
|
|
467
|
-
```typescript
|
|
468
|
-
export const TapesClipsViewHeaderContainer = () => {
|
|
469
|
-
const actor = useActorRefById<TapesClipsViewerHeaderRef>({
|
|
470
|
-
actorId: TapesActorIds.CLIPS_VIEWER_HEADER,
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
// Select UI data from controller
|
|
474
|
-
const { playlists, availablePlaylists } = useSelector(
|
|
475
|
-
actor,
|
|
476
|
-
(s) => ({
|
|
477
|
-
playlists: s.context.playlists,
|
|
478
|
-
availablePlaylists: s.context.availablePlaylists,
|
|
479
|
-
}),
|
|
480
|
-
shallowEquals,
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
// Callback sends event - no logic
|
|
484
|
-
const onPlaylistChange = useCallback(
|
|
485
|
-
(playlists: InputOption[]) => {
|
|
486
|
-
actor.send({
|
|
487
|
-
type: TapesClipsViewerHeaderEventType.PLAYLIST_CHANGED,
|
|
488
|
-
payload: { playlists },
|
|
489
|
-
});
|
|
490
|
-
},
|
|
491
|
-
[actor],
|
|
492
|
-
);
|
|
493
|
-
|
|
494
|
-
return (
|
|
495
|
-
<PanelClips.TapesViewerHeader>
|
|
496
|
-
<MultiSelectorV2Container
|
|
497
|
-
selectedOptions={playlists}
|
|
498
|
-
options={availablePlaylists}
|
|
499
|
-
onChange={onPlaylistChange}
|
|
500
|
-
/>
|
|
501
|
-
</PanelClips.TapesViewerHeader>
|
|
502
|
-
);
|
|
503
|
-
};
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
### Controllers
|
|
507
|
-
|
|
508
|
-
Location: `src/features/{feature}/application/controllers/`
|
|
509
|
-
|
|
510
|
-
Controllers handle all business rules and data transforms.
|
|
511
|
-
|
|
512
|
-
**Responsibilities:**
|
|
513
|
-
|
|
514
|
-
- Transform incoming data to UI-ready format
|
|
515
|
-
- Handle business logic (predicates, conditions)
|
|
516
|
-
- Broadcast events to other actors
|
|
517
|
-
- Manage state transitions
|
|
518
|
-
|
|
519
|
-
```typescript
|
|
520
|
-
[PublicTapesEventType.BROADCAST_CLIPS_DATA_SYNC]: {
|
|
521
|
-
actions: [
|
|
522
|
-
assign({
|
|
523
|
-
playlists: (ctx, evt) => {
|
|
524
|
-
// Transform here, not in container
|
|
525
|
-
return evt.payload.clipsData?.playlists.map((playlist) => ({
|
|
526
|
-
id: playlist.playlistId,
|
|
527
|
-
label: playlist.playlistTitle ?? "",
|
|
528
|
-
value: playlist.playlistId,
|
|
529
|
-
})) ?? [];
|
|
530
|
-
},
|
|
531
|
-
}),
|
|
532
|
-
],
|
|
533
|
-
},
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
### Data Flow
|
|
537
|
-
|
|
538
|
-
```
|
|
539
|
-
Server Data → Controller (transform) → Context → Container (useSelector) → Presentation
|
|
540
|
-
↑
|
|
541
|
-
User Action → Container (send event) → Controller (business logic)
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
### Containers, Handlers, and React Query
|
|
545
|
-
|
|
546
|
-
When events trigger handlers that fetch/update data, data flows through React Query and derived state—**never through useState in the container**.
|
|
547
|
-
|
|
548
|
-
**Rules:**
|
|
549
|
-
|
|
550
|
-
1. **No useState for handler-driven data** — Handler results live in React Query cache, not container state
|
|
551
|
-
2. **Queries only in dedicated hooks** — All queries in feature hook (e.g. `useTapesQueries`)
|
|
552
|
-
3. **Handler writes to cache** — Use `queryClient.setQueryData(key, result)`, query hook subscribes
|
|
553
|
-
4. **Trigger only in container** — Callback invokes handler prop, data returns via props from derived state
|
|
554
|
-
5. **Sync in useEffect** — Watch props, sync to actor in `useEffect`
|
|
555
|
-
|
|
556
|
-
**Pipeline:**
|
|
557
|
-
|
|
558
|
-
```
|
|
559
|
-
Event → Container (invoke handler)
|
|
560
|
-
→ Handler (fetch + setQueryData)
|
|
561
|
-
→ Query hook (subscribed to cache)
|
|
562
|
-
→ Derived state (transform)
|
|
563
|
-
→ Container props
|
|
564
|
-
→ useEffect syncs to actor
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
---
|
|
568
|
-
|
|
569
|
-
## Persisting State to localStorage
|
|
570
|
-
|
|
571
|
-
When user preferences persist across sessions, use a localStorage service invoked by the state machine.
|
|
572
|
-
|
|
573
|
-
### Service Pattern
|
|
574
|
-
|
|
575
|
-
Create service in `src/features/{feature}/infrastructure/services/`:
|
|
576
|
-
|
|
577
|
-
```typescript
|
|
578
|
-
export type ViewType = "Batch" | "Profile";
|
|
579
|
-
const STORAGE_KEY = "feature:preferenceKey";
|
|
580
|
-
|
|
581
|
-
export const getPreference = (): Promise<ViewType> => {
|
|
582
|
-
return new Promise((resolve) => {
|
|
583
|
-
try {
|
|
584
|
-
if (typeof window === "undefined" || !window.localStorage) {
|
|
585
|
-
resolve("Batch");
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
const stored = window.localStorage.getItem(STORAGE_KEY);
|
|
589
|
-
resolve(stored === "Profile" ? "Profile" : "Batch");
|
|
590
|
-
} catch {
|
|
591
|
-
resolve("Batch");
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
export const setPreference = (viewType: ViewType): void => {
|
|
597
|
-
try {
|
|
598
|
-
window.localStorage?.setItem(STORAGE_KEY, viewType);
|
|
599
|
-
} catch {
|
|
600
|
-
// Silently fail
|
|
601
|
-
}
|
|
602
|
-
};
|
|
603
|
-
```
|
|
604
|
-
|
|
605
|
-
### Machine Hydration Pattern
|
|
606
|
-
|
|
607
|
-
```typescript
|
|
608
|
-
viewManagement: {
|
|
609
|
-
initial: "hydrate",
|
|
610
|
-
states: {
|
|
611
|
-
hydrate: {
|
|
612
|
-
invoke: {
|
|
613
|
-
src: getPreference,
|
|
614
|
-
onDone: [
|
|
615
|
-
{
|
|
616
|
-
target: "profile",
|
|
617
|
-
cond: (_, evt) => evt.data === "Profile", // XState v4 uses evt.data
|
|
618
|
-
},
|
|
619
|
-
{ target: "batch" },
|
|
620
|
-
],
|
|
621
|
-
onError: { target: "batch" },
|
|
622
|
-
},
|
|
623
|
-
},
|
|
624
|
-
batch: {
|
|
625
|
-
tags: [Tags.VIEW_TYPE_BATCH],
|
|
626
|
-
entry: [broadcast({ type: EventType.BROADCAST_VIEW_TYPE_CHANGED, payload: () => ({ viewType: "Batch" }) })],
|
|
627
|
-
on: {
|
|
628
|
-
[EventType.TOGGLE_VIEW_TYPE]: {
|
|
629
|
-
target: "profile",
|
|
630
|
-
actions: [() => setPreference("Profile")],
|
|
631
|
-
},
|
|
632
|
-
},
|
|
633
|
-
},
|
|
634
|
-
profile: {
|
|
635
|
-
tags: [Tags.VIEW_TYPE_PROFILE],
|
|
636
|
-
entry: [broadcast({ type: EventType.BROADCAST_VIEW_TYPE_CHANGED, payload: () => ({ viewType: "Profile" }) })],
|
|
637
|
-
on: {
|
|
638
|
-
[EventType.TOGGLE_VIEW_TYPE]: {
|
|
639
|
-
target: "batch",
|
|
640
|
-
actions: [() => setPreference("Batch")],
|
|
641
|
-
},
|
|
642
|
-
},
|
|
643
|
-
},
|
|
644
|
-
},
|
|
645
|
-
},
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
**Key points**: Broadcast on entry for dependent controllers, use `evt.data` for invoke results, guard `window`/`localStorage` for SSR.
|
|
649
|
-
|
|
650
|
-
---
|
|
651
|
-
|
|
652
|
-
## Error Handling and Validation
|
|
653
|
-
|
|
654
|
-
### Never Suppress Unexpected Outcomes
|
|
655
|
-
|
|
656
|
-
**Always throw errors for unexpected states** - never silent returns:
|
|
657
|
-
|
|
658
|
-
```typescript
|
|
659
|
-
// Bad - silently fails, hides bugs
|
|
660
|
-
if (!playlist) return;
|
|
661
|
-
|
|
662
|
-
// Good - throws error, makes failures visible
|
|
663
|
-
if (!playlist) throw new Error("Playlist not found");
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
Silent failures hide bugs and data inconsistencies.
|
|
667
|
-
|
|
668
|
-
### Validation at Call Sites
|
|
669
|
-
|
|
670
|
-
**Validate at call sites** (controllers) for context-specific checks, then pass validated data to pure functions:
|
|
671
|
-
|
|
672
|
-
```typescript
|
|
673
|
-
// Controller validates
|
|
674
|
-
if (!ctx.activeTabId || !ctx.activeFolderId) {
|
|
675
|
-
throw new Error("Active tab ID and folder ID are required");
|
|
676
|
-
}
|
|
677
|
-
const playlist = ctx.availablePlaylists.find(
|
|
678
|
-
(p) => p.id === evt.payload.playlistId
|
|
679
|
-
);
|
|
680
|
-
if (!playlist) throw new Error("Playlist not found");
|
|
681
|
-
|
|
682
|
-
// Entity receives validated data
|
|
683
|
-
const action = tapesActionEntity.createApplyPlaylistActionForFolder({
|
|
684
|
-
playlist,
|
|
685
|
-
activeTabId: ctx.activeTabId,
|
|
686
|
-
activeFolderId: ctx.activeFolderId,
|
|
687
|
-
});
|
|
688
|
-
```
|
|
689
|
-
|
|
690
|
-
**Entity functions validate**: Input format/type issues, required parameters that can't be validated at call site.
|
|
691
|
-
|
|
692
|
-
**Entity functions do NOT validate**: Objects already validated at call sites, context-specific requirements.
|
|
693
|
-
|
|
694
|
-
### Type Safety: Required vs Optional
|
|
695
|
-
|
|
696
|
-
**Make parameters required when always validated at call sites:**
|
|
697
|
-
|
|
698
|
-
```typescript
|
|
699
|
-
// Good - required, TypeScript enforces
|
|
700
|
-
export const createAction = ({
|
|
701
|
-
playlist,
|
|
702
|
-
activeFolderId, // Required - validated at call site
|
|
703
|
-
}: {
|
|
704
|
-
playlist: InputOption;
|
|
705
|
-
activeFolderId: string;
|
|
706
|
-
}) => {
|
|
707
|
-
// No runtime check needed
|
|
708
|
-
};
|
|
709
|
-
```
|
|
710
|
-
|
|
711
|
-
TypeScript catches missing parameters at compile time, removes redundant runtime validation.
|
|
712
|
-
|
|
713
|
-
### Extracting Action Creation
|
|
714
|
-
|
|
715
|
-
When action creation logic appears in multiple controllers, extract to entity file.
|
|
716
|
-
|
|
717
|
-
**Location**: `src/features/{feature}/actions/{Feature}ActionEntity.ts`
|
|
718
|
-
|
|
719
|
-
```typescript
|
|
720
|
-
// Entity file
|
|
721
|
-
export const createApplyPlaylistAction = ({
|
|
722
|
-
playlist,
|
|
723
|
-
}: {
|
|
724
|
-
playlist: ClipsBatchPlayListTag;
|
|
725
|
-
}): PanelActionEntity => {
|
|
726
|
-
return panelActionEntity.toActionItem(TapesActionTypes.APPLY_PLAYLIST, {
|
|
727
|
-
label: playlist.label,
|
|
728
|
-
referenceId: playlist.id,
|
|
729
|
-
buttonType: "button",
|
|
730
|
-
metadata: { playlistId: playlist.id },
|
|
731
|
-
});
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
// Controller uses entity
|
|
735
|
-
const action = tapesActionEntity.createApplyPlaylistAction({ playlist });
|
|
736
|
-
```
|
|
737
|
-
|
|
738
|
-
Reusable, testable, single source of truth.
|
|
739
|
-
|
|
740
|
-
---
|
|
741
|
-
|
|
742
|
-
## Future Sections
|
|
743
|
-
|
|
744
|
-
_Add patterns as they emerge:_
|
|
745
|
-
|
|
746
|
-
- Testing patterns
|
|
747
|
-
- API/data fetching patterns
|