@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.
- package/package.json +1 -1
- package/templates/.claude/skills/slice-automation/SKILL.md +237 -35
- package/templates/backend/AGENTS.md +533 -0
- package/templates/backend/prompt.md +460 -91
- package/templates/frontend/prompt.md +11 -7
- package/templates/frontend/setup-env.sh +0 -0
- package/templates/frontend/src/App.tsx +2 -1
- package/templates/frontend/src/contexts/AuthContext.tsx +6 -5
- package/templates/prompt.md +2 -1
- package/templates/server.mjs +2 -1
|
@@ -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.
|