@holoscript/plugin-travel-hospitality 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-travel-hospitality",
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
+ }
@@ -66,9 +66,9 @@ describe('emsrYieldManagement', () => {
66
66
 
67
67
  it('3-class EMSR returns correct array lengths', () => {
68
68
  const threeClass: RateClass[] = [
69
- { id: 'Suite', adr: 400, expectedDemand: 20, demandStdDev: 8 },
69
+ { id: 'Suite', adr: 400, expectedDemand: 20, demandStdDev: 8 },
70
70
  { id: 'Standard', adr: 200, expectedDemand: 60, demandStdDev: 15 },
71
- { id: 'Budget', adr: 100, expectedDemand: 100, demandStdDev: 25 },
71
+ { id: 'Budget', adr: 100, expectedDemand: 100, demandStdDev: 25 },
72
72
  ];
73
73
  const r = emsrYieldManagement(200, threeClass);
74
74
  expect(r.protectionLevels).toHaveLength(3);
@@ -93,7 +93,7 @@ describe('revparAnalysis', () => {
93
93
  */
94
94
  it('occupancy = occupied / available', () => {
95
95
  const r = revparAnalysis(200, 160, 24_000);
96
- expect(r.occupancyRate).toBeCloseTo(0.80, 5);
96
+ expect(r.occupancyRate).toBeCloseTo(0.8, 5);
97
97
  });
98
98
 
99
99
  it('ADR = revenue / occupied rooms', () => {
@@ -134,23 +134,23 @@ describe('overbookingOptimization', () => {
134
134
  * Critical ratio = 150/(150+300) = 1/3 — expect some overbooking to be optimal
135
135
  */
136
136
  it('overbooking level ≥ 0', () => {
137
- const r = overbookingOptimization(100, 0.10, 150, 300, 95);
137
+ const r = overbookingOptimization(100, 0.1, 150, 300, 95);
138
138
  expect(r.overbook).toBeGreaterThanOrEqual(0);
139
139
  });
140
140
 
141
141
  it('authorized bookings = capacity + overbook', () => {
142
- const r = overbookingOptimization(100, 0.10, 150, 300, 95);
142
+ const r = overbookingOptimization(100, 0.1, 150, 300, 95);
143
143
  expect(r.authorizedBookings).toBe(r.capacity + r.overbook);
144
144
  });
145
145
 
146
146
  it('higher walk cost → lower overbooking', () => {
147
- const r1 = overbookingOptimization(100, 0.10, 150, 100, 90); // low walk cost
148
- const r2 = overbookingOptimization(100, 0.10, 150, 1000, 90); // high walk cost
147
+ const r1 = overbookingOptimization(100, 0.1, 150, 100, 90); // low walk cost
148
+ const r2 = overbookingOptimization(100, 0.1, 150, 1000, 90); // high walk cost
149
149
  expect(r2.overbook).toBeLessThanOrEqual(r1.overbook);
150
150
  });
151
151
 
152
152
  it('expectedWalkCost ≥ 0', () => {
153
- const r = overbookingOptimization(100, 0.10, 150, 300, 95);
153
+ const r = overbookingOptimization(100, 0.1, 150, 300, 95);
154
154
  expect(r.expectedWalkCost).toBeGreaterThanOrEqual(0);
155
155
  });
156
156
 
@@ -231,8 +231,13 @@ describe('hotelDemandForecast', () => {
231
231
 
232
232
  it('weekend indices > weekday indices (reflects weekend demand spike)', () => {
233
233
  const r = hotelDemandForecast(historical, 7);
234
- const weekdayAvg = (r.seasonalIndices[0] + r.seasonalIndices[1] + r.seasonalIndices[2] +
235
- r.seasonalIndices[3] + r.seasonalIndices[4]) / 5;
234
+ const weekdayAvg =
235
+ (r.seasonalIndices[0] +
236
+ r.seasonalIndices[1] +
237
+ r.seasonalIndices[2] +
238
+ r.seasonalIndices[3] +
239
+ r.seasonalIndices[4]) /
240
+ 5;
236
241
  const weekendAvg = (r.seasonalIndices[5] + r.seasonalIndices[6]) / 2;
237
242
  expect(weekendAvg).toBeGreaterThan(weekdayAvg);
238
243
  });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Integration proof: the travel-hospitality `revpar` trait, once registered via
3
+ * the runtime's real `registerTrait` seam, is dispatched BY THE RUNTIME and runs
4
+ * the deterministic RevPAR analytic solver — NOT called directly as a handler
5
+ * object.
6
+ *
7
+ * Mirrors government-civic-plugin's runtime-integration reference
8
+ * (civic_decision). Drives the real path: executeNode(orb) -> orb-executor ->
9
+ * applyDirectives -> traitHandlers.get('revpar').onAttach -> revparAnalysis.
10
+ * The negative control proves the registration is load-bearing (without it, the
11
+ * trait is a dead no-op — which is exactly the tier's status quo).
12
+ *
13
+ * HAND-DERIVED RevPAR (real formula, NOT solver output):
14
+ * availableRooms=200, occupiedRooms=150, totalRevenue=$30,000
15
+ * occupancyRate = occupied/available = 150/200 = 0.75
16
+ * adr = revenue/occupied = 30000/150 = 200
17
+ * RevPAR = ADR × occupancy = 200 × 0.75 = 150.00
18
+ * cross-check = revenue/available = 30000/200 = 150.00 (agrees)
19
+ */
20
+ import { describe, it, expect } from 'vitest';
21
+ import { HoloScriptRuntime } from '@holoscript/core/runtime';
22
+ import { registerTravelHospitalityTraitHandlers } from '../runtime';
23
+
24
+ // Hand-derived expectations (see header arithmetic).
25
+ const AVAILABLE_ROOMS = 200;
26
+ const OCCUPIED_ROOMS = 150;
27
+ const TOTAL_REVENUE = 30_000;
28
+ const EXPECTED_OCCUPANCY = 0.75; // 150 / 200
29
+ const EXPECTED_ADR = 200; // 30000 / 150
30
+ const EXPECTED_REVPAR = 150; // 200 × 0.75 (=== 30000 / 200)
31
+
32
+ function revparOrb(config: Record<string, unknown>): unknown {
33
+ return {
34
+ type: 'orb',
35
+ name: 'revpar-orb',
36
+ properties: {},
37
+ methods: [],
38
+ position: [0, 0, 0],
39
+ hologram: { shape: 'orb', color: '#fff', size: 1, glow: false, interactive: false },
40
+ directives: [{ type: 'trait', name: 'revpar', config }],
41
+ };
42
+ }
43
+
44
+ /** Flush the runtime's async emit dispatch so `on` listeners have fired. */
45
+ const flush = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));
46
+
47
+ describe('travel-hospitality -> HoloScript runtime integration (revpar)', () => {
48
+ it('runtime dispatch runs the RevPAR solver for a registered @revpar orb', async () => {
49
+ const runtime = new HoloScriptRuntime();
50
+ registerTravelHospitalityTraitHandlers(runtime);
51
+
52
+ const solved: Array<Record<string, unknown>> = [];
53
+ runtime.on('revpar_solved', (e: unknown) => {
54
+ solved.push(e as Record<string, unknown>);
55
+ });
56
+
57
+ await runtime.executeNode(
58
+ revparOrb({
59
+ availableRooms: AVAILABLE_ROOMS,
60
+ occupiedRooms: OCCUPIED_ROOMS,
61
+ totalRevenue: TOTAL_REVENUE,
62
+ }) as never
63
+ );
64
+ await flush();
65
+
66
+ expect(solved).toHaveLength(1);
67
+ const summary = solved[0];
68
+ // Asserted against the HAND derivation above, NOT against solver output:
69
+ // RevPAR = ADR × occupancy = 200 × 0.75 = 150.00
70
+ expect(summary.revpar as number).toBeCloseTo(EXPECTED_REVPAR, 6);
71
+ expect(summary.adr as number).toBeCloseTo(EXPECTED_ADR, 6);
72
+ expect(summary.occupancyRate as number).toBeCloseTo(EXPECTED_OCCUPANCY, 6);
73
+ expect(summary.availableRooms).toBe(AVAILABLE_ROOMS);
74
+ expect(summary.occupiedRooms).toBe(OCCUPIED_ROOMS);
75
+ });
76
+
77
+ it('NEGATIVE CONTROL: without registration the @revpar trait is a dead no-op', async () => {
78
+ const runtime = new HoloScriptRuntime(); // intentionally NOT registered
79
+ const solved: unknown[] = [];
80
+ runtime.on('revpar_solved', (e: unknown) => solved.push(e));
81
+
82
+ await runtime.executeNode(
83
+ revparOrb({
84
+ availableRooms: AVAILABLE_ROOMS,
85
+ occupiedRooms: OCCUPIED_ROOMS,
86
+ totalRevenue: TOTAL_REVENUE,
87
+ }) as never
88
+ );
89
+ await flush();
90
+
91
+ expect(solved).toHaveLength(0);
92
+ });
93
+
94
+ it('persists the solver result into durable runtime state on ATTACH', async () => {
95
+ const runtime = new HoloScriptRuntime();
96
+ registerTravelHospitalityTraitHandlers(runtime);
97
+
98
+ await runtime.executeNode(
99
+ revparOrb({
100
+ availableRooms: AVAILABLE_ROOMS,
101
+ occupiedRooms: OCCUPIED_ROOMS,
102
+ totalRevenue: TOTAL_REVENUE,
103
+ }) as never
104
+ );
105
+ await flush();
106
+
107
+ const state = runtime.getState() as Record<string, unknown>;
108
+ const persisted = state['revpar:revpar-orb'] as
109
+ | { revpar?: number; occupancyRate?: number }
110
+ | undefined;
111
+ expect(persisted).toBeDefined();
112
+ // Same hand-derived RevPAR = 150.00.
113
+ expect(persisted?.revpar).toBeCloseTo(EXPECTED_REVPAR, 6);
114
+ expect(persisted?.occupancyRate).toBeCloseTo(EXPECTED_OCCUPANCY, 6);
115
+ });
116
+
117
+ it('emits revpar_error (does not throw through the runtime) for invalid config', async () => {
118
+ const runtime = new HoloScriptRuntime();
119
+ registerTravelHospitalityTraitHandlers(runtime);
120
+
121
+ const errors: Array<Record<string, unknown>> = [];
122
+ runtime.on('revpar_error', (e: unknown) => {
123
+ errors.push(e as Record<string, unknown>);
124
+ });
125
+
126
+ // Zero available rooms + negative revenue: the real solver validates
127
+ // `availableRooms <= 0` first and throws "availableRooms must be positive",
128
+ // which the handler's try/catch turns into a revpar_error rather than a throw.
129
+ await runtime.executeNode(
130
+ revparOrb({ availableRooms: 0, occupiedRooms: 0, totalRevenue: -5_000 }) as never
131
+ );
132
+ await flush();
133
+
134
+ expect(errors).toHaveLength(1);
135
+ expect(String(errors[0].error)).toContain('availableRooms');
136
+ });
137
+ });
package/src/index.ts CHANGED
@@ -1,6 +1,18 @@
1
- export { createReservationHandler, type ReservationConfig, type ReservationStatus } from './traits/ReservationTrait';
2
- export { createItineraryHandler, type ItineraryConfig, type ItineraryItem } from './traits/ItineraryTrait';
3
- export { createRateManagementHandler, type RateManagementConfig, type RatePeriod } from './traits/RateManagementTrait';
1
+ export {
2
+ createReservationHandler,
3
+ type ReservationConfig,
4
+ type ReservationStatus,
5
+ } from './traits/ReservationTrait';
6
+ export {
7
+ createItineraryHandler,
8
+ type ItineraryConfig,
9
+ type ItineraryItem,
10
+ } from './traits/ItineraryTrait';
11
+ export {
12
+ createRateManagementHandler,
13
+ type RateManagementConfig,
14
+ type RatePeriod,
15
+ } from './traits/RateManagementTrait';
4
16
  export * from './traits/types';
5
17
 
6
18
  import { createReservationHandler } from './traits/ReservationTrait';
@@ -9,5 +21,36 @@ import { createRateManagementHandler } from './traits/RateManagementTrait';
9
21
 
10
22
  export * from './revenuemanagement';
11
23
 
12
- export const pluginMeta = { name: '@holoscript/plugin-travel-hospitality', version: '1.0.0', traits: ['reservation', 'itinerary', 'rate_management', 'emsr_yield', 'revpar', 'overbooking', 'group_displacement', 'demand_forecast'] };
13
- export const traitHandlers = [createReservationHandler(), createItineraryHandler(), createRateManagementHandler()];
24
+ // Runtime integration behavioral trait handler + registrar that wire the
25
+ // deterministic RevPAR analytic solver into HoloScriptRuntime's dispatch. Closes
26
+ // the built-but-dead-wired gap for `revpar`, mirroring government-civic's
27
+ // `civic_decision` reference integration.
28
+ export {
29
+ TRAVEL_HOSPITALITY_PLUGIN_ID,
30
+ revparHandler,
31
+ registerTravelHospitalityTraitHandlers,
32
+ type RevparTraitConfig,
33
+ type RevparSolvedEvent,
34
+ type RuntimeTraitHandler,
35
+ type TraitRegistrar,
36
+ } from './runtime';
37
+
38
+ export const pluginMeta = {
39
+ name: '@holoscript/plugin-travel-hospitality',
40
+ version: '1.0.0',
41
+ traits: [
42
+ 'reservation',
43
+ 'itinerary',
44
+ 'rate_management',
45
+ 'emsr_yield',
46
+ 'revpar',
47
+ 'overbooking',
48
+ 'group_displacement',
49
+ 'demand_forecast',
50
+ ],
51
+ };
52
+ export const traitHandlers = [
53
+ createReservationHandler(),
54
+ createItineraryHandler(),
55
+ createRateManagementHandler(),
56
+ ];
@@ -106,23 +106,22 @@ export interface RevenueReceiptOptions {
106
106
  function normalCDF(x: number): number {
107
107
  if (x < -6) return 0;
108
108
  if (x > 6) return 1;
109
- const a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741;
110
- const a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911;
109
+ const a1 = 0.254829592,
110
+ a2 = -0.284496736,
111
+ a3 = 1.421413741;
112
+ const a4 = -1.453152027,
113
+ a5 = 1.061405429,
114
+ p = 0.3275911;
111
115
  const sign = x >= 0 ? 1 : -1;
112
116
  const absX = Math.abs(x);
113
117
  const t = 1 / (1 + p * absX);
114
- const poly = ((((a5 * t + a4) * t) + a3) * t + a2) * t + a1;
118
+ const poly = (((a5 * t + a4) * t + a3) * t + a2) * t + a1;
115
119
  const erfVal = 1 - poly * t * Math.exp(-absX * absX);
116
120
  return 0.5 * (1 + sign * erfVal);
117
121
  }
118
122
 
119
123
  /** Littlewood's rule: P(demand ≥ x) ≥ r_low/r_high → protect x seats for high class */
120
- function littlewoodProtection(
121
- rLow: number,
122
- rHigh: number,
123
- mu: number,
124
- sigma: number,
125
- ): number {
124
+ function littlewoodProtection(rLow: number, rHigh: number, mu: number, sigma: number): number {
126
125
  // Find x such that P(D_high ≥ x) = r_low / r_high
127
126
  const threshold = rLow / rHigh;
128
127
  // P(D ≥ x) = 1 - Φ((x - μ)/σ)
@@ -137,28 +136,42 @@ function littlewoodProtection(
137
136
  function normInvApprox(p: number): number {
138
137
  if (p <= 0) return -6;
139
138
  if (p >= 1) return 6;
140
- const a = [-3.969683028665376e+01, 2.209460984245205e+02, -2.759285104469687e+02,
141
- 1.383577518672690e+02, -3.066479806614716e+01, 2.506628277459239e+00];
142
- const b = [-5.447609879822406e+01, 1.615858368580409e+02, -1.556989798598866e+02,
143
- 6.680131188771972e+01, -1.328068155288572e+01];
144
- const c = [-7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e+00,
145
- -2.549732539343734e+00, 4.374664141464968e+00, 2.938163982698783e+00];
146
- const d = [7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e+00, 3.754408661907416e+00];
147
- const pLow = 0.02425, pHigh = 1 - pLow;
139
+ const a = [
140
+ -3.969683028665376e1, 2.209460984245205e2, -2.759285104469687e2, 1.38357751867269e2,
141
+ -3.066479806614716e1, 2.506628277459239,
142
+ ];
143
+ const b = [
144
+ -5.447609879822406e1, 1.615858368580409e2, -1.556989798598866e2, 6.680131188771972e1,
145
+ -1.328068155288572e1,
146
+ ];
147
+ const c = [
148
+ -7.784894002430293e-3, -3.223964580411365e-1, -2.400758277161838, -2.549732539343734,
149
+ 4.374664141464968, 2.938163982698783,
150
+ ];
151
+ const d = [7.784695709041462e-3, 3.224671290700398e-1, 2.445134137142996, 3.754408661907416];
152
+ const pLow = 0.02425,
153
+ pHigh = 1 - pLow;
148
154
 
149
155
  let q: number, r: number;
150
156
  if (p < pLow) {
151
157
  q = Math.sqrt(-2 * Math.log(p));
152
- return (((((c[0]*q+c[1])*q+c[2])*q+c[3])*q+c[4])*q+c[5]) /
153
- ((((d[0]*q+d[1])*q+d[2])*q+d[3])*q+1);
158
+ return (
159
+ (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
160
+ ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1)
161
+ );
154
162
  } else if (p <= pHigh) {
155
- q = p - 0.5; r = q * q;
156
- return (((((a[0]*r+a[1])*r+a[2])*r+a[3])*r+a[4])*r+a[5])*q /
157
- (((((b[0]*r+b[1])*r+b[2])*r+b[3])*r+b[4])*r+1);
163
+ q = p - 0.5;
164
+ r = q * q;
165
+ return (
166
+ ((((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q) /
167
+ (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1)
168
+ );
158
169
  } else {
159
170
  q = Math.sqrt(-2 * Math.log(1 - p));
160
- return -(((((c[0]*q+c[1])*q+c[2])*q+c[3])*q+c[4])*q+c[5]) /
161
- ((((d[0]*q+d[1])*q+d[2])*q+d[3])*q+1);
171
+ return (
172
+ -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
173
+ ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1)
174
+ );
162
175
  }
163
176
  }
164
177
 
@@ -167,10 +180,7 @@ function normInvApprox(p: number): number {
167
180
  * Rate classes must be provided in any order; sorted high→low ADR internally.
168
181
  * capacity: total rooms available.
169
182
  */
170
- export function emsrYieldManagement(
171
- capacity: number,
172
- rateClasses: RateClass[],
173
- ): EMSRResult {
183
+ export function emsrYieldManagement(capacity: number, rateClasses: RateClass[]): EMSRResult {
174
184
  if (capacity <= 0) throw new Error('capacity must be positive');
175
185
  if (rateClasses.length === 0) throw new Error('At least one rate class required');
176
186
 
@@ -182,13 +192,14 @@ export function emsrYieldManagement(
182
192
 
183
193
  // EMSR-b: aggregate demand from classes j+1..n and find protection for class j
184
194
  for (let j = 0; j < sorted.length - 1; j++) {
185
- const r_j = sorted[j].adr; // higher class fare
186
- const r_j1 = sorted[j + 1].adr; // lower class fare
195
+ const r_j = sorted[j].adr; // higher class fare
196
+ const r_j1 = sorted[j + 1].adr; // lower class fare
187
197
 
188
198
  // Aggregate demand for classes 0..j (high-value classes)
189
- let aggMu = 0, aggVar = 0;
199
+ let aggMu = 0,
200
+ aggVar = 0;
190
201
  for (let k = 0; k <= j; k++) {
191
- aggMu += sorted[k].expectedDemand;
202
+ aggMu += sorted[k].expectedDemand;
192
203
  aggVar += sorted[k].demandStdDev ** 2;
193
204
  }
194
205
  const aggSigma = Math.sqrt(aggVar);
@@ -222,7 +233,7 @@ export function emsrYieldManagement(
222
233
  export function revparAnalysis(
223
234
  availableRooms: number,
224
235
  occupiedRooms: number,
225
- totalRevenue: number,
236
+ totalRevenue: number
226
237
  ): RevPARResult {
227
238
  if (availableRooms <= 0) throw new Error('availableRooms must be positive');
228
239
  if (occupiedRooms < 0) throw new Error('occupiedRooms must be non-negative');
@@ -249,7 +260,7 @@ export function overbookingOptimization(
249
260
  noShowRate: number,
250
261
  adr: number,
251
262
  walkCostPerGuest: number,
252
- expectedBookings: number,
263
+ expectedBookings: number
253
264
  ): OverbookingResult {
254
265
  if (capacity <= 0) throw new Error('capacity must be positive');
255
266
  if (noShowRate < 0 || noShowRate > 1) throw new Error('noShowRate must be in [0, 1]');
@@ -282,7 +293,8 @@ export function overbookingOptimization(
282
293
  // Expected walks = E[max(0, shows - capacity)] where shows = bookings - noShows
283
294
  let expectedWalks = 0;
284
295
  for (let w = 1; w <= optOverbook + 5; w++) {
285
- const pWalk = 1 - normalCDF((w - 0.5 - (authorizedBookings - mu - capacity)) / Math.max(sigma, 0.1));
296
+ const pWalk =
297
+ 1 - normalCDF((w - 0.5 - (authorizedBookings - mu - capacity)) / Math.max(sigma, 0.1));
286
298
  expectedWalks += pWalk;
287
299
  if (pWalk < 1e-6) break;
288
300
  }
@@ -292,7 +304,15 @@ export function overbookingOptimization(
292
304
  // Revenue gain vs no-overbooking: each additional booking accepted at ADR, less expected walk cost
293
305
  const expectedRevenueGain = Math.max(0, optOverbook * adr * noShowRate - expectedWalkCost);
294
306
 
295
- return { capacity, overbook: optOverbook, authorizedBookings, noShowRate, walkCostPerGuest, expectedWalkCost, expectedRevenueGain };
307
+ return {
308
+ capacity,
309
+ overbook: optOverbook,
310
+ authorizedBookings,
311
+ noShowRate,
312
+ walkCostPerGuest,
313
+ expectedWalkCost,
314
+ expectedRevenueGain,
315
+ };
296
316
  }
297
317
 
298
318
  // ─── Group displacement ───────────────────────────────────────────────────────
@@ -304,14 +324,15 @@ export function groupDisplacementAnalysis(
304
324
  groupAdr: number,
305
325
  groupRoomNights: number,
306
326
  transientAdr: number,
307
- transientDemandProbability = 1.0,
327
+ transientDemandProbability = 1.0
308
328
  ): GroupDisplacementResult {
309
329
  if (groupAdr <= 0) throw new Error('groupAdr must be positive');
310
330
  if (groupRoomNights <= 0) throw new Error('groupRoomNights must be positive');
311
331
  if (transientAdr <= 0) throw new Error('transientAdr must be positive');
312
332
 
313
333
  const displacementCost = (transientAdr - groupAdr) * groupRoomNights * transientDemandProbability;
314
- const netValue = groupAdr * groupRoomNights - (transientAdr * groupRoomNights * transientDemandProbability);
334
+ const netValue =
335
+ groupAdr * groupRoomNights - transientAdr * groupRoomNights * transientDemandProbability;
315
336
  const profitable = netValue > 0;
316
337
 
317
338
  return { groupAdr, groupRoomNights, transientAdr, displacementCost, netValue, profitable };
@@ -325,9 +346,10 @@ export function groupDisplacementAnalysis(
325
346
  */
326
347
  export function hotelDemandForecast(
327
348
  historical: number[],
328
- forecastDays: number,
349
+ forecastDays: number
329
350
  ): HotelForecastResult {
330
- if (historical.length < 14) throw new Error('At least 14 days of history required for seasonal estimation');
351
+ if (historical.length < 14)
352
+ throw new Error('At least 14 days of history required for seasonal estimation');
331
353
  if (forecastDays < 1) throw new Error('forecastDays must be ≥ 1');
332
354
 
333
355
  // Estimate weekly seasonal indices (7 days)
@@ -341,7 +363,7 @@ export function hotelDemandForecast(
341
363
  dayCounts[dow]++;
342
364
  });
343
365
  seasonalIndices.forEach((s, i) => {
344
- seasonalIndices[i] = overallMean > 0 ? (s / dayCounts[i]) / overallMean : 1;
366
+ seasonalIndices[i] = overallMean > 0 ? s / dayCounts[i] / overallMean : 1;
345
367
  });
346
368
 
347
369
  // Deseasonalise
@@ -385,11 +407,11 @@ export interface TravelAnalysisResult {
385
407
 
386
408
  export function buildTravelReceipt(
387
409
  result: TravelAnalysisResult,
388
- options?: RevenueReceiptOptions,
410
+ options?: RevenueReceiptOptions
389
411
  ): DomainSimulationReceipt {
390
412
  const violations: Array<{ criterion: string; message: string }> = [];
391
413
 
392
- if (result.revpar && result.revpar.occupancyRate < 0.40) {
414
+ if (result.revpar && result.revpar.occupancyRate < 0.4) {
393
415
  violations.push({
394
416
  criterion: 'occupancy',
395
417
  message: `Occupancy ${(result.revpar.occupancyRate * 100).toFixed(1)}% is critically low (<40%)`,
@@ -413,7 +435,11 @@ export function buildTravelReceipt(
413
435
  emsrExpectedRevenue: result.emsr?.expectedRevenue ?? null,
414
436
  overbookLevel: result.overbooking?.overbook ?? null,
415
437
  },
416
- cael: { version: 'cael.v1', event: 'travel_hospitality.revenue_management', solverType: 'travel-hospitality.emsr-b' },
438
+ cael: {
439
+ version: 'cael.v1',
440
+ event: 'travel_hospitality.revenue_management',
441
+ solverType: 'travel-hospitality.emsr-b',
442
+ },
417
443
  acceptance: { accepted: violations.length === 0, violations },
418
444
  });
419
445
  }
package/src/runtime.ts ADDED
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Runtime integration for @holoscript/plugin-travel-hospitality.
3
+ *
4
+ * Bridges the previously dead-wired `revpar` trait into a behavioral
5
+ * TraitHandler that the HoloScript runtime actually dispatches
6
+ * (HoloScriptRuntime.registerTrait -> applyDirectives / updateTraits).
7
+ *
8
+ * Before this module the plugin declared trait NAMES only (pluginMeta.traits)
9
+ * and exported the revenue-management solvers (revparAnalysis,
10
+ * emsrYieldManagement, …), but nothing invoked a solver THROUGH the runtime —
11
+ * the whole domain-plugin tier was built-but-dead-wired. This mirrors
12
+ * government-civic-plugin's reference integration (civic_decision): it wires the
13
+ * deterministic RevPAR analytic solver (`revparAnalysis`) behind the `revpar`
14
+ * trait so the runtime's directive dispatch can run it. The remaining
15
+ * travel-hospitality traits follow the same registrar shape.
16
+ *
17
+ * RevPAR (Revenue Per Available Room) = Room Revenue / Available Room-nights,
18
+ * equivalently ADR × Occupancy (revparAnalysis computes both and they agree).
19
+ */
20
+ import { registerPluginTraits } from '@holoscript/core/runtime';
21
+ import { revparAnalysis, type RevPARResult } from './revenuemanagement';
22
+
23
+ /** Stable id for this plugin's trait ownership tagging. */
24
+ export const TRAVEL_HOSPITALITY_PLUGIN_ID = 'travel-hospitality' as const;
25
+
26
+ /** Config carried by an orb's `@revpar` trait directive. */
27
+ export interface RevparTraitConfig {
28
+ /** Rooms available for the period. Required; absence/invalid emits `revpar_error`. */
29
+ availableRooms?: number;
30
+ /** Rooms occupied for the period. Required. */
31
+ occupiedRooms?: number;
32
+ /** Total room revenue for the period ($). Required. */
33
+ totalRevenue?: number;
34
+ }
35
+
36
+ /** Summary payload emitted on `revpar_solved`. */
37
+ export interface RevparSolvedEvent {
38
+ nodeId: string;
39
+ /** Revenue Per Available Room = ADR × occupancy ($). */
40
+ revpar: number;
41
+ /** Average Daily Rate ($). */
42
+ adr: number;
43
+ /** Occupancy rate [0,1]. */
44
+ occupancyRate: number;
45
+ /** Rooms available evaluated. */
46
+ availableRooms: number;
47
+ /** Rooms occupied evaluated. */
48
+ occupiedRooms: number;
49
+ /** Total room revenue evaluated ($). */
50
+ totalRevenue: number;
51
+ }
52
+
53
+ /**
54
+ * Structural view of the runtime trait-handler contract. Matches
55
+ * `@holoscript/core` TraitTypes.TraitHandler at the call sites the runtime
56
+ * actually uses (onAttach / onUpdate receive the node, the directive config,
57
+ * and a context exposing `emit`). Declared locally so the plugin stays
58
+ * decoupled from core's full trait surface.
59
+ */
60
+ export interface TraitDispatchContext {
61
+ emit: (event: string, payload?: unknown) => void;
62
+ setState?: (updates: Record<string, unknown>) => void;
63
+ }
64
+
65
+ export interface RuntimeTraitHandler {
66
+ name: string;
67
+ onAttach?: (node: unknown, config: RevparTraitConfig, context: TraitDispatchContext) => void;
68
+ onUpdate?: (
69
+ node: unknown,
70
+ config: RevparTraitConfig,
71
+ context: TraitDispatchContext,
72
+ delta: number
73
+ ) => void;
74
+ }
75
+
76
+ interface RevparNode {
77
+ id?: string;
78
+ name?: string;
79
+ properties?: Record<string, unknown>;
80
+ __revparResult?: RevPARResult;
81
+ }
82
+
83
+ /** Run the RevPAR solver on the directive config, write the result onto the node, and emit. */
84
+ function solveOntoNode(
85
+ node: unknown,
86
+ config: RevparTraitConfig | undefined,
87
+ context: TraitDispatchContext
88
+ ): void {
89
+ const carrier = node as RevparNode;
90
+ const nodeId = carrier.id ?? carrier.name ?? 'unknown';
91
+ const availableRooms = config?.availableRooms;
92
+ const occupiedRooms = config?.occupiedRooms;
93
+ const totalRevenue = config?.totalRevenue;
94
+
95
+ if (
96
+ typeof availableRooms !== 'number' ||
97
+ typeof occupiedRooms !== 'number' ||
98
+ typeof totalRevenue !== 'number'
99
+ ) {
100
+ context.emit('revpar_error', {
101
+ nodeId,
102
+ error:
103
+ 'revpar trait requires numeric config.availableRooms, config.occupiedRooms, and config.totalRevenue',
104
+ });
105
+ return;
106
+ }
107
+
108
+ try {
109
+ const result = revparAnalysis(availableRooms, occupiedRooms, totalRevenue);
110
+ carrier.__revparResult = result;
111
+ carrier.properties = {
112
+ ...(carrier.properties ?? {}),
113
+ revpar: result.revpar,
114
+ revparAdr: result.adr,
115
+ revparOccupancyRate: result.occupancyRate,
116
+ };
117
+ const summary: RevparSolvedEvent = {
118
+ nodeId,
119
+ revpar: result.revpar,
120
+ adr: result.adr,
121
+ occupancyRate: result.occupancyRate,
122
+ availableRooms: result.availableRooms,
123
+ occupiedRooms: result.occupiedRooms,
124
+ totalRevenue: result.totalRevenue,
125
+ };
126
+ context.setState?.({ [`revpar:${nodeId}`]: summary });
127
+ context.emit('revpar_solved', summary);
128
+ } catch (error) {
129
+ context.emit('revpar_error', {
130
+ nodeId,
131
+ error: error instanceof Error ? error.message : String(error),
132
+ });
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Behavioral handler for the travel-hospitality `revpar` trait. Runs the
138
+ * deterministic RevPAR analytic solver whenever an orb carrying the trait is
139
+ * attached (and on each per-frame update), writing the result onto the node and
140
+ * emitting `revpar_solved` / `revpar_error`.
141
+ */
142
+ export const revparHandler: RuntimeTraitHandler = {
143
+ name: 'revpar',
144
+ onAttach: (node, config, context) => solveOntoNode(node, config, context),
145
+ onUpdate: (node, config, context) => solveOntoNode(node, config, context),
146
+ };
147
+
148
+ /** A runtime that can register behavioral trait handlers. */
149
+ export interface TraitRegistrar {
150
+ registerTrait(name: string, handler: unknown): void;
151
+ }
152
+
153
+ /**
154
+ * Register travel-hospitality behavioral trait handlers into a runtime that
155
+ * exposes `registerTrait(name, handler)` — e.g. `@holoscript/core`
156
+ * HoloScriptRuntime. This is the consumption path the dead-wired tier was
157
+ * missing: after this call the runtime's directive dispatch (applyDirectives /
158
+ * updateTraits) will invoke the RevPAR solver for `@revpar` orbs.
159
+ */
160
+ export function registerTravelHospitalityTraitHandlers(registrar: TraitRegistrar): void {
161
+ registerPluginTraits(registrar, TRAVEL_HOSPITALITY_PLUGIN_ID, [revparHandler]);
162
+ }
@@ -1,25 +1,73 @@
1
1
  /** @itinerary Trait — Trip itinerary planning. @trait itinerary */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export interface ItineraryItem { id: string; day: number; time: string; activity: string; location: string; durationMin: number; bookingRef?: string; notes?: string; }
5
- export interface ItineraryConfig { tripName: string; startDate: string; endDate: string; travelers: number; items: ItineraryItem[]; }
6
- export interface ItineraryState { currentDay: number; completedItems: string[]; totalDays: number; }
4
+ export interface ItineraryItem {
5
+ id: string;
6
+ day: number;
7
+ time: string;
8
+ activity: string;
9
+ location: string;
10
+ durationMin: number;
11
+ bookingRef?: string;
12
+ notes?: string;
13
+ }
14
+ export interface ItineraryConfig {
15
+ tripName: string;
16
+ startDate: string;
17
+ endDate: string;
18
+ travelers: number;
19
+ items: ItineraryItem[];
20
+ }
21
+ export interface ItineraryState {
22
+ currentDay: number;
23
+ completedItems: string[];
24
+ totalDays: number;
25
+ }
7
26
 
8
- const defaultConfig: ItineraryConfig = { tripName: '', startDate: '', endDate: '', travelers: 1, items: [] };
27
+ const defaultConfig: ItineraryConfig = {
28
+ tripName: '',
29
+ startDate: '',
30
+ endDate: '',
31
+ travelers: 1,
32
+ items: [],
33
+ };
9
34
 
10
35
  export function createItineraryHandler(): TraitHandler<ItineraryConfig> {
11
- return { name: 'itinerary', defaultConfig,
36
+ return {
37
+ name: 'itinerary',
38
+ defaultConfig,
12
39
  onAttach(n: HSPlusNode, c: ItineraryConfig, ctx: TraitContext) {
13
- const days = c.startDate && c.endDate ? Math.max(1, Math.ceil((new Date(c.endDate).getTime() - new Date(c.startDate).getTime()) / 86400000)) : 1;
40
+ const days =
41
+ c.startDate && c.endDate
42
+ ? Math.max(
43
+ 1,
44
+ Math.ceil(
45
+ (new Date(c.endDate).getTime() - new Date(c.startDate).getTime()) / 86400000
46
+ )
47
+ )
48
+ : 1;
14
49
  n.__itinState = { currentDay: 1, completedItems: [], totalDays: days };
15
50
  ctx.emit?.('itinerary:planned', { days, items: c.items.length });
16
51
  },
17
- onDetach(n: HSPlusNode, _c: ItineraryConfig, ctx: TraitContext) { delete n.__itinState; ctx.emit?.('itinerary:removed'); },
52
+ onDetach(n: HSPlusNode, _c: ItineraryConfig, ctx: TraitContext) {
53
+ delete n.__itinState;
54
+ ctx.emit?.('itinerary:removed');
55
+ },
18
56
  onUpdate() {},
19
57
  onEvent(n: HSPlusNode, _c: ItineraryConfig, ctx: TraitContext, e: TraitEvent) {
20
- const s = n.__itinState as ItineraryState | undefined; if (!s) return;
21
- if (e.type === 'itinerary:complete_item') { const id = e.payload?.itemId as string; if (!s.completedItems.includes(id)) { s.completedItems.push(id); ctx.emit?.('itinerary:item_done', { id, completed: s.completedItems.length }); } }
22
- if (e.type === 'itinerary:next_day') { s.currentDay = Math.min(s.totalDays, s.currentDay + 1); ctx.emit?.('itinerary:day_changed', { day: s.currentDay }); }
58
+ const s = n.__itinState as ItineraryState | undefined;
59
+ if (!s) return;
60
+ if (e.type === 'itinerary:complete_item') {
61
+ const id = e.payload?.itemId as string;
62
+ if (!s.completedItems.includes(id)) {
63
+ s.completedItems.push(id);
64
+ ctx.emit?.('itinerary:item_done', { id, completed: s.completedItems.length });
65
+ }
66
+ }
67
+ if (e.type === 'itinerary:next_day') {
68
+ s.currentDay = Math.min(s.totalDays, s.currentDay + 1);
69
+ ctx.emit?.('itinerary:day_changed', { day: s.currentDay });
70
+ }
23
71
  },
24
72
  };
25
73
  }
@@ -1,24 +1,67 @@
1
1
  /** @rate_management Trait — Dynamic pricing and rate control. @trait rate_management */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export interface RatePeriod { startDate: string; endDate: string; rate: number; minStay: number; }
5
- export interface RateManagementConfig { baseRate: number; currency: string; seasons: RatePeriod[]; weekendMultiplier: number; dynamicPricingEnabled: boolean; minRate: number; maxRate: number; }
6
- export interface RateManagementState { currentRate: number; occupancyPercent: number; revenueToday: number; }
4
+ export interface RatePeriod {
5
+ startDate: string;
6
+ endDate: string;
7
+ rate: number;
8
+ minStay: number;
9
+ }
10
+ export interface RateManagementConfig {
11
+ baseRate: number;
12
+ currency: string;
13
+ seasons: RatePeriod[];
14
+ weekendMultiplier: number;
15
+ dynamicPricingEnabled: boolean;
16
+ minRate: number;
17
+ maxRate: number;
18
+ }
19
+ export interface RateManagementState {
20
+ currentRate: number;
21
+ occupancyPercent: number;
22
+ revenueToday: number;
23
+ }
7
24
 
8
- const defaultConfig: RateManagementConfig = { baseRate: 100, currency: 'USD', seasons: [], weekendMultiplier: 1.2, dynamicPricingEnabled: false, minRate: 50, maxRate: 500 };
25
+ const defaultConfig: RateManagementConfig = {
26
+ baseRate: 100,
27
+ currency: 'USD',
28
+ seasons: [],
29
+ weekendMultiplier: 1.2,
30
+ dynamicPricingEnabled: false,
31
+ minRate: 50,
32
+ maxRate: 500,
33
+ };
9
34
 
10
35
  export function createRateManagementHandler(): TraitHandler<RateManagementConfig> {
11
- return { name: 'rate_management', defaultConfig,
12
- onAttach(n: HSPlusNode, c: RateManagementConfig, ctx: TraitContext) { n.__rateState = { currentRate: c.baseRate, occupancyPercent: 0, revenueToday: 0 }; ctx.emit?.('rate:configured', { base: c.baseRate }); },
13
- onDetach(n: HSPlusNode, _c: RateManagementConfig, ctx: TraitContext) { delete n.__rateState; ctx.emit?.('rate:removed'); },
36
+ return {
37
+ name: 'rate_management',
38
+ defaultConfig,
39
+ onAttach(n: HSPlusNode, c: RateManagementConfig, ctx: TraitContext) {
40
+ n.__rateState = { currentRate: c.baseRate, occupancyPercent: 0, revenueToday: 0 };
41
+ ctx.emit?.('rate:configured', { base: c.baseRate });
42
+ },
43
+ onDetach(n: HSPlusNode, _c: RateManagementConfig, ctx: TraitContext) {
44
+ delete n.__rateState;
45
+ ctx.emit?.('rate:removed');
46
+ },
14
47
  onUpdate() {},
15
48
  onEvent(n: HSPlusNode, c: RateManagementConfig, ctx: TraitContext, e: TraitEvent) {
16
- const s = n.__rateState as RateManagementState | undefined; if (!s) return;
49
+ const s = n.__rateState as RateManagementState | undefined;
50
+ if (!s) return;
17
51
  if (e.type === 'rate:update_occupancy') {
18
52
  s.occupancyPercent = (e.payload?.occupancy as number) ?? 0;
19
- if (c.dynamicPricingEnabled) { s.currentRate = Math.min(c.maxRate, Math.max(c.minRate, c.baseRate * (1 + s.occupancyPercent / 100))); ctx.emit?.('rate:adjusted', { rate: s.currentRate, occupancy: s.occupancyPercent }); }
53
+ if (c.dynamicPricingEnabled) {
54
+ s.currentRate = Math.min(
55
+ c.maxRate,
56
+ Math.max(c.minRate, c.baseRate * (1 + s.occupancyPercent / 100))
57
+ );
58
+ ctx.emit?.('rate:adjusted', { rate: s.currentRate, occupancy: s.occupancyPercent });
59
+ }
60
+ }
61
+ if (e.type === 'rate:record_sale') {
62
+ s.revenueToday += s.currentRate;
63
+ ctx.emit?.('rate:sale_recorded', { revenue: s.revenueToday });
20
64
  }
21
- if (e.type === 'rate:record_sale') { s.revenueToday += s.currentRate; ctx.emit?.('rate:sale_recorded', { revenue: s.revenueToday }); }
22
65
  },
23
66
  };
24
67
  }
@@ -1,27 +1,84 @@
1
1
  /** @reservation Trait — Booking and reservation management. @trait reservation */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export type ReservationStatus = 'pending' | 'confirmed' | 'checked_in' | 'checked_out' | 'cancelled' | 'no_show';
5
- export interface ReservationConfig { confirmationNumber: string; guestName: string; roomType: string; checkIn: string; checkOut: string; guests: number; ratePerNight: number; currency: string; specialRequests?: string; }
6
- export interface ReservationState { status: ReservationStatus; totalNights: number; totalCost: number; }
4
+ export type ReservationStatus =
5
+ | 'pending'
6
+ | 'confirmed'
7
+ | 'checked_in'
8
+ | 'checked_out'
9
+ | 'cancelled'
10
+ | 'no_show';
11
+ export interface ReservationConfig {
12
+ confirmationNumber: string;
13
+ guestName: string;
14
+ roomType: string;
15
+ checkIn: string;
16
+ checkOut: string;
17
+ guests: number;
18
+ ratePerNight: number;
19
+ currency: string;
20
+ specialRequests?: string;
21
+ }
22
+ export interface ReservationState {
23
+ status: ReservationStatus;
24
+ totalNights: number;
25
+ totalCost: number;
26
+ }
7
27
 
8
- const defaultConfig: ReservationConfig = { confirmationNumber: '', guestName: '', roomType: 'standard', checkIn: '', checkOut: '', guests: 1, ratePerNight: 0, currency: 'USD' };
28
+ const defaultConfig: ReservationConfig = {
29
+ confirmationNumber: '',
30
+ guestName: '',
31
+ roomType: 'standard',
32
+ checkIn: '',
33
+ checkOut: '',
34
+ guests: 1,
35
+ ratePerNight: 0,
36
+ currency: 'USD',
37
+ };
9
38
 
10
39
  export function createReservationHandler(): TraitHandler<ReservationConfig> {
11
- return { name: 'reservation', defaultConfig,
40
+ return {
41
+ name: 'reservation',
42
+ defaultConfig,
12
43
  onAttach(n: HSPlusNode, c: ReservationConfig, ctx: TraitContext) {
13
- const nights = c.checkIn && c.checkOut ? Math.max(1, Math.ceil((new Date(c.checkOut).getTime() - new Date(c.checkIn).getTime()) / 86400000)) : 1;
14
- n.__resState = { status: 'pending' as ReservationStatus, totalNights: nights, totalCost: nights * c.ratePerNight };
44
+ const nights =
45
+ c.checkIn && c.checkOut
46
+ ? Math.max(
47
+ 1,
48
+ Math.ceil((new Date(c.checkOut).getTime() - new Date(c.checkIn).getTime()) / 86400000)
49
+ )
50
+ : 1;
51
+ n.__resState = {
52
+ status: 'pending' as ReservationStatus,
53
+ totalNights: nights,
54
+ totalCost: nights * c.ratePerNight,
55
+ };
15
56
  ctx.emit?.('reservation:created', { confirmation: c.confirmationNumber, nights });
16
57
  },
17
- onDetach(n: HSPlusNode, _c: ReservationConfig, ctx: TraitContext) { delete n.__resState; ctx.emit?.('reservation:removed'); },
58
+ onDetach(n: HSPlusNode, _c: ReservationConfig, ctx: TraitContext) {
59
+ delete n.__resState;
60
+ ctx.emit?.('reservation:removed');
61
+ },
18
62
  onUpdate() {},
19
63
  onEvent(n: HSPlusNode, _c: ReservationConfig, ctx: TraitContext, e: TraitEvent) {
20
- const s = n.__resState as ReservationState | undefined; if (!s) return;
21
- if (e.type === 'reservation:confirm') { s.status = 'confirmed'; ctx.emit?.('reservation:confirmed'); }
22
- if (e.type === 'reservation:check_in') { s.status = 'checked_in'; ctx.emit?.('reservation:checked_in'); }
23
- if (e.type === 'reservation:check_out') { s.status = 'checked_out'; ctx.emit?.('reservation:checked_out', { total: s.totalCost }); }
24
- if (e.type === 'reservation:cancel') { s.status = 'cancelled'; ctx.emit?.('reservation:cancelled'); }
64
+ const s = n.__resState as ReservationState | undefined;
65
+ if (!s) return;
66
+ if (e.type === 'reservation:confirm') {
67
+ s.status = 'confirmed';
68
+ ctx.emit?.('reservation:confirmed');
69
+ }
70
+ if (e.type === 'reservation:check_in') {
71
+ s.status = 'checked_in';
72
+ ctx.emit?.('reservation:checked_in');
73
+ }
74
+ if (e.type === 'reservation:check_out') {
75
+ s.status = 'checked_out';
76
+ ctx.emit?.('reservation:checked_out', { total: s.totalCost });
77
+ }
78
+ if (e.type === 'reservation:cancel') {
79
+ s.status = 'cancelled';
80
+ ctx.emit?.('reservation:cancelled');
81
+ }
25
82
  },
26
83
  };
27
84
  }
@@ -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/vitest.config.ts CHANGED
@@ -1,6 +1,18 @@
1
1
  import { defineConfig } from 'vitest/config';
2
+ import { resolve } from 'path';
2
3
 
4
+ // resolve.alias is REQUIRED: without it, `@holoscript/core/runtime` resolves to
5
+ // the STALE built dist and registerPluginTraits is missing ("not a function").
6
+ // Point the subpath imports at core/engine source so the runtime barrel
7
+ // (core/src/runtime.ts) and shared registrar resolve from source. Mirrors
8
+ // government-civic-plugin/vitest.config.ts.
3
9
  export default defineConfig({
10
+ resolve: {
11
+ alias: {
12
+ '@holoscript/engine': resolve(__dirname, '../../engine/src'),
13
+ '@holoscript/core': resolve(__dirname, '../../core/src'),
14
+ },
15
+ },
4
16
  test: {
5
17
  globals: true,
6
18
  environment: 'node',
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.