@nebulit/embuilder 0.1.46 → 0.1.48

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.
@@ -0,0 +1,533 @@
1
+ # Agent Learnings
2
+
3
+ ## Core Principles
4
+
5
+ ### Source of Truth
6
+ - **config.json is the TRUE source of truth** for slice status - always check it, not just index.json or slice.json
7
+ - **slice.json reflects the current specification** for field definitions, event structures, and specifications
8
+ - When config.json shows "Planned" but index.json/slice.json show "Done", verify implementation and update config.json
9
+ - The aggregate field in slice.json determines domain entity naming (Location → locationId, Restaurant → restaurantId)
10
+ - When an event is used by multiple slices, fixing it requires updating ALL slices that use it
11
+ - Field names must be consistent across: events, commands, migrations, projections, routes, tests, API docs
12
+
13
+ ### Pre-Implementation Checks
14
+ - **Always check if slice already exists** before implementing a "Planned" slice - verify `src/slices/{SliceName}/`
15
+ - **Run tests first**: existing implementations may only need test data fixes
16
+ - **Search for event usage**: `grep -r "EventName" src/slices/` before modifying events
17
+ - **Status drift**: slice status in index.json may lag behind actual implementation
18
+
19
+ ### Slice Status
20
+ - Valid statuses: "Done", "Planned", "Assigned", "Created", "Blocked", "Informational"
21
+ - **"Assigned" = "Planned"** - treat as equivalent when picking next slice
22
+ - Always update status to "Done" after completing implementation and passing tests
23
+
24
+ ## Event Management
25
+
26
+ ### Event Type Naming & Registration
27
+ - Event type names MUST use PascalCase (RestaurantRegistered, not Restaurantregistered)
28
+ - All new events MUST be added to ContextEvents union type in `src/events/ContextEvents.ts`
29
+ - Event types follow `Event<'EventName', {...fields}, {...metadata}>` pattern
30
+ - Event titles in slice.json use lowercase (e.g., "Event created" not "Event Created")
31
+ - TypeScript type names use PascalCase (e.g., `EventCreated` type)
32
+
33
+ ### Event Field Structure
34
+ - DateTime type in slice.json → TypeScript `Date` type (not string)
35
+ - Event metadata MUST include restaurantId and userId for multi-tenancy and authorization
36
+ - Auto-generated code may have incorrect casing or wrong field types - always verify against slice.json
37
+ - Field name typos in slice.json must be preserved for consistency (e.g., "restaurandId")
38
+
39
+ ### Event Verification & Creation
40
+ - **Always verify events exist** in `src/events/` and `ContextEvents.ts` before creating them
41
+ - Events from unimplemented slices can be created based on their slice.json specifications
42
+ - STATE_VIEW slices can reference events that don't exist yet - create them based on slice.json
43
+ - Check slice.json → dependencies array (INBOUND) for STATE_VIEW event sources
44
+
45
+ ### Event Structure Changes - CRITICAL
46
+ When modifying existing event structure:
47
+ 1. Find all consumers: `grep -r "EventName" src/slices/`
48
+ 2. Update ALL occurrences: command handlers, projections, tests, routes, API docs
49
+ 3. Run `npm run build` BEFORE tests - TypeScript shows ALL files with type mismatches
50
+ 4. Checklist: Event updated → handlers updated → tests updated → build passes → all tests pass
51
+
52
+ ### Cross-Slice Event Reuse
53
+ - Events created for STATE_VIEW projection can be reused by STATE_CHANGE commands
54
+ - Same event consumed by multiple projections (e.g., CheckinCancelled)
55
+ - Create event once, reference in multiple slice implementations
56
+
57
+ ## Architecture & Auto-Discovery
58
+
59
+ ### Auto-Discovery System
60
+ - **Routes**: `src/slices/**/routes.ts` (exports `api` function returning `WebApiSetup`)
61
+ - **Processors**: `src/slices/**/processor.ts` (exports `processor = { start: () => {...} }`)
62
+ - No manual registration needed - loaded automatically at server startup
63
+
64
+ ### Projections Registration
65
+ - All new projections must be registered in `src/common/loadPostgresEventstore.ts`
66
+ - **Critical**: When slice files deleted, check for stale imports causing build failures
67
+ - Pattern: `grep -n "DeletedSliceName" src/common/loadPostgresEventstore.ts`
68
+
69
+ ### PostgreSQL Critical Imports
70
+ ```typescript
71
+ import { postgreSQLRawSQLProjection } from '@event-driven-io/emmett-postgresql';
72
+ import { sql } from '@event-driven-io/dumbo'; // NOT from emmett-postgresql!
73
+ import { ContextEvents } from '../../events/ContextEvents';
74
+ ```
75
+ - Always use `.withSchema('public')` in PostgreSQL queries
76
+
77
+ ### Type Coercion
78
+ - Pongo stores numeric-looking strings (e.g., "1") as bigints (e.g., 1n)
79
+ - Use `String(value)` when comparing IDs in test assertions
80
+
81
+ ## Stream ID Patterns Reference
82
+
83
+ ### `idAttribute` and Stream ID
84
+ - If a field on command/event has `"idAttribute": true`, that field is the streamId
85
+ - Explicit stream identifier in slice.json takes precedence over `idAttribute`
86
+
87
+ ### Common Stream ID Patterns
88
+
89
+ | Aggregate Type | Stream ID Format | Helper | Used By |
90
+ |---------------|------------------|--------|---------|
91
+ | Time Entry | `time-entry-{clerkId}-{month}` | `timeEntryStreamId()` | CheckIn, CheckOut, SubmitTimeEntry, DeleteTimeentry, UpdateTimeEntry |
92
+ | Event Assignment | `{restaurantId}-event-{eventId}` | `eventStreamId()` | AssignClerkToEvent, UnAssignClerkFromEvent |
93
+ | Shift Publication | `shift-publication-{shiftId}` | N/A | PublishShift, UnpublishShift |
94
+
95
+ **Notes**: Time entry month format: MMYYYY (e.g., "022026"). Multiple commands can share same stream.
96
+
97
+ ## Implementation Patterns
98
+
99
+ > For templates and step-by-step guides, use skills: `/state-change-slice`, `/state-view-slice`, `/automation-slice`
100
+
101
+ ### STATE_CHANGE Slice Pattern
102
+
103
+ **Key Rules**:
104
+ - Use proper `initialState` function (not empty object `{}`) in `DeciderSpecification.for()`
105
+ - Do NOT add explicit type arguments - let TypeScript infer (avoids TS2558)
106
+ - Switch statements: always use explicit `break` to prevent fallthrough bugs
107
+ - you must not change the signature of evolve - it has (state:State, event:Event)
108
+ - you must not change the signature of decide - it has (state:State, command:Command) - all data to verify the command must be passed via state.
109
+
110
+ **Structure**:
111
+ ```typescript
112
+ type State = { /* track relevant state */ };
113
+ const initialState = (): State => ({ /* defaults */ });
114
+ const decide = (state: State, cmd: Command): Event[] => {
115
+ if (invalidCondition) throw 'error.code';
116
+ return [createEvent(cmd)];
117
+ };
118
+ const evolve = (state: State, event: ContextEvents): State => {
119
+ switch (event.type) {
120
+ case 'EventType': return { ...state, field: newValue };
121
+ default: return state;
122
+ }
123
+ };
124
+ ```
125
+
126
+ **Error Testing**: Use `.shouldFail()` or `assert.throws()`
127
+
128
+ ### STATE_VIEW Slice Pattern
129
+
130
+ **Key Rules**:
131
+ - Always verify ALL events from slice.json are in `canHandle` array
132
+ - Use specific event union type (e.g., `Event1 | Event2`) NOT `ContextEvents` when handling subset
133
+ - Missing event handlers = incomplete implementation
134
+
135
+ **Projection Structure**:
136
+ ```typescript
137
+ export const ProjectionName = postgreSQLRawSQLProjection({
138
+ canHandle: ['Event1', 'Event2'],
139
+ evolve: (event: Event1 | Event2) => {
140
+ switch (event.type) {
141
+ case 'Event1': return sql(db(table).insert({...}).onConflict().merge());
142
+ case 'Event2': return sql(db(table).where({...}).delete());
143
+ default: return [];
144
+ }
145
+ }
146
+ });
147
+ ```
148
+
149
+ **Event Dependencies**: Look in slice.json → readmodels → dependencies (INBOUND) for event IDs
150
+
151
+ ### AUTOMATION Slice Pattern
152
+
153
+ **Structure**: STATE_CHANGE + processor.ts + work queue projection
154
+
155
+ **Processor Implementation**:
156
+ ```typescript
157
+ import * as cron from 'node-cron';
158
+ import { createServiceClient } from '../../common/supabaseClient';
159
+
160
+ const config = { schedule: '*/30 * * * * *', endpoint: "work_queue_table" };
161
+
162
+ export const processor = {
163
+ start: () => {
164
+ cron.schedule(config.schedule, async () => {
165
+ const client = createServiceClient();
166
+ const result = await client.from(config.endpoint)
167
+ .select("*").eq('must_process', true).limit(1);
168
+
169
+ if (result.error) return;
170
+ for (const item of result.data ?? []) {
171
+ try {
172
+ await handleCommand(streamId, command, { userId: 'system', ...metadata });
173
+ } catch (error) {
174
+ console.error('Processing error:', error);
175
+ }
176
+ }
177
+ });
178
+ }
179
+ };
180
+ ```
181
+
182
+ **Key Rules**:
183
+ - Use `.limit(1)` to process one item at a time
184
+ - `createServiceClient()` bypasses RLS for automation
185
+ - Processor uses snake_case column names (e.g., `reservation_id`)
186
+ - Work queue lifecycle: add on trigger → update flags → delete on completion
187
+
188
+ ## State Tracking Patterns
189
+
190
+ ### Boolean/Toggle State
191
+ ```typescript
192
+ type State = { isActive: boolean };
193
+ const decide = (state: State, cmd: Command): Event[] => {
194
+ if (state.isActive) throw "already.active"; // Activate guard
195
+ if (!state.isActive) throw "not.active"; // Deactivate guard
196
+ return [event];
197
+ };
198
+ ```
199
+
200
+ **Database**: Use flag column (e.g., `is_online_active BOOLEAN`), update with upsert `.onConflict().merge()`
201
+
202
+ ### Set<string> for ID Tracking
203
+ ```typescript
204
+ type State = { trackedIds: Set<string> };
205
+ const evolve = (state: State, event: ContextEvents): State => {
206
+ switch (event.type) {
207
+ case 'ItemAdded': return { trackedIds: new Set([...state.trackedIds, event.data.id]) };
208
+ case 'ItemRemoved':
209
+ const newSet = new Set(state.trackedIds);
210
+ newSet.delete(event.data.id);
211
+ return { trackedIds: newSet };
212
+ }
213
+ };
214
+ ```
215
+
216
+ **Examples**: UnAssign Clerk from Event, Delete Time Entry, Update Time Entry
217
+
218
+ ### Multi-Flag State Machine
219
+ ```typescript
220
+ type State = { submitted: boolean; reverted: boolean; approved: boolean; declined: boolean };
221
+ const decide = (state: State, cmd: Command): Event[] => {
222
+ if (state.submitted) throw 'cannot submit twice';
223
+ if (!state.submitted) throw 'not_submitted';
224
+ if (state.reverted) throw 'already_reverted';
225
+ return [event];
226
+ };
227
+ ```
228
+
229
+ **Workflows**: Timesheet (Submitted → Reverted → Resubmitted), Approval (Approved → Declined → Reapproved)
230
+
231
+ ## Database & Projections
232
+
233
+ ### Migration Rules
234
+ - **NEVER modify existing migrations** - always add new ones
235
+ - Version check: `ls -1 supabase/migrations/ | grep "^V" | sort -V | tail -5`
236
+ - Naming: `V{N}__{table_name}.sql` (e.g., `V41__clerk_details.sql`)
237
+ - PostgreSQL types: Use TEXT/VARCHAR (not "string"), valid UUID format required
238
+ - Composite PKs for multi-tenant: `PRIMARY KEY (restaurant_id, entity_id)`
239
+
240
+ ### Projection Patterns
241
+
242
+ **Add/Remove**:
243
+ ```typescript
244
+ case 'Added': return sql(db(table).insert({...}).onConflict(key).merge());
245
+ case 'Removed': return sql(db(table).where({id}).delete());
246
+ ```
247
+
248
+ **Work Queue Lifecycle**:
249
+ ```typescript
250
+ case 'TriggerEvent': return sql(db(table).insert({id, must_process: false}).onConflict().merge());
251
+ case 'ProgressEvent': return sql(db(table).where({id}).update({must_process: true}));
252
+ case 'CompletionEvent': return sql(db(table).where({id}).delete());
253
+ ```
254
+
255
+ **Conditional Flags**: Set flags based on matching business data (e.g., `must_cancel = true` only when clerkId matches)
256
+
257
+ ### JSONB Field Handling
258
+ - PostgreSQL JSONB auto-parsed by pg driver - DO NOT use `JSON.parse()` in tests
259
+ - Storage: `assignees JSONB DEFAULT '[]'::jsonb`
260
+ - Updates: `assignees: JSON.stringify(data.assignees)` (plain JavaScript, not raw SQL)
261
+ - Array conversion for display: `Array.isArray(field) ? field.join(', ') : field || ''`
262
+
263
+ ### Query Endpoints - Authentication
264
+ All endpoints must:
265
+ 1. Require JWT: `requireUser(req, res, true)`
266
+ 2. Filter by authenticated user's ID
267
+ 3. Use anon key Supabase client
268
+
269
+ ### Date Handling
270
+ - slice.json DateTime → TypeScript `Date` type
271
+ - Routes: `date: new Date(assertNotEmpty(req.body.date))`
272
+ - Tests: Use Date objects: `new Date('2026-03-15T10:00:00Z')`
273
+ - Timezone: Europe/Berlin → UTC conversion (CET = UTC+1, CEST = UTC+2 for DST)
274
+
275
+ ## Test Patterns Reference
276
+
277
+ ### STATE_CHANGE Tests
278
+ ```typescript
279
+ DeciderSpecification.for(decide, evolve, initialState)
280
+ .given([PrerequisiteEvent1, PrerequisiteEvent2])
281
+ .when(Command({ field: 'value' }))
282
+ .then([ExpectedEvent({ field: 'value' })]);
283
+ ```
284
+
285
+ ### STATE_VIEW Tests
286
+ ```typescript
287
+ PostgreSQLProjectionSpec.for(ProjectionName)
288
+ .given([Event1, Event2])
289
+ .when([]) // Always empty
290
+ .then(async (state) => {
291
+ const result = await state.query();
292
+ assert.equal(result[0].field, expectedValue);
293
+ });
294
+ ```
295
+
296
+ ### Error Case Tests
297
+ ```typescript
298
+ .given([EventCreated])
299
+ .when(Command)
300
+ .shouldFail(); // Expects error
301
+ ```
302
+
303
+ ### Test Coverage
304
+ - **STATE_CHANGE**: Happy path + all error guards
305
+ - **STATE_VIEW**: One test per INBOUND event dependency
306
+ - **AUTOMATION**: Command logic + error guards (processor not unit tested)
307
+
308
+ ## Test Specifications & code-slice.json
309
+
310
+ ### Purpose of test-analyzer
311
+ - Analyzes tests to extract behavioral specifications
312
+ - Generates code-slice.json in `.slices/{Context}/{folder}/`
313
+ - Includes only specs NOT in slice.json (drift detection)
314
+
315
+ ### Drift Detection
316
+ **Generate code-slice.json when**:
317
+ - Test specs NOT in slice.json specifications array
318
+ - slice.json has `specifications: []` but tests exist
319
+
320
+ **No code-slice.json when** (No Drift):
321
+ - All test specs already in slice.json
322
+ - Quality indicator: implementation matches design
323
+
324
+ ### linkedId Lookup
325
+ **STATE_CHANGE**: Command/Event IDs from slice.json → commands/events array → id field
326
+ **STATE_VIEW**: Event IDs from slice.json → readmodels → dependencies (INBOUND) → id field
327
+
328
+ ```bash
329
+ # Find slice.json for event source
330
+ find .slices -name "slice.json" -path "*{slicename}*"
331
+
332
+ # Extract event ID (use lowercase title)
333
+ cat path/to/slice.json | jq '.events[] | select(.title == "Event created") | .id'
334
+ ```
335
+
336
+ **Note**: Event titles in slice.json are lowercase ("Event created"), TypeScript types are PascalCase (`EventCreated`)
337
+
338
+ ### Field Type Mapping
339
+ - UUID format → "UUID" type
340
+ - ISO dates → "String" or "Date"
341
+ - Whole numbers → "Integer"
342
+ - Text → "String"
343
+ - Cardinality: "Single" for non-array fields
344
+
345
+ ## Implementation Quality Checklist
346
+
347
+ ### Standard Workflow (All Slices)
348
+ 1. [ ] Build passes: `npm run build`
349
+ 2. [ ] All tests pass: `npm run test`
350
+ 3. [ ] Run test-analyzer skill (if drift detected)
351
+ 4. [ ] Update `.slices/index.json` status to "Done"
352
+ 5. [ ] Update `progress.txt` with iteration summary
353
+ 6. [ ] Update `AGENTS.md` with new learnings
354
+ 7. [ ] Commit with Co-Authored-By line
355
+
356
+ ### Additional: AUTOMATION Slices
357
+ - [ ] Processor.ts with CRON schedule
358
+ - [ ] Routes for manual invocation
359
+ - [ ] Test includes error cases
360
+
361
+ ### Additional: STATE_VIEW Slices
362
+ - [ ] All INBOUND events in canHandle array
363
+ - [ ] Each event has handler in evolve
364
+ - [ ] Test per INBOUND event
365
+ - [ ] Projection registered in `loadPostgresEventstore.ts`
366
+
367
+ ### "To-Do List" STATE_VIEW Pattern
368
+ When a STATE_VIEW acts as a work queue (items appear when created, disappear when processed):
369
+ - Use `insert().onConflict().ignore()` for the trigger event (e.g., RestaurantRegistered)
370
+ - Use `where({id}).delete()` for the completion event (e.g., BucketCreated)
371
+ - Table naming: `slice_<name>` prefix for slice-specific work queue tables
372
+ - The "bucketId" maps to "restaurantId" — both fields stored, one used as PK
373
+
374
+ ### AUTOMATION Processor with External API
375
+ When an AUTOMATION slice's `backendPrompt` specifies an external operation (e.g., "create a bucket using the Supabase storage API"):
376
+ - Perform the external API call **before** firing the command
377
+ - Use `client.storage.createBucket(id)` for Supabase storage bucket creation
378
+ - Name the bucket after the entity identifier (e.g., restaurant ID)
379
+ - On failure, catch per-item and continue (don't stop the entire batch)
380
+ - The command acts as the **confirmation** that the external operation succeeded
381
+
382
+ ### folder Field in slice.json
383
+ When a command/event has a `folder` field with `"mapping": "month"`:
384
+ - The `folder` field's value = the `month` field's value (same value, different semantic purpose)
385
+ - In storage uploads: use `${month}/` as the folder prefix in the file path (e.g., `${month}/${clerkId}-${month}.csv`)
386
+ - `backendPrompt: "use the month as a folder in the bucket"` = upload path `${month}/{filename}` not just `{filename}`
387
+ - Pass `folder` in command data and event data just like other fields
388
+
389
+ ### Additional: Completing Partial Slices
390
+ - [ ] Verify all dependencies from slice.json implemented
391
+ - [ ] Compare canHandle with dependencies
392
+ - [ ] Add missing handlers and tests
393
+
394
+ ### Event Structure Changes
395
+ - [ ] Event definition updated
396
+ - [ ] All command handlers updated
397
+ - [ ] All consuming slice tests updated
398
+ - [ ] Build passes, all tests pass
399
+
400
+ ## Common Patterns by Domain
401
+
402
+ ### Time Entry Domain
403
+
404
+ **Stream ID**: `time-entry-{clerkId}-{month}` (MMYYYY format)
405
+
406
+ **Key Events**: CheckedIn, CheckedOut, TimeEntryAdded, TimesheetSubmitted, SubmissionReverted, TimesheetApproved, TimesheetDeclined, CheckinCancelled
407
+
408
+ **Workflow**:
409
+ 1. CheckIn → ActiveCheckins
410
+ 2. CheckOut → TimeEntryCheckoutsToProcess (work queue)
411
+ 3. ProcessTimeEntryCheckout (automation) → TimeEntryAdded
412
+ 4. SubmitTimeEntry → ActiveCheckinsToCancel (mark for cancellation)
413
+ 5. CancelCheckin (automation) → CheckinCancelled
414
+ 6. SubmitTimesheet → TimesheetSubmitted → SubmissionDate, SubmissionStatus
415
+ 7. ApproveTimesheet → TimesheetApproved → ApprovalDate
416
+ 8. DeclineTimesheet → TimesheetDeclined (allows reapproval)
417
+
418
+ **Hours Calculation**: `(endDate - startDate) ms / (1000*60*60)`, rounded to 0.5
419
+
420
+ **Guard Patterns**:
421
+ - CheckOut: if CheckedOut in stream → throw "no active check"
422
+ - ProcessTimeEntryCheckout: if NO CheckedOut → throw "no confirmed checkout"
423
+ - SubmitTimesheet: if submitted → throw "cannot submit twice"
424
+ - Delete/Update: track Set<string> of entry IDs
425
+ - SubmitTimeEntry: if `approved && !declined` → throw "cannot submit time entry after approval"
426
+ - DeleteTimeentry: if `approved && !declined` → throw "cannot delete after approval"
427
+ - UpdateTimeEntry: if `submitted && !declined` → throw "cannot submit after submission"
428
+
429
+ **Cross-Slice State Rules (Time Entry Approval Guards)**:
430
+ - `TimesheetApproved` event: sets `approved=true, declined=false`
431
+ - `TimesheetDeclined` event: sets `approved=false, declined=true` (resets approval, re-enables submit/delete/update)
432
+ - `SubmissionReverted` event: sets `approved=false` (allows new submissions after revert)
433
+ - Multiple commands track the same approval state: SubmitTimeEntry, DeleteTimeentry, UpdateTimeEntry
434
+ - Decline AFTER approval is valid workflow (and allows subsequent operations)
435
+
436
+ ### Event Assignment Domain
437
+
438
+ **Stream ID**: `{restaurantId}-event-{eventId}` via `eventStreamId()` helper
439
+
440
+ **Key Events**: EventCreated, EventCancelled, EventPlanned, EventReplanned, ClerkAssignedToEvent, ClerkUnassignedFromEvent
441
+
442
+ **Inverse Operations**:
443
+ - Assign/UnAssign share same stream ID
444
+ - Use Set<string> for tracking assigned clerks
445
+ - Assign guard: `if (has(clerkId)) throw "already.assigned"`
446
+ - UnAssign guard: `if (!has(clerkId)) throw "not.assigned"`
447
+
448
+ **Planning**: EventReplanned resets isPlanned to false (allows re-planning)
449
+
450
+ ### Activation/Deactivation Pattern
451
+
452
+ **Complementary Commands**:
453
+ - Both track same boolean state, validate opposite conditions
454
+ - Activate: `if (active) throw "already.active"`
455
+ - Deactivate: `if (!active) throw "not.active"`
456
+ - Shared evolve handles BOTH events
457
+
458
+ **Implementation Order**: events → STATE_VIEW → Activate → Deactivate
459
+
460
+ **Examples**: OnlineReservation, Shift Publication
461
+
462
+ ### Dashboard Read Models
463
+
464
+ **Characteristics**:
465
+ - Handle 4 events: Create, Cancel, Assign, Unassign
466
+ - JSONB assignees array
467
+ - PRIMARY KEY on entity_id
468
+ - Table naming: `active_{entity}_for_dashboard`
469
+
470
+ **Test Pattern**: Create → Assignment/unassignment → Delete
471
+
472
+ ## Common Gotchas
473
+
474
+ ### Hooks Rewrite Files
475
+ - Claude Code hooks extensively rewrite files after save
476
+ - Always read files AFTER hook execution
477
+ - Hooks add restaurantId, userId, enrich ContextEvents.ts
478
+
479
+ ### Auto-Generation Issues
480
+ - Migration files may get auto-numbered incorrectly (V99) - renumber
481
+ - Projection `canHandle` may be incomplete - verify against slice.json
482
+ - Pre-generated routes may use wrong streamId
483
+
484
+ ### TypeScript Issues
485
+ - Don't chain `.given()` directly on `DeciderSpecification.for()`
486
+ - macOS filesystem case-insensitive, TypeScript imports case-sensitive
487
+ - Use specific event union, NOT `ContextEvents` when handling subset
488
+ - Don't add explicit type arguments to `DeciderSpecification.for()`
489
+
490
+ ### State Management
491
+ - Always use explicit `break` in switch statements
492
+ - Tests for state-dependent commands need prerequisite events in given
493
+ - Paired commands share same stream
494
+ - Use proper `initialState` function (not `{}`)
495
+
496
+ ### Knex/Database
497
+ - `.insert({...}).del()` does NOT insert - it deletes
498
+ - Use `.insert().onConflict().merge()` for upserts
499
+ - Use `.where().update()` or `.where().delete()` for modifications
500
+
501
+ ### Specifications
502
+ - Empty `specifications: []` → create tests covering event dependencies
503
+ - Dependency-only events → include in `canHandle` as no-op (return `[]`)
504
+ - Spec assertions may target different read model than slice's own
505
+ - Pre-existing test failures don't block new slice implementation
506
+
507
+ ### Metadata Updates
508
+ - Specs may be added to slice.json AFTER tests implemented
509
+ - Verify executable tests exist before assuming new work needed
510
+ - If tests exist and pass → metadata sync → commit as "chore: sync specifications"
511
+
512
+ ### Rate Limit Recovery
513
+ - Agent context resets after hitting API rate limits ("You've hit your limit")
514
+ - On resume, re-read slice.json and current implementation to verify state before continuing
515
+ - Do not re-implement already-completed files — check for existing files first
516
+
517
+ ## Error Handling
518
+
519
+ - HTTP 409 for conflicts, HTTP 500 for server errors
520
+ - Business-friendly error codes (e.g., "already.active", "not.assigned")
521
+ - German error messages typical in this codebase
522
+ - Check errors most specific to most general (e.g., not_submitted → already_reverted → already_approved)
523
+
524
+ ## UI Documentation (ui-prompt.md)
525
+
526
+ Each STATE_CHANGE slice should include `ui-prompt.md` with:
527
+ 1. Endpoint URL + HTTP method
528
+ 2. Payload example (realistic JSON)
529
+ 3. Required headers (correlation_id, Authorization)
530
+ 4. Response format (success/error)
531
+ 5. Field descriptions + API client code + curl commands
532
+
533
+ For STATE_VIEW: include query endpoint docs + database table definition.