@nebulit/embuilder 0.1.46 → 0.1.47

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebulit/embuilder",
3
- "version": "0.1.46",
3
+ "version": "0.1.47",
4
4
  "description": "Event-model driven development toolkit for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,49 +1,251 @@
1
1
  ---
2
- name: skeleton-automation
3
- description: Generate automation slices from config.json
2
+ name: slice-automation
3
+ description: builds an automation slice from an event model
4
4
  ---
5
5
 
6
- # Generate Automation Slice
6
+ ### Critical understanding
7
7
 
8
- Generate automation slices (background processors with CRON scheduling) from your event model configuration.
8
+ Make sure to read the Agents.md file before building anything.
9
9
 
10
- ## Task
10
+ If the processors-Array is not empty, it´s an automation slice.
11
11
 
12
- You are tasked with generating one or more automation slices using the emmet-supabase Yeoman generator.
12
+ Important - an automation slice is "just" a state-change slice with an additional automation that triggers the command.
13
+ So also read the skill for 'state-change-slice'
13
14
 
14
- ## Steps
15
+ ## Critical Requirements
15
16
 
16
- 1. Check if a `config.json` file exists in the current directory
17
- - If not found, inform the user they need a config.json file
18
- - Exit with instructions on creating one
17
+ ### Restaurant ID Requirement
18
+ - **CRITICAL**: ALL events MUST have `restaurantId` in their metadata (camelCase)
19
+ - **NEVER** use `locationId` or `location_id` - these are outdated and forbidden
20
+ - **CRITICAL**: ALL database tables (including TODO lists) MUST have a `restaurant_id` column (snake_case)
21
+ - This ensures proper multi-tenancy and data isolation
19
22
 
20
- 2. Read the config.json to find available AUTOMATION slices:
21
- - Look for slices with `"sliceType": "AUTOMATION"`
22
- - Extract their IDs and titles
23
+ ## Overview
23
24
 
24
- 3. Show the user available automation slices and ask which ones to generate
25
- - Allow multiple selections
26
- - Or accept slice IDs from the user's original request
25
+ Automations are processes that happen in the background, based on a TODO List.
26
+ TODO Lists are always tables (read models) and each row in the table is a TODO Item.
27
27
 
28
- 4. Run the local generator with selected slices:
29
- ```bash
30
- npx yo ./.claude/skills/gen-skeleton/generators/emmet-supabase/app --action AUTOMATION --slices <slice-id-1>,<slice-id-2>
31
- ```
28
+ The automation is only responsible to:
29
+ 1. Fetch items from the TODO List (read model)
30
+ 2. Fire the command for each item
31
+ 3. Repeat on a schedule using CRON
32
32
 
33
- 5. After generation completes:
34
- - List the files that were created
35
- - Run tests for the generated slices if available
36
- - Explain the CRON configuration if applicable
37
- - Suggest next steps
33
+ ## Architecture Pattern
34
+
35
+ ```
36
+ ┌─────────────────┐
37
+ │ TODO List │ (Read Model - Inbound Dependency)
38
+ │ "items to do" │ Example: "clerks_to_invite", "items_to_fetch"
39
+ └────────┬────────┘
40
+
41
+ │ reads
42
+
43
+ ┌────────▼────────┐
44
+ │ processor.ts │ (CRON scheduled automation)
45
+ │ │ - Fetches TODO items
46
+ │ │ - Fires commands
47
+ └────────┬────────┘
48
+
49
+ │ invokes
50
+
51
+ ┌────────▼────────┐
52
+ │ CommandHandler │ (State-change logic)
53
+ │ Command.ts │ - decide() function
54
+ │ routes.ts │ - evolve() function
55
+ └─────────────────┘
56
+ ```
57
+
58
+ ## Implementation Structure
59
+
60
+ An automation slice consists of:
61
+ 1. **processor.ts** - CRON automation that fetches TODO items and fires commands
62
+ 2. **[Command]Command.ts** - Command handler with decide/evolve logic
63
+ 3. **routes.ts** - HTTP API endpoint for manual command invocation
64
+
65
+ ## Processor Configuration
66
+
67
+ Two patterns exist in the codebase:
68
+
69
+ ### Pattern 1: Using startProcessor helper (Recommended)
70
+
71
+ ```typescript
72
+ import {ProcessorConfig, ProcessorTodoItem, startProcessor} from "../../process/process";
73
+ import {handleYourCommand} from "./YourCommandCommand";
74
+
75
+ export type ItemToProcess = {
76
+ itemId: string,
77
+ // other fields from read model
78
+ }
79
+
80
+ const config: ProcessorConfig = {
81
+ schedule: "*/5 * * * * *", // Every 5 seconds (cron format)
82
+ endpoint: "your-todo-list-collection", // Read model endpoint
83
+ query: {
84
+ "status": "OPEN", // Filter criteria
85
+ "_limit": "1" // Process one at a time
86
+ }
87
+ }
88
+
89
+ const handler = async (item: ItemToProcess & ProcessorTodoItem) => {
90
+ console.log(`Processing item: ${item.itemId}`)
91
+
92
+ try {
93
+ await handleYourCommand(`aggregate-${item.itemId}`, {
94
+ type: "YourCommand",
95
+ data: {
96
+ itemId: item.itemId,
97
+ // map other fields
98
+ },
99
+ metadata: {}
100
+ })
101
+
102
+ console.log(`Successfully processed item: ${item.itemId}`)
103
+ } catch (error) {
104
+ console.error(`Error processing item ${item.itemId}:`, error)
105
+ }
106
+ }
107
+
108
+ export const processor = {
109
+ start: () => {
110
+ console.log("[YourProcessor] Starting processor...")
111
+ startProcessor<ItemToProcess>(config, handler)
112
+ }
113
+ }
114
+ ```
115
+
116
+ ### Pattern 2: Direct Supabase query (Legacy)
117
+
118
+ ```typescript
119
+ import {ProcessorConfig} from "../../process/process";
120
+ import {YourCommand, handleYourCommand} from "./YourCommandCommand";
121
+ import cron from "node-cron";
122
+ import {createServiceClient} from "../../supabase/api";
123
+
124
+ const config: ProcessorConfig = {
125
+ schedule: '*/30 * * * * *', // Every 30 seconds
126
+ endpoint: "your_todo_table", // Supabase table name
127
+ }
128
+
129
+ export const processor = {
130
+ start: () => {
131
+ cron.schedule(config.schedule, async () => {
132
+ console.log("Running process")
133
+ let client = createServiceClient()
134
+ let result = await client.from(config.endpoint).select("*")
135
+
136
+ if (result.count == 0) {
137
+ console.log(`Nothing to do for ${config.endpoint}`)
138
+ return;
139
+ }
140
+
141
+ for (const item of result.data ?? []) {
142
+ const command: YourCommand = {
143
+ type: "YourCommand",
144
+ data: {
145
+ itemId: item.itemId!
146
+ },
147
+ metadata: {}
148
+ }
149
+
150
+ const id = item.itemId
151
+ if (!id) {
152
+ throw `Cannot process Command ${command.type}. No Id available.`
153
+ }
154
+ await handleYourCommand(id, command)
155
+ }
156
+ })
157
+ }
158
+ }
159
+ ```
160
+
161
+ ## CRON Schedule Format
162
+
163
+ ```
164
+ ┌───────────── second (0-59)
165
+ │ ┌─────────── minute (0-59)
166
+ │ │ ┌───────── hour (0-23)
167
+ │ │ │ ┌─────── day of month (1-31)
168
+ │ │ │ │ ┌───── month (1-12)
169
+ │ │ │ │ │ ┌─── day of week (0-7)
170
+ │ │ │ │ │ │
171
+ * * * * * *
172
+ ```
173
+
174
+ Common schedules:
175
+ - `*/5 * * * * *` - Every 5 seconds
176
+ - `*/30 * * * * *` - Every 30 seconds
177
+ - `0 */1 * * * *` - Every minute
178
+ - `0 0 * * * *` - Every hour
179
+
180
+ ## Real Examples from Codebase
181
+
182
+ ### Example 1: ConfirmInvitation (Clerk management)
183
+
184
+ **TODO List Read Model**: `clerks_to_invite` table
185
+ **Automation**: `src/slices/ConfirmInvitation/processor.ts`
186
+ **Command**: `ConfirmInvitationCommand`
187
+ **Event Emitted**: `ClerkInvitationConfirmed`
188
+
189
+ ```typescript
190
+ // processor.ts
191
+ const config: ProcessorConfig = {
192
+ schedule: '*/30 * * * * *',
193
+ endpoint: "clerks_to_invite",
194
+ }
195
+
196
+ // Fetches clerks from TODO list and confirms their invitations
197
+ for (const clerk of result.data ?? []) {
198
+ const command: ConfirmInvitationCommand = {
199
+ type: "ConfirmInvitation",
200
+ data: {
201
+ clerkId: clerk.clerkId!
202
+ },
203
+ metadata: {}
204
+ }
205
+ await handleConfirmInvitation(clerk.clerkId, command)
206
+ }
207
+ ```
208
+
209
+ in each slice folder, generate a file .slice.json
210
+ ```
211
+ {
212
+ "id" : "<slice id>",
213
+ "slice": "<slice title>",
214
+ "context": "<contextx>",
215
+ "link": "https://miro.com/app/board/<board-id>=/?moveToWidget=<slice id>"
216
+ }
217
+ ```
218
+
219
+ ## Key Points
220
+
221
+ 1. **TODO List Naming**: Use format `<things>_to_<action>` (e.g., `clerks_to_invite`, `items_to_fetch`)
222
+ 2. **Read Model Endpoint**: TODO list is accessed via `/api/query/<name>-collection` endpoint
223
+ 3. **Processor Types**: Define TypeScript types for TODO items matching read model structure
224
+ 4. **Error Handling**: Always wrap command execution in try-catch, log errors but continue processing
225
+ 5. **Schedule Wisely**: Balance responsiveness vs system load (30 seconds is common for production)
226
+ 6. **Status Management**: TODO list typically has a `status` field ("OPEN", "PROCESSING", "DONE")
227
+ 7. **Limit Processing**: Use `_limit: "1"` to process one item at a time, preventing concurrent issues
228
+ 8. **Stream Naming**: Use aggregate-based stream names (e.g., `catalogue-${itemId}`)
229
+
230
+ ## Implementation Steps
231
+
232
+ 1. **Read the input JSON** from templates/sample-input.json
233
+ 2. **Create processor.ts** following one of the patterns above
234
+ 3. **Implement state-change slice** using the 'state-change-slice' skill
235
+ - Creates `[Command]Command.ts` with decide/evolve functions
236
+ - No `routes.ts` for Automations.
237
+ 4. **Register processor** in main application startup
238
+ 5. **Ensure TODO List exists** as a read model (separate slice handles this)
239
+
240
+ ## Sample Input Structure
241
+
242
+ Sample input: read 'templates/sample-input.json'
243
+
244
+ The input defines:
245
+ - **processors[]**: Array of automation definitions
246
+ - title: Automation name
247
+ - dependencies: Inbound (TODO list) and Outbound (Command) connections
248
+ - **commands[]**: Commands triggered by the automation
249
+ - **events[]**: Events emitted by the commands
38
250
 
39
- ## Important Notes
40
251
 
41
- - The generator is located in `.claude/skills/gen-skeleton/generators/emmet-supabase`
42
- - Multiple slices can be generated in one command (comma-separated)
43
- - Slice IDs must exactly match those in config.json
44
- - Generated files typically include:
45
- - processor.ts (background automation logic)
46
- - CRON configuration
47
- - Tests
48
- - Automation slices read from TODO lists (work queues) and fire commands on a schedule
49
- - Common use cases: auto-confirm invitations, process checkouts, send notifications
@@ -0,0 +1,531 @@
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
+
108
+ **Structure**:
109
+ ```typescript
110
+ type State = { /* track relevant state */ };
111
+ const initialState = (): State => ({ /* defaults */ });
112
+ const decide = (state: State, cmd: Command): Event[] => {
113
+ if (invalidCondition) throw 'error.code';
114
+ return [createEvent(cmd)];
115
+ };
116
+ const evolve = (state: State, event: ContextEvents): State => {
117
+ switch (event.type) {
118
+ case 'EventType': return { ...state, field: newValue };
119
+ default: return state;
120
+ }
121
+ };
122
+ ```
123
+
124
+ **Error Testing**: Use `.shouldFail()` or `assert.throws()`
125
+
126
+ ### STATE_VIEW Slice Pattern
127
+
128
+ **Key Rules**:
129
+ - Always verify ALL events from slice.json are in `canHandle` array
130
+ - Use specific event union type (e.g., `Event1 | Event2`) NOT `ContextEvents` when handling subset
131
+ - Missing event handlers = incomplete implementation
132
+
133
+ **Projection Structure**:
134
+ ```typescript
135
+ export const ProjectionName = postgreSQLRawSQLProjection({
136
+ canHandle: ['Event1', 'Event2'],
137
+ evolve: (event: Event1 | Event2) => {
138
+ switch (event.type) {
139
+ case 'Event1': return sql(db(table).insert({...}).onConflict().merge());
140
+ case 'Event2': return sql(db(table).where({...}).delete());
141
+ default: return [];
142
+ }
143
+ }
144
+ });
145
+ ```
146
+
147
+ **Event Dependencies**: Look in slice.json → readmodels → dependencies (INBOUND) for event IDs
148
+
149
+ ### AUTOMATION Slice Pattern
150
+
151
+ **Structure**: STATE_CHANGE + processor.ts + work queue projection
152
+
153
+ **Processor Implementation**:
154
+ ```typescript
155
+ import * as cron from 'node-cron';
156
+ import { createServiceClient } from '../../common/supabaseClient';
157
+
158
+ const config = { schedule: '*/30 * * * * *', endpoint: "work_queue_table" };
159
+
160
+ export const processor = {
161
+ start: () => {
162
+ cron.schedule(config.schedule, async () => {
163
+ const client = createServiceClient();
164
+ const result = await client.from(config.endpoint)
165
+ .select("*").eq('must_process', true).limit(1);
166
+
167
+ if (result.error) return;
168
+ for (const item of result.data ?? []) {
169
+ try {
170
+ await handleCommand(streamId, command, { userId: 'system', ...metadata });
171
+ } catch (error) {
172
+ console.error('Processing error:', error);
173
+ }
174
+ }
175
+ });
176
+ }
177
+ };
178
+ ```
179
+
180
+ **Key Rules**:
181
+ - Use `.limit(1)` to process one item at a time
182
+ - `createServiceClient()` bypasses RLS for automation
183
+ - Processor uses snake_case column names (e.g., `reservation_id`)
184
+ - Work queue lifecycle: add on trigger → update flags → delete on completion
185
+
186
+ ## State Tracking Patterns
187
+
188
+ ### Boolean/Toggle State
189
+ ```typescript
190
+ type State = { isActive: boolean };
191
+ const decide = (state: State, cmd: Command): Event[] => {
192
+ if (state.isActive) throw "already.active"; // Activate guard
193
+ if (!state.isActive) throw "not.active"; // Deactivate guard
194
+ return [event];
195
+ };
196
+ ```
197
+
198
+ **Database**: Use flag column (e.g., `is_online_active BOOLEAN`), update with upsert `.onConflict().merge()`
199
+
200
+ ### Set<string> for ID Tracking
201
+ ```typescript
202
+ type State = { trackedIds: Set<string> };
203
+ const evolve = (state: State, event: ContextEvents): State => {
204
+ switch (event.type) {
205
+ case 'ItemAdded': return { trackedIds: new Set([...state.trackedIds, event.data.id]) };
206
+ case 'ItemRemoved':
207
+ const newSet = new Set(state.trackedIds);
208
+ newSet.delete(event.data.id);
209
+ return { trackedIds: newSet };
210
+ }
211
+ };
212
+ ```
213
+
214
+ **Examples**: UnAssign Clerk from Event, Delete Time Entry, Update Time Entry
215
+
216
+ ### Multi-Flag State Machine
217
+ ```typescript
218
+ type State = { submitted: boolean; reverted: boolean; approved: boolean; declined: boolean };
219
+ const decide = (state: State, cmd: Command): Event[] => {
220
+ if (state.submitted) throw 'cannot submit twice';
221
+ if (!state.submitted) throw 'not_submitted';
222
+ if (state.reverted) throw 'already_reverted';
223
+ return [event];
224
+ };
225
+ ```
226
+
227
+ **Workflows**: Timesheet (Submitted → Reverted → Resubmitted), Approval (Approved → Declined → Reapproved)
228
+
229
+ ## Database & Projections
230
+
231
+ ### Migration Rules
232
+ - **NEVER modify existing migrations** - always add new ones
233
+ - Version check: `ls -1 supabase/migrations/ | grep "^V" | sort -V | tail -5`
234
+ - Naming: `V{N}__{table_name}.sql` (e.g., `V41__clerk_details.sql`)
235
+ - PostgreSQL types: Use TEXT/VARCHAR (not "string"), valid UUID format required
236
+ - Composite PKs for multi-tenant: `PRIMARY KEY (restaurant_id, entity_id)`
237
+
238
+ ### Projection Patterns
239
+
240
+ **Add/Remove**:
241
+ ```typescript
242
+ case 'Added': return sql(db(table).insert({...}).onConflict(key).merge());
243
+ case 'Removed': return sql(db(table).where({id}).delete());
244
+ ```
245
+
246
+ **Work Queue Lifecycle**:
247
+ ```typescript
248
+ case 'TriggerEvent': return sql(db(table).insert({id, must_process: false}).onConflict().merge());
249
+ case 'ProgressEvent': return sql(db(table).where({id}).update({must_process: true}));
250
+ case 'CompletionEvent': return sql(db(table).where({id}).delete());
251
+ ```
252
+
253
+ **Conditional Flags**: Set flags based on matching business data (e.g., `must_cancel = true` only when clerkId matches)
254
+
255
+ ### JSONB Field Handling
256
+ - PostgreSQL JSONB auto-parsed by pg driver - DO NOT use `JSON.parse()` in tests
257
+ - Storage: `assignees JSONB DEFAULT '[]'::jsonb`
258
+ - Updates: `assignees: JSON.stringify(data.assignees)` (plain JavaScript, not raw SQL)
259
+ - Array conversion for display: `Array.isArray(field) ? field.join(', ') : field || ''`
260
+
261
+ ### Query Endpoints - Authentication
262
+ All endpoints must:
263
+ 1. Require JWT: `requireUser(req, res, true)`
264
+ 2. Filter by authenticated user's ID
265
+ 3. Use anon key Supabase client
266
+
267
+ ### Date Handling
268
+ - slice.json DateTime → TypeScript `Date` type
269
+ - Routes: `date: new Date(assertNotEmpty(req.body.date))`
270
+ - Tests: Use Date objects: `new Date('2026-03-15T10:00:00Z')`
271
+ - Timezone: Europe/Berlin → UTC conversion (CET = UTC+1, CEST = UTC+2 for DST)
272
+
273
+ ## Test Patterns Reference
274
+
275
+ ### STATE_CHANGE Tests
276
+ ```typescript
277
+ DeciderSpecification.for(decide, evolve, initialState)
278
+ .given([PrerequisiteEvent1, PrerequisiteEvent2])
279
+ .when(Command({ field: 'value' }))
280
+ .then([ExpectedEvent({ field: 'value' })]);
281
+ ```
282
+
283
+ ### STATE_VIEW Tests
284
+ ```typescript
285
+ PostgreSQLProjectionSpec.for(ProjectionName)
286
+ .given([Event1, Event2])
287
+ .when([]) // Always empty
288
+ .then(async (state) => {
289
+ const result = await state.query();
290
+ assert.equal(result[0].field, expectedValue);
291
+ });
292
+ ```
293
+
294
+ ### Error Case Tests
295
+ ```typescript
296
+ .given([EventCreated])
297
+ .when(Command)
298
+ .shouldFail(); // Expects error
299
+ ```
300
+
301
+ ### Test Coverage
302
+ - **STATE_CHANGE**: Happy path + all error guards
303
+ - **STATE_VIEW**: One test per INBOUND event dependency
304
+ - **AUTOMATION**: Command logic + error guards (processor not unit tested)
305
+
306
+ ## Test Specifications & code-slice.json
307
+
308
+ ### Purpose of test-analyzer
309
+ - Analyzes tests to extract behavioral specifications
310
+ - Generates code-slice.json in `.slices/{Context}/{folder}/`
311
+ - Includes only specs NOT in slice.json (drift detection)
312
+
313
+ ### Drift Detection
314
+ **Generate code-slice.json when**:
315
+ - Test specs NOT in slice.json specifications array
316
+ - slice.json has `specifications: []` but tests exist
317
+
318
+ **No code-slice.json when** (No Drift):
319
+ - All test specs already in slice.json
320
+ - Quality indicator: implementation matches design
321
+
322
+ ### linkedId Lookup
323
+ **STATE_CHANGE**: Command/Event IDs from slice.json → commands/events array → id field
324
+ **STATE_VIEW**: Event IDs from slice.json → readmodels → dependencies (INBOUND) → id field
325
+
326
+ ```bash
327
+ # Find slice.json for event source
328
+ find .slices -name "slice.json" -path "*{slicename}*"
329
+
330
+ # Extract event ID (use lowercase title)
331
+ cat path/to/slice.json | jq '.events[] | select(.title == "Event created") | .id'
332
+ ```
333
+
334
+ **Note**: Event titles in slice.json are lowercase ("Event created"), TypeScript types are PascalCase (`EventCreated`)
335
+
336
+ ### Field Type Mapping
337
+ - UUID format → "UUID" type
338
+ - ISO dates → "String" or "Date"
339
+ - Whole numbers → "Integer"
340
+ - Text → "String"
341
+ - Cardinality: "Single" for non-array fields
342
+
343
+ ## Implementation Quality Checklist
344
+
345
+ ### Standard Workflow (All Slices)
346
+ 1. [ ] Build passes: `npm run build`
347
+ 2. [ ] All tests pass: `npm run test`
348
+ 3. [ ] Run test-analyzer skill (if drift detected)
349
+ 4. [ ] Update `.slices/index.json` status to "Done"
350
+ 5. [ ] Update `progress.txt` with iteration summary
351
+ 6. [ ] Update `AGENTS.md` with new learnings
352
+ 7. [ ] Commit with Co-Authored-By line
353
+
354
+ ### Additional: AUTOMATION Slices
355
+ - [ ] Processor.ts with CRON schedule
356
+ - [ ] Routes for manual invocation
357
+ - [ ] Test includes error cases
358
+
359
+ ### Additional: STATE_VIEW Slices
360
+ - [ ] All INBOUND events in canHandle array
361
+ - [ ] Each event has handler in evolve
362
+ - [ ] Test per INBOUND event
363
+ - [ ] Projection registered in `loadPostgresEventstore.ts`
364
+
365
+ ### "To-Do List" STATE_VIEW Pattern
366
+ When a STATE_VIEW acts as a work queue (items appear when created, disappear when processed):
367
+ - Use `insert().onConflict().ignore()` for the trigger event (e.g., RestaurantRegistered)
368
+ - Use `where({id}).delete()` for the completion event (e.g., BucketCreated)
369
+ - Table naming: `slice_<name>` prefix for slice-specific work queue tables
370
+ - The "bucketId" maps to "restaurantId" — both fields stored, one used as PK
371
+
372
+ ### AUTOMATION Processor with External API
373
+ When an AUTOMATION slice's `backendPrompt` specifies an external operation (e.g., "create a bucket using the Supabase storage API"):
374
+ - Perform the external API call **before** firing the command
375
+ - Use `client.storage.createBucket(id)` for Supabase storage bucket creation
376
+ - Name the bucket after the entity identifier (e.g., restaurant ID)
377
+ - On failure, catch per-item and continue (don't stop the entire batch)
378
+ - The command acts as the **confirmation** that the external operation succeeded
379
+
380
+ ### folder Field in slice.json
381
+ When a command/event has a `folder` field with `"mapping": "month"`:
382
+ - The `folder` field's value = the `month` field's value (same value, different semantic purpose)
383
+ - In storage uploads: use `${month}/` as the folder prefix in the file path (e.g., `${month}/${clerkId}-${month}.csv`)
384
+ - `backendPrompt: "use the month as a folder in the bucket"` = upload path `${month}/{filename}` not just `{filename}`
385
+ - Pass `folder` in command data and event data just like other fields
386
+
387
+ ### Additional: Completing Partial Slices
388
+ - [ ] Verify all dependencies from slice.json implemented
389
+ - [ ] Compare canHandle with dependencies
390
+ - [ ] Add missing handlers and tests
391
+
392
+ ### Event Structure Changes
393
+ - [ ] Event definition updated
394
+ - [ ] All command handlers updated
395
+ - [ ] All consuming slice tests updated
396
+ - [ ] Build passes, all tests pass
397
+
398
+ ## Common Patterns by Domain
399
+
400
+ ### Time Entry Domain
401
+
402
+ **Stream ID**: `time-entry-{clerkId}-{month}` (MMYYYY format)
403
+
404
+ **Key Events**: CheckedIn, CheckedOut, TimeEntryAdded, TimesheetSubmitted, SubmissionReverted, TimesheetApproved, TimesheetDeclined, CheckinCancelled
405
+
406
+ **Workflow**:
407
+ 1. CheckIn → ActiveCheckins
408
+ 2. CheckOut → TimeEntryCheckoutsToProcess (work queue)
409
+ 3. ProcessTimeEntryCheckout (automation) → TimeEntryAdded
410
+ 4. SubmitTimeEntry → ActiveCheckinsToCancel (mark for cancellation)
411
+ 5. CancelCheckin (automation) → CheckinCancelled
412
+ 6. SubmitTimesheet → TimesheetSubmitted → SubmissionDate, SubmissionStatus
413
+ 7. ApproveTimesheet → TimesheetApproved → ApprovalDate
414
+ 8. DeclineTimesheet → TimesheetDeclined (allows reapproval)
415
+
416
+ **Hours Calculation**: `(endDate - startDate) ms / (1000*60*60)`, rounded to 0.5
417
+
418
+ **Guard Patterns**:
419
+ - CheckOut: if CheckedOut in stream → throw "no active check"
420
+ - ProcessTimeEntryCheckout: if NO CheckedOut → throw "no confirmed checkout"
421
+ - SubmitTimesheet: if submitted → throw "cannot submit twice"
422
+ - Delete/Update: track Set<string> of entry IDs
423
+ - SubmitTimeEntry: if `approved && !declined` → throw "cannot submit time entry after approval"
424
+ - DeleteTimeentry: if `approved && !declined` → throw "cannot delete after approval"
425
+ - UpdateTimeEntry: if `submitted && !declined` → throw "cannot submit after submission"
426
+
427
+ **Cross-Slice State Rules (Time Entry Approval Guards)**:
428
+ - `TimesheetApproved` event: sets `approved=true, declined=false`
429
+ - `TimesheetDeclined` event: sets `approved=false, declined=true` (resets approval, re-enables submit/delete/update)
430
+ - `SubmissionReverted` event: sets `approved=false` (allows new submissions after revert)
431
+ - Multiple commands track the same approval state: SubmitTimeEntry, DeleteTimeentry, UpdateTimeEntry
432
+ - Decline AFTER approval is valid workflow (and allows subsequent operations)
433
+
434
+ ### Event Assignment Domain
435
+
436
+ **Stream ID**: `{restaurantId}-event-{eventId}` via `eventStreamId()` helper
437
+
438
+ **Key Events**: EventCreated, EventCancelled, EventPlanned, EventReplanned, ClerkAssignedToEvent, ClerkUnassignedFromEvent
439
+
440
+ **Inverse Operations**:
441
+ - Assign/UnAssign share same stream ID
442
+ - Use Set<string> for tracking assigned clerks
443
+ - Assign guard: `if (has(clerkId)) throw "already.assigned"`
444
+ - UnAssign guard: `if (!has(clerkId)) throw "not.assigned"`
445
+
446
+ **Planning**: EventReplanned resets isPlanned to false (allows re-planning)
447
+
448
+ ### Activation/Deactivation Pattern
449
+
450
+ **Complementary Commands**:
451
+ - Both track same boolean state, validate opposite conditions
452
+ - Activate: `if (active) throw "already.active"`
453
+ - Deactivate: `if (!active) throw "not.active"`
454
+ - Shared evolve handles BOTH events
455
+
456
+ **Implementation Order**: events → STATE_VIEW → Activate → Deactivate
457
+
458
+ **Examples**: OnlineReservation, Shift Publication
459
+
460
+ ### Dashboard Read Models
461
+
462
+ **Characteristics**:
463
+ - Handle 4 events: Create, Cancel, Assign, Unassign
464
+ - JSONB assignees array
465
+ - PRIMARY KEY on entity_id
466
+ - Table naming: `active_{entity}_for_dashboard`
467
+
468
+ **Test Pattern**: Create → Assignment/unassignment → Delete
469
+
470
+ ## Common Gotchas
471
+
472
+ ### Hooks Rewrite Files
473
+ - Claude Code hooks extensively rewrite files after save
474
+ - Always read files AFTER hook execution
475
+ - Hooks add restaurantId, userId, enrich ContextEvents.ts
476
+
477
+ ### Auto-Generation Issues
478
+ - Migration files may get auto-numbered incorrectly (V99) - renumber
479
+ - Projection `canHandle` may be incomplete - verify against slice.json
480
+ - Pre-generated routes may use wrong streamId
481
+
482
+ ### TypeScript Issues
483
+ - Don't chain `.given()` directly on `DeciderSpecification.for()`
484
+ - macOS filesystem case-insensitive, TypeScript imports case-sensitive
485
+ - Use specific event union, NOT `ContextEvents` when handling subset
486
+ - Don't add explicit type arguments to `DeciderSpecification.for()`
487
+
488
+ ### State Management
489
+ - Always use explicit `break` in switch statements
490
+ - Tests for state-dependent commands need prerequisite events in given
491
+ - Paired commands share same stream
492
+ - Use proper `initialState` function (not `{}`)
493
+
494
+ ### Knex/Database
495
+ - `.insert({...}).del()` does NOT insert - it deletes
496
+ - Use `.insert().onConflict().merge()` for upserts
497
+ - Use `.where().update()` or `.where().delete()` for modifications
498
+
499
+ ### Specifications
500
+ - Empty `specifications: []` → create tests covering event dependencies
501
+ - Dependency-only events → include in `canHandle` as no-op (return `[]`)
502
+ - Spec assertions may target different read model than slice's own
503
+ - Pre-existing test failures don't block new slice implementation
504
+
505
+ ### Metadata Updates
506
+ - Specs may be added to slice.json AFTER tests implemented
507
+ - Verify executable tests exist before assuming new work needed
508
+ - If tests exist and pass → metadata sync → commit as "chore: sync specifications"
509
+
510
+ ### Rate Limit Recovery
511
+ - Agent context resets after hitting API rate limits ("You've hit your limit")
512
+ - On resume, re-read slice.json and current implementation to verify state before continuing
513
+ - Do not re-implement already-completed files — check for existing files first
514
+
515
+ ## Error Handling
516
+
517
+ - HTTP 409 for conflicts, HTTP 500 for server errors
518
+ - Business-friendly error codes (e.g., "already.active", "not.assigned")
519
+ - German error messages typical in this codebase
520
+ - Check errors most specific to most general (e.g., not_submitted → already_reverted → already_approved)
521
+
522
+ ## UI Documentation (ui-prompt.md)
523
+
524
+ Each STATE_CHANGE slice should include `ui-prompt.md` with:
525
+ 1. Endpoint URL + HTTP method
526
+ 2. Payload example (realistic JSON)
527
+ 3. Required headers (correlation_id, Authorization)
528
+ 4. Response format (success/error)
529
+ 5. Field descriptions + API client code + curl commands
530
+
531
+ For STATE_VIEW: include query endpoint docs + database table definition.
@@ -14,16 +14,17 @@ Build React + TypeScript UI components from slice JSON definitions using establi
14
14
  7. A slice can define additional prompts as codegen/uiPrompt. any additional prompts defined in backend are hints for the implementation of the slice and have to be taken into account. If you use the additional prompt, add a line in progress.txt
15
15
  8. Write a short progress one liner after each step to progress.txt
16
16
  9. Analyze and Implement according to the Rest of the instructions in this file, make use of the skills in the skills directory, but also your previsously collected
17
- knowledge. Make a list TODO list for what needs to be done. Also make sure to adjust the implementation according to the json definition.
17
+ knowledge. Make a list TODO list for what needs to be done. Also make sure to adjust the implementation according to the json definition.
18
18
  10. The slice in the json is always true, the code follows what is defined in the json
19
19
  11. the slice is only 'Done' if APIs are implemented.
20
20
  12. make sure to read the ui-prompt.md in /backend/src/slices/<slice>
21
- 13. Run quality checks ( npm run build, tsc ) - Attention - it´s enough to run the tests for the slice. Do not run all tests.
22
- 15. Update the Slice in the index.json to status 'Done' and remove assignment
23
- 16. If checks pass, commit ALL changes with message: `feat: [Slice Name]` and merge back to main as FF merge ( update
21
+ 13. Place the component where it belongs. If you can´t find a place, add a new page with /debug/<page> to showcase the component.
22
+ 14. Run quality checks ( npm run build, tsc ) - Attention - it´s enough to run the tests for the slice. Do not run all tests.
23
+ 16. Update the Slice in the index.json to status 'Done' and remove assignment
24
+ 17. If checks pass, commit ALL changes with message: `feat: [Slice Name]` and merge back to main as FF merge ( update
24
25
  first )
25
- 17. Append your progress to `progress.txt` after each step in the iteration.
26
- 18. append your new learnings to frontend/AGENTS.md in a compressed form, reusable for future iterations. Only add learnings if they are not already there.
26
+ 18. Append your progress to `progress.txt` after each step in the iteration.
27
+ 19. append your new learnings to frontend/AGENTS.md in a compressed form, reusable for future iterations. Only add learnings if they are not already there.
27
28
  20. Finish the iteration.
28
29
 
29
30
  ---
@@ -208,7 +209,7 @@ export function CreateEventDialog({ open, onOpenChange }) {
208
209
  if (!form.name) return toast.error("Name required");
209
210
  try {
210
211
  await createEvent.mutateAsync(form);
211
- toast.success("Created");
212
+ toast.success("Planned");
212
213
  onOpenChange(false);
213
214
  } catch (err) {
214
215
  toast.error(`Error: ${err.message}`);
File without changes
@@ -23,7 +23,8 @@ const App = () => (
23
23
  <Routes>
24
24
  <Route path="/register" element={<Register/>}/>
25
25
  <Route path="/auth" element={<Auth/>}/>
26
- <Route path="/" element={<ProtectedRoute><Dashboard/></ProtectedRoute>}/>
26
+ {/*<Route path="/" element={<ProtectedRoute><Dashboard/></ProtectedRoute>}/>*/}
27
+ <Route path="/" element={<Dashboard/>}/>
27
28
  <Route path="*" element={<NotFound/>}/>
28
29
  </Routes>
29
30
  </BrowserRouter>
@@ -21,10 +21,10 @@ const AuthContext = createContext<AuthContextType>({
21
21
  export const useAuth = () => useContext(AuthContext);
22
22
 
23
23
  const fetchAssignedTenant = async (userId: string): Promise<string | null> => {
24
- try {
24
+ /*try {
25
25
  const { data, error } = await supabase
26
- .from("slice_assigned_restaurants")
27
- .select("restaurant_id")
26
+ .from("tenant_to_user_mapping")
27
+ .select("tenant_id")
28
28
  .eq("owner_id", userId)
29
29
  .maybeSingle();
30
30
 
@@ -32,11 +32,12 @@ const fetchAssignedTenant = async (userId: string): Promise<string | null> => {
32
32
  console.error("Error fetching assigned tenant:", error);
33
33
  return null;
34
34
  }
35
- return data?.restaurant_id ?? null;
35
+ return data?.tenant_id ?? null;
36
36
  } catch (error) {
37
37
  console.error("Error fetching assigned tenant:", error);
38
38
  return null;
39
- }
39
+ }*/
40
+ return null
40
41
  };
41
42
 
42
43
  export const AuthProvider = ({ children }: { children: ReactNode }) => {
@@ -223,7 +223,7 @@ const server = createServer(async (req, res) => {
223
223
  if (index == -1) {
224
224
  sliceIndices.slices.push(sliceIndex);
225
225
  } else {
226
- sliceIndices.slices[index] = sliceIndex;
226
+ sliceIndices.slices[index] = {...sliceIndices.slices[index], ...sliceIndex};
227
227
  }
228
228
  });
229
229
  writeFileSync(indexFile, JSON.stringify(sliceIndices, null, 2));
@@ -275,6 +275,7 @@ const server = createServer(async (req, res) => {
275
275
  const allSlices = indexData.slices.map(s => ({
276
276
  title: s.slice,
277
277
  status: s.status,
278
+ assignee: s.assignee,
278
279
  id: s.id,
279
280
  specifications: specificationsMap.get(s.id)
280
281
  }));