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.
Files changed (92) hide show
  1. package/README.md +61 -69
  2. package/package.json +2 -8
  3. package/scripts/check-pack-readme.mjs +1 -16
  4. package/scripts/check-workflow-contracts.mjs +34 -44
  5. package/scripts/install-cli.mjs +273 -417
  6. package/src/AGENTS.md +59 -192
  7. package/src/{.agents/agents → agents}/research/best-practices-researcher.md +2 -2
  8. package/src/{.agents/commands → commands}/assess.md +4 -4
  9. package/src/commands/install.md +43 -0
  10. package/src/{.agents/commands → commands}/metrics.md +1 -1
  11. package/src/commands/workflow-agents.md +101 -0
  12. package/src/{.agents/commands → commands}/workflow-compound.md +1 -1
  13. package/src/{.agents/commands → commands}/workflow-plan.md +24 -33
  14. package/src/commands/workflow-work.md +836 -0
  15. package/src/{.agents/references → references}/README.md +1 -1
  16. package/src/{.agents/skills → skills}/capture-skill/SKILL.md +1 -1
  17. package/src/{.agents/skills → skills}/compound-docs/SKILL.md +6 -6
  18. package/src/{.agents/skills → skills}/compound-docs/references/yaml-schema.md +2 -2
  19. package/src/skills/setup-agents/SKILL.md +250 -0
  20. package/src/skills/standards/SKILL.md +79 -0
  21. package/src/skills/standards/references/architecture.md +228 -0
  22. package/src/skills/standards/references/code-quality.md +192 -0
  23. package/src/skills/standards/references/presentation.md +515 -0
  24. package/src/skills/standards/references/services.md +172 -0
  25. package/src/skills/standards/references/state-management.md +936 -0
  26. package/.claude-plugin/plugin.json +0 -7
  27. package/.cursor-plugin/plugin.json +0 -20
  28. package/.cursor-plugin/registration.json +0 -5
  29. package/scripts/check-version-parity.mjs +0 -36
  30. package/scripts/generate-platform-artifacts.mjs +0 -230
  31. package/src/.agents/commands/install.md +0 -51
  32. package/src/.agents/commands/workflow-work.md +0 -690
  33. package/src/.agents/registry.json +0 -48
  34. package/src/.agents/scripts/self-check.mjs +0 -227
  35. package/src/.agents/scripts/sync-opencode.mjs +0 -362
  36. package/src/.agents/skills/presentation-composability/SKILL.md +0 -72
  37. package/src/.agents/skills/react-ddd-mvc-frontend/SKILL.md +0 -51
  38. package/src/.agents/skills/react-ddd-mvc-frontend/references/feature-structure.md +0 -25
  39. package/src/.agents/skills/react-ddd-mvc-frontend/references/implementation-principles.md +0 -21
  40. package/src/.agents/skills/react-ddd-mvc-frontend/references/responsibility-gates.md +0 -41
  41. package/src/.agents/skills/react-ddd-mvc-frontend/references/source-map.md +0 -11
  42. package/src/.agents/skills/standards/SKILL.md +0 -747
  43. package/src/.agents/skills/xstate-actor-orchestration/SKILL.md +0 -197
  44. package/src/.agents/skills/xstate-actor-orchestration/agents/openai.yaml +0 -4
  45. package/src/.agents/skills/xstate-actor-orchestration/assets/statecharts/.gitkeep +0 -0
  46. package/src/.agents/skills/xstate-actor-orchestration/references/actor-system-patterns.md +0 -71
  47. package/src/.agents/skills/xstate-actor-orchestration/references/event-contracts.md +0 -73
  48. package/src/.agents/skills/xstate-actor-orchestration/references/functional-domain-patterns.md +0 -53
  49. package/src/.agents/skills/xstate-actor-orchestration/references/machine-structure-and-tags.md +0 -36
  50. package/src/.agents/skills/xstate-actor-orchestration/references/react-container-pattern.md +0 -45
  51. package/src/.agents/skills/xstate-actor-orchestration/references/reliability-observability.md +0 -39
  52. package/src/.agents/skills/xstate-actor-orchestration/references/skill-validation.md +0 -33
  53. package/src/.agents/skills/xstate-actor-orchestration/references/source-map.md +0 -44
  54. package/src/.agents/skills/xstate-actor-orchestration/references/statechart-review-and-signoff.md +0 -59
  55. package/src/.agents/skills/xstate-actor-orchestration/references/testing-strategy.md +0 -35
  56. package/src/.agents/skills/xstate-actor-orchestration/scripts/create-statechart-artifact.sh +0 -71
  57. package/src/.agents/skills/xstate-actor-orchestration/scripts/validate-skill.sh +0 -138
  58. package/src/generated/opencode.managed.json +0 -115
  59. /package/src/{.agents/agents → agents}/research/framework-docs-researcher.md +0 -0
  60. /package/src/{.agents/agents → agents}/research/git-history-analyzer.md +0 -0
  61. /package/src/{.agents/agents → agents}/research/learnings-researcher.md +0 -0
  62. /package/src/{.agents/agents → agents}/research/repo-research-analyst.md +0 -0
  63. /package/src/{.agents/agents → agents}/review/agent-native-reviewer.md +0 -0
  64. /package/src/{.agents/agents → agents}/review/planning-technical-reviewer.md +0 -0
  65. /package/src/{.agents/agents → agents}/workflow/bug-reproduction-validator.md +0 -0
  66. /package/src/{.agents/agents → agents}/workflow/lint.md +0 -0
  67. /package/src/{.agents/agents → agents}/workflow/spec-flow-analyzer.md +0 -0
  68. /package/src/{.agents/commands → commands}/test-browser.md +0 -0
  69. /package/src/{.agents/commands → commands}/workflow-brainstorm.md +0 -0
  70. /package/src/{.agents/commands → commands}/workflow-review.md +0 -0
  71. /package/src/{.agents/commands → commands}/workflow-tech-review.md +0 -0
  72. /package/src/{.agents/commands → commands}/workflow-triage.md +0 -0
  73. /package/src/{.agents/references → references}/standards/README.md +0 -0
  74. /package/src/{.agents/skills → skills}/agent-browser/SKILL.md +0 -0
  75. /package/src/{.agents/skills → skills}/audit-traceability/SKILL.md +0 -0
  76. /package/src/{.agents/skills → skills}/brainstorming/SKILL.md +0 -0
  77. /package/src/{.agents/skills → skills}/compound-docs/assets/critical-pattern-template.md +0 -0
  78. /package/src/{.agents/skills → skills}/compound-docs/assets/resolution-template.md +0 -0
  79. /package/src/{.agents/skills → skills}/compound-docs/schema.project.yaml +0 -0
  80. /package/src/{.agents/skills → skills}/compound-docs/schema.yaml +0 -0
  81. /package/src/{.agents/skills → skills}/data-foundations/SKILL.md +0 -0
  82. /package/src/{.agents/skills → skills}/document-review/SKILL.md +0 -0
  83. /package/src/{.agents/skills → skills}/file-todos/SKILL.md +0 -0
  84. /package/src/{.agents/skills → skills}/file-todos/assets/todo-template.md +0 -0
  85. /package/src/{.agents/skills → skills}/financial-workflow-integrity/SKILL.md +0 -0
  86. /package/src/{.agents/skills → skills}/git-worktree/SKILL.md +0 -0
  87. /package/src/{.agents/skills → skills}/pii-protection-prisma/SKILL.md +0 -0
  88. /package/src/{.agents/skills → skills}/process-metrics/SKILL.md +0 -0
  89. /package/src/{.agents/skills → skills}/process-metrics/assets/daily-template.md +0 -0
  90. /package/src/{.agents/skills → skills}/process-metrics/assets/monthly-template.md +0 -0
  91. /package/src/{.agents/skills → skills}/process-metrics/assets/weekly-template.md +0 -0
  92. /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