@holoscript/plugin-restaurant 2.0.1 → 2.0.2

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,13 +1,13 @@
1
1
  {
2
2
  "name": "@holoscript/plugin-restaurant",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "main": "src/index.ts",
5
5
  "peerDependencies": {
6
- "@holoscript/core": "8.0.6"
6
+ "@holoscript/core": ">=8.0.0"
7
7
  },
8
8
  "license": "MIT",
9
9
  "scripts": {
10
10
  "test": "vitest run --passWithNoTests",
11
11
  "test:coverage": "vitest run --coverage --passWithNoTests"
12
12
  }
13
- }
13
+ }
@@ -22,15 +22,15 @@ import {
22
22
  describe('tableAssignment', () => {
23
23
  const tables = [
24
24
  { id: 'T1', capacity: 2, section: 'patio', available: true },
25
- { id: 'T2', capacity: 4, section: 'main', available: true },
26
- { id: 'T3', capacity: 6, section: 'main', available: true },
27
- { id: 'T4', capacity: 8, section: 'bar', available: true },
25
+ { id: 'T2', capacity: 4, section: 'main', available: true },
26
+ { id: 'T3', capacity: 6, section: 'main', available: true },
27
+ { id: 'T4', capacity: 8, section: 'bar', available: true },
28
28
  ];
29
29
 
30
30
  it('party of 2 assigned to smallest fitting table (best-fit)', () => {
31
31
  const parties = [{ id: 'P1', partySize: 2, priorityGuest: false }];
32
32
  const r = tableAssignment(tables, parties);
33
- const assigned = r.assignments.find(a => a.partyId === 'P1');
33
+ const assigned = r.assignments.find((a) => a.partyId === 'P1');
34
34
  expect(assigned!.tableCapacity).toBeGreaterThanOrEqual(2);
35
35
  // Best-fit: should assign T1 (cap=2) over T2 (cap=4)
36
36
  expect(assigned!.tableCapacity).toBe(2);
@@ -39,11 +39,11 @@ describe('tableAssignment', () => {
39
39
  it('priority guests seated before regular guests', () => {
40
40
  const parties = [
41
41
  { id: 'Regular', partySize: 4, priorityGuest: false },
42
- { id: 'VIP', partySize: 4, priorityGuest: true },
42
+ { id: 'VIP', partySize: 4, priorityGuest: true },
43
43
  ];
44
44
  const r = tableAssignment(tables, parties);
45
45
  // VIP should be assigned (both should fit since we have 2 4+ tables)
46
- expect(r.assignments.some(a => a.partyId === 'VIP')).toBe(true);
46
+ expect(r.assignments.some((a) => a.partyId === 'VIP')).toBe(true);
47
47
  });
48
48
 
49
49
  it('party larger than any table goes to unassigned', () => {
@@ -55,14 +55,14 @@ describe('tableAssignment', () => {
55
55
  it('utilization = partySize / tableCapacity', () => {
56
56
  const parties = [{ id: 'P1', partySize: 3, priorityGuest: false }];
57
57
  const r = tableAssignment(tables, parties);
58
- const assigned = r.assignments.find(a => a.partyId === 'P1');
58
+ const assigned = r.assignments.find((a) => a.partyId === 'P1');
59
59
  expect(assigned!.utilization).toBeCloseTo(assigned!.partySize / assigned!.tableCapacity, 4);
60
60
  });
61
61
 
62
62
  it('section preference respected when available', () => {
63
63
  const parties = [{ id: 'P1', partySize: 2, preferredSection: 'patio', priorityGuest: false }];
64
64
  const r = tableAssignment(tables, parties);
65
- const assigned = r.assignments.find(a => a.partyId === 'P1');
65
+ const assigned = r.assignments.find((a) => a.partyId === 'P1');
66
66
  expect(assigned!.sectionMatch).toBe(true);
67
67
  expect(assigned!.tableId).toBe('T1');
68
68
  });
@@ -84,9 +84,9 @@ describe('tableAssignment', () => {
84
84
  describe('kitchenQueueScheduler', () => {
85
85
  const tickets = [
86
86
  { id: 'T1', estimatedMinutes: 15, priority: 2 as const, tableId: 'A', items: ['burger'] },
87
- { id: 'T2', estimatedMinutes: 8, priority: 1 as const, tableId: 'B', items: ['soup'] },
88
- { id: 'T3', estimatedMinutes: 20, priority: 2 as const, tableId: 'C', items: ['steak'] },
89
- { id: 'T4', estimatedMinutes: 5, priority: 3 as const, tableId: 'D', items: ['dessert'] },
87
+ { id: 'T2', estimatedMinutes: 8, priority: 1 as const, tableId: 'B', items: ['soup'] },
88
+ { id: 'T3', estimatedMinutes: 20, priority: 2 as const, tableId: 'C', items: ['steak'] },
89
+ { id: 'T4', estimatedMinutes: 5, priority: 3 as const, tableId: 'D', items: ['dessert'] },
90
90
  ];
91
91
 
92
92
  it('priority 1 ticket scheduled first', () => {
@@ -115,8 +115,8 @@ describe('kitchenQueueScheduler', () => {
115
115
  it('within same priority: shorter job goes first (SJF)', () => {
116
116
  const r = kitchenQueueScheduler(tickets);
117
117
  // Priority 2 tickets: T1 (15min) and T3 (20min) → T1 before T3
118
- const t1pos = r.sequence.findIndex(s => s.ticketId === 'T1');
119
- const t3pos = r.sequence.findIndex(s => s.ticketId === 'T3');
118
+ const t1pos = r.sequence.findIndex((s) => s.ticketId === 'T1');
119
+ const t3pos = r.sequence.findIndex((s) => s.ticketId === 'T3');
120
120
  expect(t1pos).toBeLessThan(t3pos);
121
121
  });
122
122
 
@@ -140,33 +140,33 @@ describe('menuEngineering', () => {
140
140
  * Dogs: low popularity + low margin
141
141
  */
142
142
  const items = [
143
- { id: 'burger', name: 'Burger', popularity: 100, contributionMargin: 12.00 }, // star — avg margin = (12+4+25+3)/4 = 11; 12 > 11 → star
144
- { id: 'pasta', name: 'Pasta', popularity: 80, contributionMargin: 4.00 }, // plow-horse
145
- { id: 'lobster',name: 'Lobster', popularity: 20, contributionMargin: 25.00 }, // puzzle
146
- { id: 'salad', name: 'Salad', popularity: 30, contributionMargin: 3.00 }, // dog
143
+ { id: 'burger', name: 'Burger', popularity: 100, contributionMargin: 12.0 }, // star — avg margin = (12+4+25+3)/4 = 11; 12 > 11 → star
144
+ { id: 'pasta', name: 'Pasta', popularity: 80, contributionMargin: 4.0 }, // plow-horse
145
+ { id: 'lobster', name: 'Lobster', popularity: 20, contributionMargin: 25.0 }, // puzzle
146
+ { id: 'salad', name: 'Salad', popularity: 30, contributionMargin: 3.0 }, // dog
147
147
  ];
148
148
 
149
149
  it('burger (high pop, high margin) → star', () => {
150
150
  const r = menuEngineering(items);
151
- const burger = r.items.find(i => i.id === 'burger');
151
+ const burger = r.items.find((i) => i.id === 'burger');
152
152
  expect(burger!.category).toBe('star');
153
153
  });
154
154
 
155
155
  it('pasta (high pop, low margin) → plow-horse', () => {
156
156
  const r = menuEngineering(items);
157
- const pasta = r.items.find(i => i.id === 'pasta');
157
+ const pasta = r.items.find((i) => i.id === 'pasta');
158
158
  expect(pasta!.category).toBe('plow-horse');
159
159
  });
160
160
 
161
161
  it('lobster (low pop, high margin) → puzzle', () => {
162
162
  const r = menuEngineering(items);
163
- const lobster = r.items.find(i => i.id === 'lobster');
163
+ const lobster = r.items.find((i) => i.id === 'lobster');
164
164
  expect(lobster!.category).toBe('puzzle');
165
165
  });
166
166
 
167
167
  it('salad (low pop, low margin) → dog', () => {
168
168
  const r = menuEngineering(items);
169
- const salad = r.items.find(i => i.id === 'salad');
169
+ const salad = r.items.find((i) => i.id === 'salad');
170
170
  expect(salad!.category).toBe('dog');
171
171
  });
172
172
 
@@ -178,7 +178,11 @@ describe('menuEngineering', () => {
178
178
 
179
179
  it('categoryCount sums to total items', () => {
180
180
  const r = menuEngineering(items);
181
- const total = r.categoryCount.star + r.categoryCount['plow-horse'] + r.categoryCount.puzzle + r.categoryCount.dog;
181
+ const total =
182
+ r.categoryCount.star +
183
+ r.categoryCount['plow-horse'] +
184
+ r.categoryCount.puzzle +
185
+ r.categoryCount.dog;
182
186
  expect(total).toBe(items.length);
183
187
  });
184
188
 
@@ -196,7 +200,7 @@ describe('foodCostAnalysis', () => {
196
200
  */
197
201
  const lines = [
198
202
  { item: 'burger', ingredientCostUSD: 3, sellingPriceUSD: 12, unitsSold: 100 },
199
- { item: 'steak', ingredientCostUSD: 15, sellingPriceUSD: 35, unitsSold: 50 },
203
+ { item: 'steak', ingredientCostUSD: 15, sellingPriceUSD: 35, unitsSold: 50 },
200
204
  ];
201
205
 
202
206
  it('per-item foodCostPct = ingredient / price', () => {
@@ -218,14 +222,16 @@ describe('foodCostAnalysis', () => {
218
222
 
219
223
  it('overTarget=true when blended cost exceeds target', () => {
220
224
  // Steak-heavy menu at 42.9% → over standard 30% target
221
- const heavySteak = [{ item: 'steak', ingredientCostUSD: 15, sellingPriceUSD: 35, unitsSold: 200 }];
222
- const r = foodCostAnalysis(heavySteak, 0.30);
225
+ const heavySteak = [
226
+ { item: 'steak', ingredientCostUSD: 15, sellingPriceUSD: 35, unitsSold: 200 },
227
+ ];
228
+ const r = foodCostAnalysis(heavySteak, 0.3);
223
229
  expect(r.overTarget).toBe(true);
224
230
  });
225
231
 
226
232
  it('overTarget=false when within target', () => {
227
233
  const lowCost = [{ item: 'drink', ingredientCostUSD: 1, sellingPriceUSD: 8, unitsSold: 100 }];
228
- const r = foodCostAnalysis(lowCost, 0.30);
234
+ const r = foodCostAnalysis(lowCost, 0.3);
229
235
  expect(r.overTarget).toBe(false);
230
236
  });
231
237
 
@@ -238,14 +244,14 @@ describe('foodCostAnalysis', () => {
238
244
 
239
245
  describe('turnTimePredictor', () => {
240
246
  it('dinner base is longer than lunch', () => {
241
- const lunch = turnTimePredictor({ partySize: 2, mealPeriod: 'lunch', specialEvent: false });
247
+ const lunch = turnTimePredictor({ partySize: 2, mealPeriod: 'lunch', specialEvent: false });
242
248
  const dinner = turnTimePredictor({ partySize: 2, mealPeriod: 'dinner', specialEvent: false });
243
249
  expect(dinner.predictedTurnMin).toBeGreaterThan(lunch.predictedTurnMin);
244
250
  });
245
251
 
246
252
  it('special event adds to turn time', () => {
247
- const normal = turnTimePredictor({ partySize: 2, mealPeriod: 'dinner', specialEvent: false });
248
- const special = turnTimePredictor({ partySize: 2, mealPeriod: 'dinner', specialEvent: true });
253
+ const normal = turnTimePredictor({ partySize: 2, mealPeriod: 'dinner', specialEvent: false });
254
+ const special = turnTimePredictor({ partySize: 2, mealPeriod: 'dinner', specialEvent: true });
249
255
  expect(special.predictedTurnMin).toBeGreaterThan(normal.predictedTurnMin);
250
256
  });
251
257
 
@@ -262,13 +268,15 @@ describe('turnTimePredictor', () => {
262
268
 
263
269
  it('confidenceInterval spans ±10% of predictedTurnMin', () => {
264
270
  const r = turnTimePredictor({ partySize: 4, mealPeriod: 'dinner', specialEvent: false });
265
- const margin = r.predictedTurnMin * 0.10;
271
+ const margin = r.predictedTurnMin * 0.1;
266
272
  expect(r.confidenceInterval[0]).toBeCloseTo(r.predictedTurnMin - margin, 1);
267
273
  expect(r.confidenceInterval[1]).toBeCloseTo(r.predictedTurnMin + margin, 1);
268
274
  });
269
275
 
270
276
  it('throws for partySize < 1', () => {
271
- expect(() => turnTimePredictor({ partySize: 0, mealPeriod: 'dinner', specialEvent: false })).toThrow();
277
+ expect(() =>
278
+ turnTimePredictor({ partySize: 0, mealPeriod: 'dinner', specialEvent: false })
279
+ ).toThrow();
272
280
  });
273
281
  });
274
282
 
@@ -276,7 +284,9 @@ describe('turnTimePredictor', () => {
276
284
 
277
285
  describe('buildRestaurantReceipt', () => {
278
286
  it('plugin=restaurant and CAEL event correct', () => {
279
- const menu = menuEngineering([{ id: 'burger', name: 'Burger', popularity: 100, contributionMargin: 8 }]);
287
+ const menu = menuEngineering([
288
+ { id: 'burger', name: 'Burger', popularity: 100, contributionMargin: 8 },
289
+ ]);
280
290
  const receipt = buildRestaurantReceipt({ menuEngineering: menu, converged: true });
281
291
  expect(receipt.plugin).toBe('restaurant');
282
292
  expect(receipt.cael.event).toBe('restaurant.operations_analysis');
@@ -284,7 +294,9 @@ describe('buildRestaurantReceipt', () => {
284
294
  });
285
295
 
286
296
  it('accepted=true for efficient operation', () => {
287
- const foodCost = foodCostAnalysis([{ item: 'drink', ingredientCostUSD: 1, sellingPriceUSD: 8, unitsSold: 100 }]);
297
+ const foodCost = foodCostAnalysis([
298
+ { item: 'drink', ingredientCostUSD: 1, sellingPriceUSD: 8, unitsSold: 100 },
299
+ ]);
288
300
  const receipt = buildRestaurantReceipt({ foodCost, converged: true });
289
301
  expect(receipt.acceptance.accepted).toBe(true);
290
302
  });
@@ -292,7 +304,7 @@ describe('buildRestaurantReceipt', () => {
292
304
  it('accepted=false when food cost over target', () => {
293
305
  const foodCost = foodCostAnalysis(
294
306
  [{ item: 'steak', ingredientCostUSD: 15, sellingPriceUSD: 35, unitsSold: 100 }],
295
- 0.30, // 42.9% actual > 30% target
307
+ 0.3 // 42.9% actual > 30% target
296
308
  );
297
309
  const receipt = buildRestaurantReceipt({ foodCost, converged: true });
298
310
  expect(receipt.acceptance.accepted).toBe(false);
package/src/index.ts CHANGED
@@ -1,8 +1,22 @@
1
1
  export * from './restaurantsolver';
2
2
  export { createMenuHandler, type MenuConfig, type MenuItem } from './traits/MenuTrait';
3
- export { createOrderHandler, type OrderConfig, type OrderItem, type OrderStatus } from './traits/OrderTrait';
4
- export { createKitchenDisplayHandler, type KitchenDisplayConfig, type KitchenTicket } from './traits/KitchenDisplayTrait';
5
- export { createTableManagementHandler, type TableManagementConfig, type Table, type TableStatus } from './traits/TableManagementTrait';
3
+ export {
4
+ createOrderHandler,
5
+ type OrderConfig,
6
+ type OrderItem,
7
+ type OrderStatus,
8
+ } from './traits/OrderTrait';
9
+ export {
10
+ createKitchenDisplayHandler,
11
+ type KitchenDisplayConfig,
12
+ type KitchenTicket,
13
+ } from './traits/KitchenDisplayTrait';
14
+ export {
15
+ createTableManagementHandler,
16
+ type TableManagementConfig,
17
+ type Table,
18
+ type TableStatus,
19
+ } from './traits/TableManagementTrait';
6
20
  export * from './traits/types';
7
21
 
8
22
  import { createMenuHandler } from './traits/MenuTrait';
@@ -10,5 +24,14 @@ import { createOrderHandler } from './traits/OrderTrait';
10
24
  import { createKitchenDisplayHandler } from './traits/KitchenDisplayTrait';
11
25
  import { createTableManagementHandler } from './traits/TableManagementTrait';
12
26
 
13
- export const pluginMeta = { name: '@holoscript/plugin-restaurant', version: '1.0.0', traits: ['menu', 'order', 'kitchen_display', 'table_management'] };
14
- export const traitHandlers = [createMenuHandler(), createOrderHandler(), createKitchenDisplayHandler(), createTableManagementHandler()];
27
+ export const pluginMeta = {
28
+ name: '@holoscript/plugin-restaurant',
29
+ version: '1.0.0',
30
+ traits: ['menu', 'order', 'kitchen_display', 'table_management'],
31
+ };
32
+ export const traitHandlers = [
33
+ createMenuHandler(),
34
+ createOrderHandler(),
35
+ createKitchenDisplayHandler(),
36
+ createTableManagementHandler(),
37
+ ];
@@ -64,7 +64,7 @@ export interface KitchenQueueResult {
64
64
  endMin: number;
65
65
  tableId: string;
66
66
  }>;
67
- makespan: number; // total completion time minutes
67
+ makespan: number; // total completion time minutes
68
68
  avgWaitMin: number;
69
69
  avgFlowMin: number;
70
70
  }
@@ -86,8 +86,8 @@ export interface MenuEngineeringResult {
86
86
  name: string;
87
87
  popularity: number;
88
88
  contributionMargin: number;
89
- popularityIndex: number; // vs average popularity
90
- marginIndex: number; // vs average margin
89
+ popularityIndex: number; // vs average popularity
90
+ marginIndex: number; // vs average margin
91
91
  category: MenuCategory;
92
92
  }>;
93
93
  averagePopularity: number;
@@ -134,12 +134,12 @@ export interface RestaurantReceiptOptions {
134
134
  */
135
135
  export function tableAssignment(
136
136
  tables: RestaurantTable[],
137
- parties: PartyRequest[],
137
+ parties: PartyRequest[]
138
138
  ): TableAssignmentResult {
139
139
  if (tables.length === 0) throw new Error('No tables available');
140
140
  if (parties.length === 0) throw new Error('No parties to seat');
141
141
 
142
- const available = tables.map(t => ({ ...t }));
142
+ const available = tables.map((t) => ({ ...t }));
143
143
  const sorted = [...parties].sort((a, b) => {
144
144
  // Priority guests first, then by descending party size
145
145
  if (a.priorityGuest !== b.priorityGuest) return a.priorityGuest ? -1 : 1;
@@ -151,11 +151,16 @@ export function tableAssignment(
151
151
 
152
152
  for (const party of sorted) {
153
153
  // Best-fit: find smallest available table that fits
154
- const candidates = available.filter(t => t.available && t.capacity >= party.partySize);
155
- if (candidates.length === 0) { unassigned.push(party.id); continue; }
154
+ const candidates = available.filter((t) => t.available && t.capacity >= party.partySize);
155
+ if (candidates.length === 0) {
156
+ unassigned.push(party.id);
157
+ continue;
158
+ }
156
159
 
157
160
  // Prefer preferred section, then smallest-capacity table
158
- const sectionMatch = candidates.filter(t => !party.preferredSection || t.section === party.preferredSection);
161
+ const sectionMatch = candidates.filter(
162
+ (t) => !party.preferredSection || t.section === party.preferredSection
163
+ );
159
164
  const pool = sectionMatch.length > 0 ? sectionMatch : candidates;
160
165
  pool.sort((a, b) => a.capacity - b.capacity);
161
166
  const table = pool[0];
@@ -174,7 +179,11 @@ export function tableAssignment(
174
179
  const seatedSeats = assignments.reduce((a, x) => a + x.partySize, 0);
175
180
  const totalSeats = tables.reduce((a, t) => a + t.capacity, 0);
176
181
 
177
- return { assignments, unassigned, overallUtilization: totalSeats > 0 ? seatedSeats / totalSeats : 0 };
182
+ return {
183
+ assignments,
184
+ unassigned,
185
+ overallUtilization: totalSeats > 0 ? seatedSeats / totalSeats : 0,
186
+ };
178
187
  }
179
188
 
180
189
  // ─── Kitchen Queue (SJF with priority) ───────────────────────────────────────
@@ -188,12 +197,13 @@ export function kitchenQueueScheduler(tickets: KitchenTicket[]): KitchenQueueRes
188
197
 
189
198
  // Sort: priority level first, then shortest cook time
190
199
  const sorted = [...tickets].sort((a, b) =>
191
- a.priority !== b.priority ? a.priority - b.priority : a.estimatedMinutes - b.estimatedMinutes,
200
+ a.priority !== b.priority ? a.priority - b.priority : a.estimatedMinutes - b.estimatedMinutes
192
201
  );
193
202
 
194
203
  const sequence: KitchenQueueResult['sequence'] = [];
195
204
  let time = 0;
196
- let totalWait = 0, totalFlow = 0;
205
+ let totalWait = 0,
206
+ totalFlow = 0;
197
207
 
198
208
  for (const ticket of sorted) {
199
209
  const start = time;
@@ -225,47 +235,75 @@ export function menuEngineering(items: MenuItem[]): MenuEngineeringResult {
225
235
  if (items.length === 0) throw new Error('No menu items');
226
236
 
227
237
  const avgPopularity = items.reduce((s, i) => s + i.popularity, 0) / items.length;
228
- const avgMargin = items.reduce((s, i) => s + i.contributionMargin, 0) / items.length;
238
+ const avgMargin = items.reduce((s, i) => s + i.contributionMargin, 0) / items.length;
229
239
 
230
- const analyzed = items.map(item => {
240
+ const analyzed = items.map((item) => {
231
241
  const popularityIndex = item.popularity / avgPopularity;
232
- const marginIndex = item.contributionMargin / avgMargin;
242
+ const marginIndex = item.contributionMargin / avgMargin;
233
243
  const category: MenuCategory =
234
- popularityIndex >= 1 && marginIndex >= 1 ? 'star' :
235
- popularityIndex >= 1 && marginIndex < 1 ? 'plow-horse' :
236
- popularityIndex < 1 && marginIndex >= 1 ? 'puzzle' : 'dog';
237
- return { id: item.id, name: item.name, popularity: item.popularity, contributionMargin: item.contributionMargin, popularityIndex, marginIndex, category };
244
+ popularityIndex >= 1 && marginIndex >= 1
245
+ ? 'star'
246
+ : popularityIndex >= 1 && marginIndex < 1
247
+ ? 'plow-horse'
248
+ : popularityIndex < 1 && marginIndex >= 1
249
+ ? 'puzzle'
250
+ : 'dog';
251
+ return {
252
+ id: item.id,
253
+ name: item.name,
254
+ popularity: item.popularity,
255
+ contributionMargin: item.contributionMargin,
256
+ popularityIndex,
257
+ marginIndex,
258
+ category,
259
+ };
238
260
  });
239
261
 
240
- const categoryCount: Record<MenuCategory, number> = { star: 0, 'plow-horse': 0, puzzle: 0, dog: 0 };
262
+ const categoryCount: Record<MenuCategory, number> = {
263
+ star: 0,
264
+ 'plow-horse': 0,
265
+ puzzle: 0,
266
+ dog: 0,
267
+ };
241
268
  for (const a of analyzed) categoryCount[a.category]++;
242
269
 
243
- return { items: analyzed, averagePopularity: avgPopularity, averageMargin: avgMargin, categoryCount };
270
+ return {
271
+ items: analyzed,
272
+ averagePopularity: avgPopularity,
273
+ averageMargin: avgMargin,
274
+ categoryCount,
275
+ };
244
276
  }
245
277
 
246
278
  // ─── Food Cost Optimizer ──────────────────────────────────────────────────────
247
279
 
248
- export function foodCostAnalysis(
249
- lines: FoodCostLine[],
250
- targetFoodCostPct = 0.30,
251
- ): FoodCostResult {
280
+ export function foodCostAnalysis(lines: FoodCostLine[], targetFoodCostPct = 0.3): FoodCostResult {
252
281
  if (lines.length === 0) throw new Error('No food cost lines');
253
- if (targetFoodCostPct <= 0 || targetFoodCostPct >= 1) throw new Error('targetFoodCostPct must be in (0,1)');
282
+ if (targetFoodCostPct <= 0 || targetFoodCostPct >= 1)
283
+ throw new Error('targetFoodCostPct must be in (0,1)');
254
284
 
255
- const analyzed = lines.map(l => {
256
- const revenueUSD = l.sellingPriceUSD * l.unitsSold;
257
- const costUSD = l.ingredientCostUSD * l.unitsSold;
285
+ const analyzed = lines.map((l) => {
286
+ const revenueUSD = l.sellingPriceUSD * l.unitsSold;
287
+ const costUSD = l.ingredientCostUSD * l.unitsSold;
258
288
  const grossProfitUSD = revenueUSD - costUSD;
259
- const foodCostPct = l.sellingPriceUSD > 0 ? l.ingredientCostUSD / l.sellingPriceUSD : 0;
289
+ const foodCostPct = l.sellingPriceUSD > 0 ? l.ingredientCostUSD / l.sellingPriceUSD : 0;
260
290
  return { ...l, foodCostPct, revenueUSD, costUSD, grossProfitUSD };
261
291
  });
262
292
 
263
293
  const totalRevenue = analyzed.reduce((s, l) => s + l.revenueUSD, 0);
264
- const totalCost = analyzed.reduce((s, l) => s + l.costUSD, 0);
294
+ const totalCost = analyzed.reduce((s, l) => s + l.costUSD, 0);
265
295
  const blendedFoodCostPct = totalRevenue > 0 ? totalCost / totalRevenue : 0;
266
296
  const variance = blendedFoodCostPct - targetFoodCostPct;
267
297
 
268
- return { lines: analyzed, totalRevenue, totalCost, blendedFoodCostPct, targetFoodCostPct, variance, overTarget: variance > 0 };
298
+ return {
299
+ lines: analyzed,
300
+ totalRevenue,
301
+ totalCost,
302
+ blendedFoodCostPct,
303
+ targetFoodCostPct,
304
+ variance,
305
+ overTarget: variance > 0,
306
+ };
269
307
  }
270
308
 
271
309
  // ─── Turn Time Predictor ──────────────────────────────────────────────────────
@@ -296,11 +334,14 @@ export function turnTimePredictor(input: TurnTimePredictorInput): TurnTimeResult
296
334
 
297
335
  const periodBase = input.mealPeriod === 'lunch' ? 45 : input.mealPeriod === 'brunch' ? 55 : 75;
298
336
  const predictedTurnMin = periodBase + 5 * input.partySize + (input.specialEvent ? 15 : 0);
299
- const turnsPerEvening = 480 / predictedTurnMin; // 8hr service / turn time
337
+ const turnsPerEvening = 480 / predictedTurnMin; // 8hr service / turn time
300
338
 
301
339
  // ±10% CI (simplified residual from regression)
302
- const margin = predictedTurnMin * 0.10;
303
- const confidenceInterval: [number, number] = [predictedTurnMin - margin, predictedTurnMin + margin];
340
+ const margin = predictedTurnMin * 0.1;
341
+ const confidenceInterval: [number, number] = [
342
+ predictedTurnMin - margin,
343
+ predictedTurnMin + margin,
344
+ ];
304
345
 
305
346
  return { predictedTurnMin, turnsPerEvening, confidenceInterval };
306
347
  }
@@ -318,18 +359,27 @@ export interface RestaurantAnalysisResult {
318
359
 
319
360
  export function buildRestaurantReceipt(
320
361
  result: RestaurantAnalysisResult,
321
- options?: RestaurantReceiptOptions,
362
+ options?: RestaurantReceiptOptions
322
363
  ): DomainSimulationReceipt {
323
364
  const violations: Array<{ criterion: string; message: string }> = [];
324
365
 
325
366
  if (result.tableAssign && result.tableAssign.unassigned.length > 0) {
326
- violations.push({ criterion: 'unseated_parties', message: `${result.tableAssign.unassigned.length} party/parties could not be seated` });
367
+ violations.push({
368
+ criterion: 'unseated_parties',
369
+ message: `${result.tableAssign.unassigned.length} party/parties could not be seated`,
370
+ });
327
371
  }
328
372
  if (result.foodCost?.overTarget) {
329
- violations.push({ criterion: 'food_cost', message: `Blended food cost ${(result.foodCost.blendedFoodCostPct * 100).toFixed(1)}% exceeds ${(result.foodCost.targetFoodCostPct * 100).toFixed(0)}% target` });
373
+ violations.push({
374
+ criterion: 'food_cost',
375
+ message: `Blended food cost ${(result.foodCost.blendedFoodCostPct * 100).toFixed(1)}% exceeds ${(result.foodCost.targetFoodCostPct * 100).toFixed(0)}% target`,
376
+ });
330
377
  }
331
378
  if (result.queue && result.queue.makespan > 60) {
332
- violations.push({ criterion: 'kitchen_throughput', message: `Kitchen makespan ${result.queue.makespan.toFixed(0)} min exceeds 60 min service standard` });
379
+ violations.push({
380
+ criterion: 'kitchen_throughput',
381
+ message: `Kitchen makespan ${result.queue.makespan.toFixed(0)} min exceeds 60 min service standard`,
382
+ });
333
383
  }
334
384
 
335
385
  return buildDomainSimulationReceipt({
@@ -344,7 +394,11 @@ export function buildRestaurantReceipt(
344
394
  blendedFoodCostPct: result.foodCost?.blendedFoodCostPct ?? null,
345
395
  menuStarCount: result.menuEngineering?.categoryCount.star ?? null,
346
396
  },
347
- cael: { version: 'cael.v1', event: 'restaurant.operations_analysis', solverType: 'restaurant.table-assignment' },
397
+ cael: {
398
+ version: 'cael.v1',
399
+ event: 'restaurant.operations_analysis',
400
+ solverType: 'restaurant.table-assignment',
401
+ },
348
402
  acceptance: { accepted: violations.length === 0, violations },
349
403
  });
350
404
  }
@@ -1,31 +1,73 @@
1
1
  /** @kitchen_display Trait — Kitchen display system (KDS). @trait kitchen_display */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export interface KitchenTicket { orderId: string; items: string[]; priority: 'normal' | 'rush' | 'vip'; receivedAt: number; startedAt: number | null; completedAt: number | null; }
5
- export interface KitchenDisplayConfig { stations: string[]; maxActiveTickets: number; targetPrepTimeMin: number; }
6
- export interface KitchenDisplayState { activeTickets: KitchenTicket[]; completedToday: number; avgPrepTimeMin: number; }
4
+ export interface KitchenTicket {
5
+ orderId: string;
6
+ items: string[];
7
+ priority: 'normal' | 'rush' | 'vip';
8
+ receivedAt: number;
9
+ startedAt: number | null;
10
+ completedAt: number | null;
11
+ }
12
+ export interface KitchenDisplayConfig {
13
+ stations: string[];
14
+ maxActiveTickets: number;
15
+ targetPrepTimeMin: number;
16
+ }
17
+ export interface KitchenDisplayState {
18
+ activeTickets: KitchenTicket[];
19
+ completedToday: number;
20
+ avgPrepTimeMin: number;
21
+ }
7
22
 
8
- const defaultConfig: KitchenDisplayConfig = { stations: ['grill', 'salad', 'dessert'], maxActiveTickets: 20, targetPrepTimeMin: 15 };
23
+ const defaultConfig: KitchenDisplayConfig = {
24
+ stations: ['grill', 'salad', 'dessert'],
25
+ maxActiveTickets: 20,
26
+ targetPrepTimeMin: 15,
27
+ };
9
28
 
10
29
  export function createKitchenDisplayHandler(): TraitHandler<KitchenDisplayConfig> {
11
- return { name: 'kitchen_display', defaultConfig,
12
- onAttach(n: HSPlusNode, _c: KitchenDisplayConfig, ctx: TraitContext) { n.__kdsState = { activeTickets: [], completedToday: 0, avgPrepTimeMin: 0 }; ctx.emit?.('kds:online'); },
13
- onDetach(n: HSPlusNode, _c: KitchenDisplayConfig, ctx: TraitContext) { delete n.__kdsState; ctx.emit?.('kds:offline'); },
30
+ return {
31
+ name: 'kitchen_display',
32
+ defaultConfig,
33
+ onAttach(n: HSPlusNode, _c: KitchenDisplayConfig, ctx: TraitContext) {
34
+ n.__kdsState = { activeTickets: [], completedToday: 0, avgPrepTimeMin: 0 };
35
+ ctx.emit?.('kds:online');
36
+ },
37
+ onDetach(n: HSPlusNode, _c: KitchenDisplayConfig, ctx: TraitContext) {
38
+ delete n.__kdsState;
39
+ ctx.emit?.('kds:offline');
40
+ },
14
41
  onUpdate() {},
15
42
  onEvent(n: HSPlusNode, c: KitchenDisplayConfig, ctx: TraitContext, e: TraitEvent) {
16
- const s = n.__kdsState as KitchenDisplayState | undefined; if (!s) return;
43
+ const s = n.__kdsState as KitchenDisplayState | undefined;
44
+ if (!s) return;
17
45
  if (e.type === 'kds:new_ticket') {
18
- const ticket: KitchenTicket = { orderId: (e.payload?.orderId as string) ?? '', items: (e.payload?.items as string[]) ?? [], priority: (e.payload?.priority as KitchenTicket['priority']) ?? 'normal', receivedAt: Date.now(), startedAt: null, completedAt: null };
19
- if (s.activeTickets.length < c.maxActiveTickets) { s.activeTickets.push(ticket); ctx.emit?.('kds:ticket_received', { orderId: ticket.orderId }); }
20
- else ctx.emit?.('kds:queue_full');
46
+ const ticket: KitchenTicket = {
47
+ orderId: (e.payload?.orderId as string) ?? '',
48
+ items: (e.payload?.items as string[]) ?? [],
49
+ priority: (e.payload?.priority as KitchenTicket['priority']) ?? 'normal',
50
+ receivedAt: Date.now(),
51
+ startedAt: null,
52
+ completedAt: null,
53
+ };
54
+ if (s.activeTickets.length < c.maxActiveTickets) {
55
+ s.activeTickets.push(ticket);
56
+ ctx.emit?.('kds:ticket_received', { orderId: ticket.orderId });
57
+ } else ctx.emit?.('kds:queue_full');
21
58
  }
22
59
  if (e.type === 'kds:complete_ticket') {
23
- const idx = s.activeTickets.findIndex(t => t.orderId === (e.payload?.orderId as string));
24
- if (idx >= 0) { const t = s.activeTickets.splice(idx, 1)[0]; t.completedAt = Date.now(); s.completedToday++;
60
+ const idx = s.activeTickets.findIndex((t) => t.orderId === (e.payload?.orderId as string));
61
+ if (idx >= 0) {
62
+ const t = s.activeTickets.splice(idx, 1)[0];
63
+ t.completedAt = Date.now();
64
+ s.completedToday++;
25
65
  const prepTime = (t.completedAt - t.receivedAt) / 60000;
26
- s.avgPrepTimeMin = (s.avgPrepTimeMin * (s.completedToday - 1) + prepTime) / s.completedToday;
66
+ s.avgPrepTimeMin =
67
+ (s.avgPrepTimeMin * (s.completedToday - 1) + prepTime) / s.completedToday;
27
68
  ctx.emit?.('kds:ticket_done', { orderId: t.orderId, prepMin: Math.round(prepTime) });
28
- if (prepTime > c.targetPrepTimeMin) ctx.emit?.('kds:slow_ticket', { orderId: t.orderId, prepMin: Math.round(prepTime) });
69
+ if (prepTime > c.targetPrepTimeMin)
70
+ ctx.emit?.('kds:slow_ticket', { orderId: t.orderId, prepMin: Math.round(prepTime) });
29
71
  }
30
72
  }
31
73
  },
@@ -1,19 +1,66 @@
1
1
  /** @menu Trait — Restaurant menu management. @trait menu */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export interface MenuItem { id: string; name: string; price: number; category: string; description: string; allergens: string[]; calories?: number; available: boolean; }
5
- export interface MenuConfig { items: MenuItem[]; currency: string; taxRate: number; serviceChargePercent: number; }
4
+ export interface MenuItem {
5
+ id: string;
6
+ name: string;
7
+ price: number;
8
+ category: string;
9
+ description: string;
10
+ allergens: string[];
11
+ calories?: number;
12
+ available: boolean;
13
+ }
14
+ export interface MenuConfig {
15
+ items: MenuItem[];
16
+ currency: string;
17
+ taxRate: number;
18
+ serviceChargePercent: number;
19
+ }
6
20
 
7
- const defaultConfig: MenuConfig = { items: [], currency: 'USD', taxRate: 0.08, serviceChargePercent: 0 };
21
+ const defaultConfig: MenuConfig = {
22
+ items: [],
23
+ currency: 'USD',
24
+ taxRate: 0.08,
25
+ serviceChargePercent: 0,
26
+ };
8
27
 
9
28
  export function createMenuHandler(): TraitHandler<MenuConfig> {
10
- return { name: 'menu', defaultConfig,
11
- onAttach(n: HSPlusNode, c: MenuConfig, ctx: TraitContext) { n.__menuState = { availableItems: c.items.filter(i => i.available).length, categories: [...new Set(c.items.map(i => i.category))] }; ctx.emit?.('menu:loaded', { items: c.items.length }); },
12
- onDetach(n: HSPlusNode, _c: MenuConfig, ctx: TraitContext) { delete n.__menuState; ctx.emit?.('menu:unloaded'); },
29
+ return {
30
+ name: 'menu',
31
+ defaultConfig,
32
+ onAttach(n: HSPlusNode, c: MenuConfig, ctx: TraitContext) {
33
+ n.__menuState = {
34
+ availableItems: c.items.filter((i) => i.available).length,
35
+ categories: [...new Set(c.items.map((i) => i.category))],
36
+ };
37
+ ctx.emit?.('menu:loaded', { items: c.items.length });
38
+ },
39
+ onDetach(n: HSPlusNode, _c: MenuConfig, ctx: TraitContext) {
40
+ delete n.__menuState;
41
+ ctx.emit?.('menu:unloaded');
42
+ },
13
43
  onUpdate() {},
14
44
  onEvent(_n: HSPlusNode, c: MenuConfig, ctx: TraitContext, e: TraitEvent) {
15
- if (e.type === 'menu:search') { const q = ((e.payload?.query as string) ?? '').toLowerCase(); const results = c.items.filter(i => i.available && (i.name.toLowerCase().includes(q) || i.category.toLowerCase().includes(q))); ctx.emit?.('menu:results', { count: results.length, items: results.map(r => ({ id: r.id, name: r.name, price: r.price })) }); }
16
- if (e.type === 'menu:toggle_availability') { const item = c.items.find(i => i.id === (e.payload?.itemId as string)); if (item) { item.available = !item.available; ctx.emit?.('menu:availability_changed', { item: item.name, available: item.available }); } }
45
+ if (e.type === 'menu:search') {
46
+ const q = ((e.payload?.query as string) ?? '').toLowerCase();
47
+ const results = c.items.filter(
48
+ (i) =>
49
+ i.available &&
50
+ (i.name.toLowerCase().includes(q) || i.category.toLowerCase().includes(q))
51
+ );
52
+ ctx.emit?.('menu:results', {
53
+ count: results.length,
54
+ items: results.map((r) => ({ id: r.id, name: r.name, price: r.price })),
55
+ });
56
+ }
57
+ if (e.type === 'menu:toggle_availability') {
58
+ const item = c.items.find((i) => i.id === (e.payload?.itemId as string));
59
+ if (item) {
60
+ item.available = !item.available;
61
+ ctx.emit?.('menu:availability_changed', { item: item.name, available: item.available });
62
+ }
63
+ }
17
64
  },
18
65
  };
19
66
  }
@@ -1,27 +1,69 @@
1
1
  /** @order Trait — Customer order lifecycle. @trait order */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export type OrderStatus = 'new' | 'confirmed' | 'preparing' | 'ready' | 'served' | 'paid' | 'cancelled';
5
- export interface OrderItem { menuItemId: string; name: string; quantity: number; price: number; modifications?: string[]; }
6
- export interface OrderConfig { tableNumber: number; items: OrderItem[]; orderType: 'dine_in' | 'takeout' | 'delivery'; }
7
- export interface OrderState { status: OrderStatus; orderId: string; subtotal: number; createdAt: number; }
4
+ export type OrderStatus =
5
+ | 'new'
6
+ | 'confirmed'
7
+ | 'preparing'
8
+ | 'ready'
9
+ | 'served'
10
+ | 'paid'
11
+ | 'cancelled';
12
+ export interface OrderItem {
13
+ menuItemId: string;
14
+ name: string;
15
+ quantity: number;
16
+ price: number;
17
+ modifications?: string[];
18
+ }
19
+ export interface OrderConfig {
20
+ tableNumber: number;
21
+ items: OrderItem[];
22
+ orderType: 'dine_in' | 'takeout' | 'delivery';
23
+ }
24
+ export interface OrderState {
25
+ status: OrderStatus;
26
+ orderId: string;
27
+ subtotal: number;
28
+ createdAt: number;
29
+ }
8
30
 
9
31
  const defaultConfig: OrderConfig = { tableNumber: 0, items: [], orderType: 'dine_in' };
10
32
 
11
33
  export function createOrderHandler(): TraitHandler<OrderConfig> {
12
- return { name: 'order', defaultConfig,
34
+ return {
35
+ name: 'order',
36
+ defaultConfig,
13
37
  onAttach(n: HSPlusNode, c: OrderConfig, ctx: TraitContext) {
14
38
  const subtotal = c.items.reduce((s, i) => s + i.price * i.quantity, 0);
15
- n.__orderState = { status: 'new' as OrderStatus, orderId: `ORD-${Date.now()}`, subtotal, createdAt: Date.now() };
39
+ n.__orderState = {
40
+ status: 'new' as OrderStatus,
41
+ orderId: `ORD-${Date.now()}`,
42
+ subtotal,
43
+ createdAt: Date.now(),
44
+ };
16
45
  ctx.emit?.('order:created', { table: c.tableNumber, items: c.items.length, subtotal });
17
46
  },
18
- onDetach(n: HSPlusNode, _c: OrderConfig, ctx: TraitContext) { delete n.__orderState; ctx.emit?.('order:removed'); },
47
+ onDetach(n: HSPlusNode, _c: OrderConfig, ctx: TraitContext) {
48
+ delete n.__orderState;
49
+ ctx.emit?.('order:removed');
50
+ },
19
51
  onUpdate() {},
20
52
  onEvent(n: HSPlusNode, _c: OrderConfig, ctx: TraitContext, e: TraitEvent) {
21
- const s = n.__orderState as OrderState | undefined; if (!s) return;
22
- const flow: OrderStatus[] = ['new','confirmed','preparing','ready','served','paid'];
23
- if (e.type === 'order:advance') { const i = flow.indexOf(s.status); if (i < flow.length - 1) { s.status = flow[i+1]; ctx.emit?.('order:status_changed', { status: s.status, orderId: s.orderId }); } }
24
- if (e.type === 'order:cancel') { s.status = 'cancelled'; ctx.emit?.('order:cancelled', { orderId: s.orderId }); }
53
+ const s = n.__orderState as OrderState | undefined;
54
+ if (!s) return;
55
+ const flow: OrderStatus[] = ['new', 'confirmed', 'preparing', 'ready', 'served', 'paid'];
56
+ if (e.type === 'order:advance') {
57
+ const i = flow.indexOf(s.status);
58
+ if (i < flow.length - 1) {
59
+ s.status = flow[i + 1];
60
+ ctx.emit?.('order:status_changed', { status: s.status, orderId: s.orderId });
61
+ }
62
+ }
63
+ if (e.type === 'order:cancel') {
64
+ s.status = 'cancelled';
65
+ ctx.emit?.('order:cancelled', { orderId: s.orderId });
66
+ }
25
67
  },
26
68
  };
27
69
  }
@@ -2,34 +2,81 @@
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
4
  export type TableStatus = 'available' | 'reserved' | 'occupied' | 'cleaning' | 'blocked';
5
- export interface Table { id: string; seats: number; section: string; status: TableStatus; partySize?: number; seatedAt?: number; }
6
- export interface TableManagementConfig { tables: Table[]; turnoverTargetMin: number; waitlistEnabled: boolean; }
7
- export interface TableManagementState { availableCount: number; occupiedCount: number; waitlistSize: number; avgTurnoverMin: number; }
5
+ export interface Table {
6
+ id: string;
7
+ seats: number;
8
+ section: string;
9
+ status: TableStatus;
10
+ partySize?: number;
11
+ seatedAt?: number;
12
+ }
13
+ export interface TableManagementConfig {
14
+ tables: Table[];
15
+ turnoverTargetMin: number;
16
+ waitlistEnabled: boolean;
17
+ }
18
+ export interface TableManagementState {
19
+ availableCount: number;
20
+ occupiedCount: number;
21
+ waitlistSize: number;
22
+ avgTurnoverMin: number;
23
+ }
8
24
 
9
- const defaultConfig: TableManagementConfig = { tables: [], turnoverTargetMin: 60, waitlistEnabled: true };
25
+ const defaultConfig: TableManagementConfig = {
26
+ tables: [],
27
+ turnoverTargetMin: 60,
28
+ waitlistEnabled: true,
29
+ };
10
30
 
11
31
  export function createTableManagementHandler(): TraitHandler<TableManagementConfig> {
12
- return { name: 'table_management', defaultConfig,
32
+ return {
33
+ name: 'table_management',
34
+ defaultConfig,
13
35
  onAttach(n: HSPlusNode, c: TableManagementConfig, ctx: TraitContext) {
14
- const avail = c.tables.filter(t => t.status === 'available').length;
15
- n.__tableState = { availableCount: avail, occupiedCount: c.tables.length - avail, waitlistSize: 0, avgTurnoverMin: 0 };
36
+ const avail = c.tables.filter((t) => t.status === 'available').length;
37
+ n.__tableState = {
38
+ availableCount: avail,
39
+ occupiedCount: c.tables.length - avail,
40
+ waitlistSize: 0,
41
+ avgTurnoverMin: 0,
42
+ };
16
43
  ctx.emit?.('tables:initialized', { total: c.tables.length, available: avail });
17
44
  },
18
- onDetach(n: HSPlusNode, _c: TableManagementConfig, ctx: TraitContext) { delete n.__tableState; ctx.emit?.('tables:closed'); },
45
+ onDetach(n: HSPlusNode, _c: TableManagementConfig, ctx: TraitContext) {
46
+ delete n.__tableState;
47
+ ctx.emit?.('tables:closed');
48
+ },
19
49
  onUpdate() {},
20
50
  onEvent(n: HSPlusNode, c: TableManagementConfig, ctx: TraitContext, e: TraitEvent) {
21
- const s = n.__tableState as TableManagementState | undefined; if (!s) return;
51
+ const s = n.__tableState as TableManagementState | undefined;
52
+ if (!s) return;
22
53
  if (e.type === 'tables:seat') {
23
- const tableId = e.payload?.tableId as string; const party = (e.payload?.partySize as number) ?? 1;
24
- const table = c.tables.find(t => t.id === tableId);
25
- if (table && table.status === 'available') { table.status = 'occupied'; table.partySize = party; table.seatedAt = Date.now(); s.availableCount--; s.occupiedCount++; ctx.emit?.('tables:seated', { table: tableId, party }); }
54
+ const tableId = e.payload?.tableId as string;
55
+ const party = (e.payload?.partySize as number) ?? 1;
56
+ const table = c.tables.find((t) => t.id === tableId);
57
+ if (table && table.status === 'available') {
58
+ table.status = 'occupied';
59
+ table.partySize = party;
60
+ table.seatedAt = Date.now();
61
+ s.availableCount--;
62
+ s.occupiedCount++;
63
+ ctx.emit?.('tables:seated', { table: tableId, party });
64
+ }
26
65
  }
27
66
  if (e.type === 'tables:clear') {
28
67
  const tableId = e.payload?.tableId as string;
29
- const table = c.tables.find(t => t.id === tableId);
30
- if (table && table.status === 'occupied') { table.status = 'available'; s.availableCount++; s.occupiedCount--; ctx.emit?.('tables:cleared', { table: tableId }); }
68
+ const table = c.tables.find((t) => t.id === tableId);
69
+ if (table && table.status === 'occupied') {
70
+ table.status = 'available';
71
+ s.availableCount++;
72
+ s.occupiedCount--;
73
+ ctx.emit?.('tables:cleared', { table: tableId });
74
+ }
75
+ }
76
+ if (e.type === 'tables:add_waitlist') {
77
+ s.waitlistSize++;
78
+ ctx.emit?.('tables:waitlist_updated', { size: s.waitlistSize });
31
79
  }
32
- if (e.type === 'tables:add_waitlist') { s.waitlistSize++; ctx.emit?.('tables:waitlist_updated', { size: s.waitlistSize }); }
33
80
  },
34
81
  };
35
82
  }
@@ -1,4 +1,22 @@
1
- export interface HSPlusNode { id?: string; properties?: Record<string, unknown>; [key: string]: unknown; }
2
- export interface TraitContext { emit?: (event: string, payload?: unknown) => void; [key: string]: unknown; }
3
- export interface TraitEvent { type: string; payload?: Record<string, unknown>; [key: string]: unknown; }
4
- export interface TraitHandler<T = unknown> { name: string; defaultConfig: T; onAttach(n: HSPlusNode, c: T, ctx: TraitContext): void; onDetach(n: HSPlusNode, c: T, ctx: TraitContext): void; onUpdate(n: HSPlusNode, c: T, ctx: TraitContext, d: number): void; onEvent(n: HSPlusNode, c: T, ctx: TraitContext, e: TraitEvent): void; }
1
+ export interface HSPlusNode {
2
+ id?: string;
3
+ properties?: Record<string, unknown>;
4
+ [key: string]: unknown;
5
+ }
6
+ export interface TraitContext {
7
+ emit?: (event: string, payload?: unknown) => void;
8
+ [key: string]: unknown;
9
+ }
10
+ export interface TraitEvent {
11
+ type: string;
12
+ payload?: Record<string, unknown>;
13
+ [key: string]: unknown;
14
+ }
15
+ export interface TraitHandler<T = unknown> {
16
+ name: string;
17
+ defaultConfig: T;
18
+ onAttach(n: HSPlusNode, c: T, ctx: TraitContext): void;
19
+ onDetach(n: HSPlusNode, c: T, ctx: TraitContext): void;
20
+ onUpdate(n: HSPlusNode, c: T, ctx: TraitContext, d: number): void;
21
+ onEvent(n: HSPlusNode, c: T, ctx: TraitContext, e: TraitEvent): void;
22
+ }
package/tsconfig.json CHANGED
@@ -1 +1,5 @@
1
- { "extends": "../../../tsconfig.json", "compilerOptions": { "outDir": "dist", "rootDir": "src", "declaration": true }, "include": ["src"] }
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": { "outDir": "dist", "rootDir": "src", "declaration": true },
4
+ "include": ["src"]
5
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025-2026 HoloScript Contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.