@holoscript/plugin-retail-ecommerce 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,14 +1,14 @@
1
1
  {
2
2
  "name": "@holoscript/plugin-retail-ecommerce",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "HoloScript domain plugin for retail-ecommerce",
5
5
  "main": "src/index.ts",
6
6
  "peerDependencies": {
7
- "@holoscript/core": "8.0.6"
7
+ "@holoscript/core": ">=8.0.0"
8
8
  },
9
9
  "license": "MIT",
10
10
  "scripts": {
11
11
  "test": "vitest run --passWithNoTests",
12
12
  "test:coverage": "vitest run --coverage --passWithNoTests"
13
13
  }
14
- }
14
+ }
@@ -30,7 +30,7 @@ describe('economicOrderQuantity', () => {
30
30
  */
31
31
  it('EOQ = √(2DS/H)', () => {
32
32
  const r = economicOrderQuantity(1000, 10, 2.5);
33
- expect(r.eoq).toBeCloseTo(Math.sqrt(2 * 1000 * 10 / 2.5), 1);
33
+ expect(r.eoq).toBeCloseTo(Math.sqrt((2 * 1000 * 10) / 2.5), 1);
34
34
  });
35
35
 
36
36
  it('ordersPerYear = demand / EOQ', () => {
@@ -150,12 +150,12 @@ describe('customerLifetimeValue', () => {
150
150
  * Discounted CLV = Σ_{t=1}^{5} 400 / 1.1^t ≈ 400 × 3.791 ≈ $1516
151
151
  */
152
152
  it('simpleCLV = AOV × frequency × lifespan', () => {
153
- const r = customerLifetimeValue(100, 4, 5, 0.10);
153
+ const r = customerLifetimeValue(100, 4, 5, 0.1);
154
154
  expect(r.simpleCLV).toBeCloseTo(2000, 4);
155
155
  });
156
156
 
157
157
  it('discountedCLV < simpleCLV for positive discount rate', () => {
158
- const r = customerLifetimeValue(100, 4, 5, 0.10);
158
+ const r = customerLifetimeValue(100, 4, 5, 0.1);
159
159
  expect(r.discountedCLV).toBeLessThan(r.simpleCLV);
160
160
  });
161
161
 
@@ -165,18 +165,18 @@ describe('customerLifetimeValue', () => {
165
165
  });
166
166
 
167
167
  it('CLV:CAC ratio computed when CAC provided', () => {
168
- const r = customerLifetimeValue(100, 4, 5, 0.10, 200);
168
+ const r = customerLifetimeValue(100, 4, 5, 0.1, 200);
169
169
  expect(r.clvToCac).not.toBeNull();
170
170
  expect(r.clvToCac!).toBeCloseTo(r.discountedCLV / 200, 4);
171
171
  });
172
172
 
173
173
  it('CLV:CAC null when CAC not provided', () => {
174
- const r = customerLifetimeValue(100, 4, 5, 0.10);
174
+ const r = customerLifetimeValue(100, 4, 5, 0.1);
175
175
  expect(r.clvToCac).toBeNull();
176
176
  });
177
177
 
178
178
  it('throws for zero AOV', () => {
179
- expect(() => customerLifetimeValue(0, 4, 5, 0.10)).toThrow();
179
+ expect(() => customerLifetimeValue(0, 4, 5, 0.1)).toThrow();
180
180
  });
181
181
  });
182
182
 
@@ -194,9 +194,9 @@ describe('conversionFunnelAnalysis', () => {
194
194
 
195
195
  it('step conversions correct', () => {
196
196
  const r = conversionFunnelAnalysis(stages, counts, 75);
197
- expect(r.stepConversions[0]).toBeCloseTo(0.20, 4);
197
+ expect(r.stepConversions[0]).toBeCloseTo(0.2, 4);
198
198
  expect(r.stepConversions[1]).toBeCloseTo(0.25, 4);
199
- expect(r.stepConversions[2]).toBeCloseTo(0.50, 4);
199
+ expect(r.stepConversions[2]).toBeCloseTo(0.5, 4);
200
200
  });
201
201
 
202
202
  it('overall conversion = final / first', () => {
@@ -222,9 +222,9 @@ describe('conversionFunnelAnalysis', () => {
222
222
 
223
223
  describe('abcClassification', () => {
224
224
  const items = [
225
- { id: 'SKU-1', annualVolume: 100, unitCost: 500 }, // $50k — A
226
- { id: 'SKU-2', annualVolume: 500, unitCost: 20 }, // $10k — A/B
227
- { id: 'SKU-3', annualVolume: 1000, unitCost: 5 }, // $5k — B
225
+ { id: 'SKU-1', annualVolume: 100, unitCost: 500 }, // $50k — A
226
+ { id: 'SKU-2', annualVolume: 500, unitCost: 20 }, // $10k — A/B
227
+ { id: 'SKU-3', annualVolume: 1000, unitCost: 5 }, // $5k — B
228
228
  { id: 'SKU-4', annualVolume: 5000, unitCost: 0.5 }, // $2.5k — C
229
229
  { id: 'SKU-5', annualVolume: 2000, unitCost: 0.2 }, // $400 — C
230
230
  ];
@@ -303,13 +303,13 @@ describe('buildRetailReceipt', () => {
303
303
  });
304
304
 
305
305
  it('accepted=true for healthy metrics', () => {
306
- const clv = customerLifetimeValue(200, 5, 3, 0.10, 150); // CLV:CAC ≈ 3+
306
+ const clv = customerLifetimeValue(200, 5, 3, 0.1, 150); // CLV:CAC ≈ 3+
307
307
  const receipt = buildRetailReceipt({ clv, converged: true });
308
308
  expect(receipt.acceptance.accepted).toBe(true);
309
309
  });
310
310
 
311
311
  it('accepted=false when CLV:CAC < 3', () => {
312
- const clv = customerLifetimeValue(50, 1, 1, 0.10, 500); // poor CLV:CAC
312
+ const clv = customerLifetimeValue(50, 1, 1, 0.1, 500); // poor CLV:CAC
313
313
  const receipt = buildRetailReceipt({ clv, converged: true });
314
314
  if (clv.clvToCac! < 3) {
315
315
  expect(receipt.acceptance.accepted).toBe(false);
@@ -79,9 +79,7 @@ describe('retail-ecommerce -> HoloScript runtime integration (eoq)', () => {
79
79
  await flush();
80
80
 
81
81
  const state = runtime.getState() as Record<string, unknown>;
82
- const persisted = state['eoq:retail'] as
83
- | { eoq?: number; annualDemand?: number }
84
- | undefined;
82
+ const persisted = state['eoq:retail'] as { eoq?: number; annualDemand?: number } | undefined;
85
83
  expect(persisted).toBeDefined();
86
84
  expect(persisted?.eoq).toBeCloseTo(EXPECTED_EOQ, 5);
87
85
  expect(persisted?.annualDemand).toBe(1000);
@@ -97,9 +95,7 @@ describe('retail-ecommerce -> HoloScript runtime integration (eoq)', () => {
97
95
  });
98
96
 
99
97
  // holdingCostPerUnit omitted -> the handler emits eoq_error, never throws.
100
- await runtime.executeNode(
101
- eoqOrb({ annualDemand: 1000, orderingCost: 10 }) as never,
102
- );
98
+ await runtime.executeNode(eoqOrb({ annualDemand: 1000, orderingCost: 10 }) as never);
103
99
  await flush();
104
100
 
105
101
  expect(errors).toHaveLength(1);
package/src/index.ts CHANGED
@@ -1,7 +1,26 @@
1
- export { createCartHandler, type CartConfig, type CartItem, type CartState } from './traits/CartTrait';
2
- export { createCheckoutHandler, type CheckoutConfig, type CheckoutStep, type PaymentMethod, type Address } from './traits/CheckoutTrait';
3
- export { createProductCatalogHandler, type ProductCatalogConfig, type Product } from './traits/ProductCatalogTrait';
4
- export { createShippingRateHandler, type ShippingRateConfig, type ShippingMethod } from './traits/ShippingRateTrait';
1
+ export {
2
+ createCartHandler,
3
+ type CartConfig,
4
+ type CartItem,
5
+ type CartState,
6
+ } from './traits/CartTrait';
7
+ export {
8
+ createCheckoutHandler,
9
+ type CheckoutConfig,
10
+ type CheckoutStep,
11
+ type PaymentMethod,
12
+ type Address,
13
+ } from './traits/CheckoutTrait';
14
+ export {
15
+ createProductCatalogHandler,
16
+ type ProductCatalogConfig,
17
+ type Product,
18
+ } from './traits/ProductCatalogTrait';
19
+ export {
20
+ createShippingRateHandler,
21
+ type ShippingRateConfig,
22
+ type ShippingMethod,
23
+ } from './traits/ShippingRateTrait';
5
24
  export { createReturnHandler, type ReturnConfig, type ReturnStatus } from './traits/ReturnTrait';
6
25
  export * from './traits/types';
7
26
 
@@ -12,7 +31,29 @@ import { createShippingRateHandler } from './traits/ShippingRateTrait';
12
31
  import { createReturnHandler } from './traits/ReturnTrait';
13
32
 
14
33
  export * from './retailsolver';
15
- export * from './runtime';
16
34
 
17
- export const pluginMeta = { name: '@holoscript/plugin-retail-ecommerce', version: '1.0.0', traits: ['cart', 'checkout', 'product_catalog', 'shipping_rate', 'return', 'eoq', 'price_optimization', 'markdown', 'clv', 'conversion_funnel', 'abc_classification', 'inventory_metrics'] };
18
- export const traitHandlers = [createCartHandler(), createCheckoutHandler(), createProductCatalogHandler(), createShippingRateHandler(), createReturnHandler()];
35
+ export const pluginMeta = {
36
+ name: '@holoscript/plugin-retail-ecommerce',
37
+ version: '1.0.0',
38
+ traits: [
39
+ 'cart',
40
+ 'checkout',
41
+ 'product_catalog',
42
+ 'shipping_rate',
43
+ 'return',
44
+ 'eoq',
45
+ 'price_optimization',
46
+ 'markdown',
47
+ 'clv',
48
+ 'conversion_funnel',
49
+ 'abc_classification',
50
+ 'inventory_metrics',
51
+ ],
52
+ };
53
+ export const traitHandlers = [
54
+ createCartHandler(),
55
+ createCheckoutHandler(),
56
+ createProductCatalogHandler(),
57
+ createShippingRateHandler(),
58
+ createReturnHandler(),
59
+ ];
@@ -62,7 +62,12 @@ export interface MarkdownResult {
62
62
  /** Days until season end */
63
63
  daysRemaining: number;
64
64
  /** Optimal markdown schedule */
65
- schedule: Array<{ dayOfMarkdown: number; discountPct: number; price: number; expectedUnits: number }>;
65
+ schedule: Array<{
66
+ dayOfMarkdown: number;
67
+ discountPct: number;
68
+ price: number;
69
+ expectedUnits: number;
70
+ }>;
66
71
  /** Expected total revenue */
67
72
  expectedRevenue: number;
68
73
  /** Expected unsold units */
@@ -126,7 +131,7 @@ export function economicOrderQuantity(
126
131
  orderingCost: number,
127
132
  holdingCostPerUnit: number,
128
133
  leadTimeDays = 0,
129
- workingDaysPerYear = 250,
134
+ workingDaysPerYear = 250
130
135
  ): EOQResult {
131
136
  if (annualDemand <= 0) throw new Error('annualDemand must be positive');
132
137
  if (orderingCost <= 0) throw new Error('orderingCost must be positive');
@@ -138,7 +143,15 @@ export function economicOrderQuantity(
138
143
  const dailyDemand = annualDemand / workingDaysPerYear;
139
144
  const reorderPoint = dailyDemand * leadTimeDays;
140
145
 
141
- return { annualDemand, orderingCost, holdingCostPerUnit, eoq, ordersPerYear, totalAnnualCost, reorderPoint };
146
+ return {
147
+ annualDemand,
148
+ orderingCost,
149
+ holdingCostPerUnit,
150
+ eoq,
151
+ ordersPerYear,
152
+ totalAnnualCost,
153
+ reorderPoint,
154
+ };
142
155
  }
143
156
 
144
157
  // ─── Price optimization via demand elasticity ─────────────────────────────────
@@ -159,7 +172,7 @@ export function priceOptimization(
159
172
  currentDemand: number,
160
173
  elasticity: number,
161
174
  variableCost = 0,
162
- priceRange?: [number, number],
175
+ priceRange?: [number, number]
163
176
  ): PriceOptimizationResult {
164
177
  if (currentPrice <= 0) throw new Error('currentPrice must be positive');
165
178
  if (currentDemand <= 0) throw new Error('currentDemand must be positive');
@@ -167,22 +180,24 @@ export function priceOptimization(
167
180
 
168
181
  const demandAt = (p: number) => currentDemand * Math.pow(p / currentPrice, elasticity);
169
182
  const revenueAt = (p: number) => p * demandAt(p);
170
- const profitAt = (p: number) => (p - variableCost) * demandAt(p);
183
+ const profitAt = (p: number) => (p - variableCost) * demandAt(p);
171
184
 
172
185
  // Profit-maximizing price via ternary search
173
186
  const pLo = priceRange ? priceRange[0] : variableCost * 1.01;
174
187
  const pHi = priceRange ? priceRange[1] : currentPrice * 3;
175
188
 
176
- let lo = Math.max(pLo, variableCost + 0.01), hi = pHi;
189
+ let lo = Math.max(pLo, variableCost + 0.01),
190
+ hi = pHi;
177
191
  for (let i = 0; i < 100; i++) {
178
192
  const m1 = lo + (hi - lo) / 3;
179
193
  const m2 = hi - (hi - lo) / 3;
180
- if (profitAt(m1) < profitAt(m2)) lo = m1; else hi = m2;
194
+ if (profitAt(m1) < profitAt(m2)) lo = m1;
195
+ else hi = m2;
181
196
  }
182
197
  const optimalPrice = (lo + hi) / 2;
183
198
 
184
- const currentRevenue = revenueAt(currentPrice);
185
- const optimalRevenue = revenueAt(optimalPrice);
199
+ const currentRevenue = revenueAt(currentPrice);
200
+ const optimalRevenue = revenueAt(optimalPrice);
186
201
  const revenueUpliftPct = ((optimalRevenue - currentRevenue) / currentRevenue) * 100;
187
202
 
188
203
  // Price-demand curve: 20 points from 50% to 200% of current price
@@ -191,7 +206,15 @@ export function priceOptimization(
191
206
  return { price: p, demand: demandAt(p), revenue: revenueAt(p) };
192
207
  });
193
208
 
194
- return { currentPrice, elasticity, optimalPrice, currentRevenue, optimalRevenue, revenueUpliftPct, priceDemandCurve };
209
+ return {
210
+ currentPrice,
211
+ elasticity,
212
+ optimalPrice,
213
+ currentRevenue,
214
+ optimalRevenue,
215
+ revenueUpliftPct,
216
+ priceDemandCurve,
217
+ };
195
218
  }
196
219
 
197
220
  // ─── Markdown optimization ────────────────────────────────────────────────────
@@ -206,7 +229,7 @@ export function markdownOptimization(
206
229
  daysRemaining: number,
207
230
  baseDailySalesRate: number,
208
231
  elasticity: number,
209
- markdownSteps: number[] = [10, 20, 30, 50],
232
+ markdownSteps: number[] = [10, 20, 30, 50]
210
233
  ): MarkdownResult {
211
234
  if (originalPrice <= 0) throw new Error('originalPrice must be positive');
212
235
  if (inventory <= 0) throw new Error('inventory must be positive');
@@ -236,7 +259,12 @@ export function markdownOptimization(
236
259
  totalRevenue += unitsSold * currentPrice;
237
260
  remaining = Math.max(0, remaining - unitsSold);
238
261
 
239
- schedule.push({ dayOfMarkdown, discountPct, price: newPrice, expectedUnits: Math.round(newRate * stepSize) });
262
+ schedule.push({
263
+ dayOfMarkdown,
264
+ discountPct,
265
+ price: newPrice,
266
+ expectedUnits: Math.round(newRate * stepSize),
267
+ });
240
268
 
241
269
  currentPrice = newPrice;
242
270
  currentRate = newRate;
@@ -249,7 +277,14 @@ export function markdownOptimization(
249
277
  totalRevenue += finalUnits * currentPrice;
250
278
  remaining = Math.max(0, remaining - finalUnits);
251
279
 
252
- return { originalPrice, inventory, daysRemaining, schedule, expectedRevenue: totalRevenue, expectedUnsold: remaining };
280
+ return {
281
+ originalPrice,
282
+ inventory,
283
+ daysRemaining,
284
+ schedule,
285
+ expectedRevenue: totalRevenue,
286
+ expectedUnsold: remaining,
287
+ };
253
288
  }
254
289
 
255
290
  // ─── Customer Lifetime Value ──────────────────────────────────────────────────
@@ -264,7 +299,7 @@ export function customerLifetimeValue(
264
299
  purchaseFrequency: number,
265
300
  lifespanYears: number,
266
301
  discountRate: number,
267
- cac?: number,
302
+ cac?: number
268
303
  ): CLVResult {
269
304
  if (avgOrderValue <= 0) throw new Error('avgOrderValue must be positive');
270
305
  if (purchaseFrequency <= 0) throw new Error('purchaseFrequency must be positive');
@@ -283,7 +318,15 @@ export function customerLifetimeValue(
283
318
 
284
319
  const clvToCac = cac != null && cac > 0 ? discountedCLV / cac : null;
285
320
 
286
- return { avgOrderValue, purchaseFrequency, lifespanYears, discountRate, simpleCLV, discountedCLV, clvToCac };
321
+ return {
322
+ avgOrderValue,
323
+ purchaseFrequency,
324
+ lifespanYears,
325
+ discountRate,
326
+ simpleCLV,
327
+ discountedCLV,
328
+ clvToCac,
329
+ };
287
330
  }
288
331
 
289
332
  // ─── Conversion funnel analysis ───────────────────────────────────────────────
@@ -294,25 +337,25 @@ export function customerLifetimeValue(
294
337
  export function conversionFunnelAnalysis(
295
338
  stages: string[],
296
339
  counts: number[],
297
- avgOrderValue: number,
340
+ avgOrderValue: number
298
341
  ): FunnelAnalysisResult {
299
342
  if (stages.length !== counts.length) throw new Error('stages and counts must have same length');
300
343
  if (stages.length < 2) throw new Error('Funnel needs at least 2 stages');
301
- if (counts.some(c => c < 0)) throw new Error('counts must be non-negative');
344
+ if (counts.some((c) => c < 0)) throw new Error('counts must be non-negative');
302
345
  if (avgOrderValue <= 0) throw new Error('avgOrderValue must be positive');
303
346
 
304
- const stepConversions = counts.slice(1).map((c, i) => counts[i] > 0 ? c / counts[i] : 0);
347
+ const stepConversions = counts.slice(1).map((c, i) => (counts[i] > 0 ? c / counts[i] : 0));
305
348
  const overallConversion = counts[0] > 0 ? counts[counts.length - 1] / counts[0] : 0;
306
349
  const currentRevenue = counts[counts.length - 1] * avgOrderValue;
307
350
 
308
351
  // Sensitivity: if step i conversion improves by 10pp, how much extra revenue?
309
352
  const sensitivityRevenue = stepConversions.map((conv, i) => {
310
- const newConv = Math.min(1, conv + 0.10);
353
+ const newConv = Math.min(1, conv + 0.1);
311
354
  const upliftFactor = newConv / (conv || 1e-9);
312
355
  // Downstream: all subsequent stages are multiplied by this factor
313
356
  let newFinal = counts[i + 1] * upliftFactor;
314
357
  for (let j = i + 2; j < counts.length; j++) {
315
- newFinal *= (counts[j - 1] > 0 ? counts[j] / counts[j - 1] : 1);
358
+ newFinal *= counts[j - 1] > 0 ? counts[j] / counts[j - 1] : 1;
316
359
  }
317
360
  return (newFinal - counts[counts.length - 1]) * avgOrderValue;
318
361
  });
@@ -329,21 +372,23 @@ export function conversionFunnelAnalysis(
329
372
  * C: remaining 5% of value
330
373
  */
331
374
  export function abcClassification(
332
- items: Array<{ id: string; annualVolume: number; unitCost: number }>,
375
+ items: Array<{ id: string; annualVolume: number; unitCost: number }>
333
376
  ): ABCClassification {
334
377
  if (items.length === 0) throw new Error('No items provided');
335
378
 
336
- const withValues = items.map(item => ({
337
- id: item.id,
338
- annualValue: item.annualVolume * item.unitCost,
339
- })).sort((a, b) => b.annualValue - a.annualValue);
379
+ const withValues = items
380
+ .map((item) => ({
381
+ id: item.id,
382
+ annualValue: item.annualVolume * item.unitCost,
383
+ }))
384
+ .sort((a, b) => b.annualValue - a.annualValue);
340
385
 
341
386
  const totalValue = withValues.reduce((a, b) => a + b.annualValue, 0);
342
387
  let cumulative = 0;
343
388
  const classCounts = { A: 0, B: 0, C: 0 };
344
- const classValue = { A: 0, B: 0, C: 0 };
389
+ const classValue = { A: 0, B: 0, C: 0 };
345
390
 
346
- const classified = withValues.map(item => {
391
+ const classified = withValues.map((item) => {
347
392
  cumulative += item.annualValue;
348
393
  const cumulativePct = (cumulative / totalValue) * 100;
349
394
  const cls: 'A' | 'B' | 'C' = cumulativePct <= 80 ? 'A' : cumulativePct <= 95 ? 'B' : 'C';
@@ -381,7 +426,7 @@ export interface InventoryMetrics {
381
426
  export function inventoryMetrics(
382
427
  cogs: number,
383
428
  avgInventory: number,
384
- targetDsi?: number,
429
+ targetDsi?: number
385
430
  ): InventoryMetrics {
386
431
  if (cogs <= 0) throw new Error('COGS must be positive');
387
432
  if (avgInventory <= 0) throw new Error('avgInventory must be positive');
@@ -390,7 +435,10 @@ export function inventoryMetrics(
390
435
  const dsi = 365 / inventoryTurnover;
391
436
 
392
437
  return {
393
- cogs, avgInventory, inventoryTurnover, dsi,
438
+ cogs,
439
+ avgInventory,
440
+ inventoryTurnover,
441
+ dsi,
394
442
  targetDsi,
395
443
  withinTarget: targetDsi != null ? dsi <= targetDsi : undefined,
396
444
  };
@@ -411,7 +459,7 @@ export interface RetailAnalysisResult {
411
459
 
412
460
  export function buildRetailReceipt(
413
461
  result: RetailAnalysisResult,
414
- options?: RetailReceiptOptions,
462
+ options?: RetailReceiptOptions
415
463
  ): DomainSimulationReceipt {
416
464
  const violations: Array<{ criterion: string; message: string }> = [];
417
465
 
@@ -440,7 +488,11 @@ export function buildRetailReceipt(
440
488
  clv: result.clv?.discountedCLV ?? null,
441
489
  overallConversion: result.funnel?.overallConversion ?? null,
442
490
  },
443
- cael: { version: 'cael.v1', event: 'retail_ecommerce.retail_analysis', solverType: 'retail-ecommerce.analytics' },
491
+ cael: {
492
+ version: 'cael.v1',
493
+ event: 'retail_ecommerce.retail_analysis',
494
+ solverType: 'retail-ecommerce.analytics',
495
+ },
444
496
  acceptance: { accepted: violations.length === 0, violations },
445
497
  });
446
498
  }
package/src/runtime.ts CHANGED
@@ -63,7 +63,7 @@ export interface RuntimeTraitHandler {
63
63
  node: unknown,
64
64
  config: EoqTraitConfig,
65
65
  context: TraitDispatchContext,
66
- delta: number,
66
+ delta: number
67
67
  ) => void;
68
68
  }
69
69
 
@@ -78,7 +78,7 @@ interface EoqNode {
78
78
  function solveOntoNode(
79
79
  node: unknown,
80
80
  config: EoqTraitConfig | undefined,
81
- context: TraitDispatchContext,
81
+ context: TraitDispatchContext
82
82
  ): void {
83
83
  const carrier = node as EoqNode;
84
84
  const nodeId = carrier.id ?? carrier.name ?? 'unknown';
@@ -106,7 +106,7 @@ function solveOntoNode(
106
106
  orderingCost,
107
107
  holdingCostPerUnit,
108
108
  config?.leadTimeDays ?? 0,
109
- config?.workingDaysPerYear ?? 250,
109
+ config?.workingDaysPerYear ?? 250
110
110
  );
111
111
  carrier.__eoqResult = result;
112
112
  carrier.properties = {
@@ -1,39 +1,72 @@
1
1
  /** @cart Trait — Shopping cart management. @trait cart */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export interface CartItem { productId: string; quantity: number; price: number; variant?: string; name: string; }
5
- export interface CartConfig { currency: string; items: CartItem[]; maxItems: number; taxRate: number; }
6
- export interface CartState { items: CartItem[]; subtotal: number; tax: number; total: number; itemCount: number; }
4
+ export interface CartItem {
5
+ productId: string;
6
+ quantity: number;
7
+ price: number;
8
+ variant?: string;
9
+ name: string;
10
+ }
11
+ export interface CartConfig {
12
+ currency: string;
13
+ items: CartItem[];
14
+ maxItems: number;
15
+ taxRate: number;
16
+ }
17
+ export interface CartState {
18
+ items: CartItem[];
19
+ subtotal: number;
20
+ tax: number;
21
+ total: number;
22
+ itemCount: number;
23
+ }
7
24
 
8
25
  const defaultConfig: CartConfig = { currency: 'USD', items: [], maxItems: 99, taxRate: 0 };
9
26
 
10
- function computeTotals(items: CartItem[], taxRate: number): Pick<CartState, 'subtotal' | 'tax' | 'total' | 'itemCount'> {
27
+ function computeTotals(
28
+ items: CartItem[],
29
+ taxRate: number
30
+ ): Pick<CartState, 'subtotal' | 'tax' | 'total' | 'itemCount'> {
11
31
  const subtotal = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
12
32
  const tax = subtotal * taxRate;
13
- return { subtotal, tax, total: subtotal + tax, itemCount: items.reduce((sum, i) => sum + i.quantity, 0) };
33
+ return {
34
+ subtotal,
35
+ tax,
36
+ total: subtotal + tax,
37
+ itemCount: items.reduce((sum, i) => sum + i.quantity, 0),
38
+ };
14
39
  }
15
40
 
16
41
  export function createCartHandler(): TraitHandler<CartConfig> {
17
42
  return {
18
- name: 'cart', defaultConfig,
43
+ name: 'cart',
44
+ defaultConfig,
19
45
  onAttach(node: HSPlusNode, config: CartConfig, ctx: TraitContext) {
20
46
  const totals = computeTotals(config.items, config.taxRate);
21
47
  node.__cartState = { items: [...config.items], ...totals };
22
48
  ctx.emit?.('cart:created', { itemCount: totals.itemCount });
23
49
  },
24
- onDetach(node: HSPlusNode, _c: CartConfig, ctx: TraitContext) { delete node.__cartState; ctx.emit?.('cart:cleared'); },
50
+ onDetach(node: HSPlusNode, _c: CartConfig, ctx: TraitContext) {
51
+ delete node.__cartState;
52
+ ctx.emit?.('cart:cleared');
53
+ },
25
54
  onUpdate() {},
26
55
  onEvent(node: HSPlusNode, config: CartConfig, ctx: TraitContext, event: TraitEvent) {
27
- const s = node.__cartState as CartState | undefined; if (!s) return;
56
+ const s = node.__cartState as CartState | undefined;
57
+ if (!s) return;
28
58
  if (event.type === 'cart:add_item') {
29
59
  const item = event.payload as unknown as CartItem;
30
- const existing = s.items.find(i => i.productId === item.productId && i.variant === item.variant);
31
- if (existing) existing.quantity += item.quantity; else s.items.push({ ...item });
60
+ const existing = s.items.find(
61
+ (i) => i.productId === item.productId && i.variant === item.variant
62
+ );
63
+ if (existing) existing.quantity += item.quantity;
64
+ else s.items.push({ ...item });
32
65
  Object.assign(s, computeTotals(s.items, config.taxRate));
33
66
  ctx.emit?.('cart:updated', { itemCount: s.itemCount, total: s.total });
34
67
  }
35
68
  if (event.type === 'cart:remove_item') {
36
- s.items = s.items.filter(i => i.productId !== (event.payload?.productId as string));
69
+ s.items = s.items.filter((i) => i.productId !== (event.payload?.productId as string));
37
70
  Object.assign(s, computeTotals(s.items, config.taxRate));
38
71
  ctx.emit?.('cart:updated', { itemCount: s.itemCount, total: s.total });
39
72
  }
@@ -2,28 +2,83 @@
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
4
  export type CheckoutStep = 'shipping' | 'payment' | 'review' | 'confirm' | 'complete';
5
- export type PaymentMethod = 'credit_card' | 'debit_card' | 'paypal' | 'crypto' | 'bank_transfer' | 'apple_pay';
6
- export interface Address { line1: string; line2?: string; city: string; state: string; postalCode: string; country: string; }
7
- export interface CheckoutConfig { step: CheckoutStep; paymentMethod: PaymentMethod; shippingAddress: Address | null; billingAddress: Address | null; orderTotal: number; tax: number; }
8
- export interface CheckoutState { currentStep: CheckoutStep; isProcessing: boolean; orderId: string | null; error: string | null; }
5
+ export type PaymentMethod =
6
+ | 'credit_card'
7
+ | 'debit_card'
8
+ | 'paypal'
9
+ | 'crypto'
10
+ | 'bank_transfer'
11
+ | 'apple_pay';
12
+ export interface Address {
13
+ line1: string;
14
+ line2?: string;
15
+ city: string;
16
+ state: string;
17
+ postalCode: string;
18
+ country: string;
19
+ }
20
+ export interface CheckoutConfig {
21
+ step: CheckoutStep;
22
+ paymentMethod: PaymentMethod;
23
+ shippingAddress: Address | null;
24
+ billingAddress: Address | null;
25
+ orderTotal: number;
26
+ tax: number;
27
+ }
28
+ export interface CheckoutState {
29
+ currentStep: CheckoutStep;
30
+ isProcessing: boolean;
31
+ orderId: string | null;
32
+ error: string | null;
33
+ }
9
34
 
10
- const defaultConfig: CheckoutConfig = { step: 'shipping', paymentMethod: 'credit_card', shippingAddress: null, billingAddress: null, orderTotal: 0, tax: 0 };
35
+ const defaultConfig: CheckoutConfig = {
36
+ step: 'shipping',
37
+ paymentMethod: 'credit_card',
38
+ shippingAddress: null,
39
+ billingAddress: null,
40
+ orderTotal: 0,
41
+ tax: 0,
42
+ };
11
43
 
12
44
  export function createCheckoutHandler(): TraitHandler<CheckoutConfig> {
13
45
  return {
14
- name: 'checkout', defaultConfig,
15
- onAttach(node: HSPlusNode, config: CheckoutConfig, ctx: TraitContext) { node.__checkoutState = { currentStep: config.step, isProcessing: false, orderId: null, error: null }; ctx.emit?.('checkout:started'); },
16
- onDetach(node: HSPlusNode, _c: CheckoutConfig, ctx: TraitContext) { delete node.__checkoutState; ctx.emit?.('checkout:abandoned'); },
46
+ name: 'checkout',
47
+ defaultConfig,
48
+ onAttach(node: HSPlusNode, config: CheckoutConfig, ctx: TraitContext) {
49
+ node.__checkoutState = {
50
+ currentStep: config.step,
51
+ isProcessing: false,
52
+ orderId: null,
53
+ error: null,
54
+ };
55
+ ctx.emit?.('checkout:started');
56
+ },
57
+ onDetach(node: HSPlusNode, _c: CheckoutConfig, ctx: TraitContext) {
58
+ delete node.__checkoutState;
59
+ ctx.emit?.('checkout:abandoned');
60
+ },
17
61
  onUpdate() {},
18
62
  onEvent(node: HSPlusNode, _c: CheckoutConfig, ctx: TraitContext, event: TraitEvent) {
19
- const s = node.__checkoutState as CheckoutState | undefined; if (!s) return;
63
+ const s = node.__checkoutState as CheckoutState | undefined;
64
+ if (!s) return;
20
65
  if (event.type === 'checkout:next_step') {
21
66
  const steps: CheckoutStep[] = ['shipping', 'payment', 'review', 'confirm', 'complete'];
22
67
  const idx = steps.indexOf(s.currentStep);
23
- if (idx < steps.length - 1) { s.currentStep = steps[idx + 1]; ctx.emit?.('checkout:step_changed', { step: s.currentStep }); }
68
+ if (idx < steps.length - 1) {
69
+ s.currentStep = steps[idx + 1];
70
+ ctx.emit?.('checkout:step_changed', { step: s.currentStep });
71
+ }
72
+ }
73
+ if (event.type === 'checkout:process_payment') {
74
+ s.isProcessing = true;
75
+ ctx.emit?.('checkout:processing');
76
+ }
77
+ if (event.type === 'checkout:complete') {
78
+ s.currentStep = 'complete';
79
+ s.orderId = event.payload?.orderId as string;
80
+ ctx.emit?.('checkout:completed', { orderId: s.orderId });
24
81
  }
25
- if (event.type === 'checkout:process_payment') { s.isProcessing = true; ctx.emit?.('checkout:processing'); }
26
- if (event.type === 'checkout:complete') { s.currentStep = 'complete'; s.orderId = event.payload?.orderId as string; ctx.emit?.('checkout:completed', { orderId: s.orderId }); }
27
82
  },
28
83
  };
29
84
  }
@@ -1,26 +1,60 @@
1
1
  /** @product_catalog Trait — Product catalog with search and filtering. @trait product_catalog */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export interface Product { id: string; name: string; price: number; category: string; brand?: string; inStock: boolean; imageUrl?: string; }
5
- export interface ProductCatalogConfig { products: Product[]; categories: string[]; sortBy: 'price_asc' | 'price_desc' | 'name' | 'newest'; pageSize: number; }
4
+ export interface Product {
5
+ id: string;
6
+ name: string;
7
+ price: number;
8
+ category: string;
9
+ brand?: string;
10
+ inStock: boolean;
11
+ imageUrl?: string;
12
+ }
13
+ export interface ProductCatalogConfig {
14
+ products: Product[];
15
+ categories: string[];
16
+ sortBy: 'price_asc' | 'price_desc' | 'name' | 'newest';
17
+ pageSize: number;
18
+ }
6
19
 
7
- const defaultConfig: ProductCatalogConfig = { products: [], categories: [], sortBy: 'name', pageSize: 20 };
20
+ const defaultConfig: ProductCatalogConfig = {
21
+ products: [],
22
+ categories: [],
23
+ sortBy: 'name',
24
+ pageSize: 20,
25
+ };
8
26
 
9
27
  export function createProductCatalogHandler(): TraitHandler<ProductCatalogConfig> {
10
28
  return {
11
- name: 'product_catalog', defaultConfig,
12
- onAttach(node: HSPlusNode, config: ProductCatalogConfig, ctx: TraitContext) { node.__catalogState = { currentPage: 0, filteredCount: config.products.length, activeFilters: {} }; ctx.emit?.('catalog:loaded', { productCount: config.products.length }); },
13
- onDetach(node: HSPlusNode, _c: ProductCatalogConfig, ctx: TraitContext) { delete node.__catalogState; ctx.emit?.('catalog:unloaded'); },
29
+ name: 'product_catalog',
30
+ defaultConfig,
31
+ onAttach(node: HSPlusNode, config: ProductCatalogConfig, ctx: TraitContext) {
32
+ node.__catalogState = {
33
+ currentPage: 0,
34
+ filteredCount: config.products.length,
35
+ activeFilters: {},
36
+ };
37
+ ctx.emit?.('catalog:loaded', { productCount: config.products.length });
38
+ },
39
+ onDetach(node: HSPlusNode, _c: ProductCatalogConfig, ctx: TraitContext) {
40
+ delete node.__catalogState;
41
+ ctx.emit?.('catalog:unloaded');
42
+ },
14
43
  onUpdate() {},
15
44
  onEvent(node: HSPlusNode, config: ProductCatalogConfig, ctx: TraitContext, event: TraitEvent) {
16
45
  if (event.type === 'catalog:search') {
17
- const query = (event.payload?.query as string || '').toLowerCase();
18
- const results = config.products.filter(p => p.name.toLowerCase().includes(query));
19
- ctx.emit?.('catalog:results', { count: results.length, products: results.slice(0, config.pageSize) });
46
+ const query = ((event.payload?.query as string) || '').toLowerCase();
47
+ const results = config.products.filter((p) => p.name.toLowerCase().includes(query));
48
+ ctx.emit?.('catalog:results', {
49
+ count: results.length,
50
+ products: results.slice(0, config.pageSize),
51
+ });
20
52
  }
21
53
  if (event.type === 'catalog:filter') {
22
54
  const category = event.payload?.category as string;
23
- const results = category ? config.products.filter(p => p.category === category) : config.products;
55
+ const results = category
56
+ ? config.products.filter((p) => p.category === category)
57
+ : config.products;
24
58
  ctx.emit?.('catalog:filtered', { count: results.length, category });
25
59
  }
26
60
  },
@@ -1,23 +1,61 @@
1
1
  /** @return Trait — Return and refund management. @trait return */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export type ReturnStatus = 'requested' | 'approved' | 'shipped' | 'received' | 'refunded' | 'denied';
5
- export interface ReturnConfig { orderId: string; reason: string; status: ReturnStatus; refundAmount: number; returnWindowDays: number; }
4
+ export type ReturnStatus =
5
+ | 'requested'
6
+ | 'approved'
7
+ | 'shipped'
8
+ | 'received'
9
+ | 'refunded'
10
+ | 'denied';
11
+ export interface ReturnConfig {
12
+ orderId: string;
13
+ reason: string;
14
+ status: ReturnStatus;
15
+ refundAmount: number;
16
+ returnWindowDays: number;
17
+ }
6
18
 
7
- const defaultConfig: ReturnConfig = { orderId: '', reason: '', status: 'requested', refundAmount: 0, returnWindowDays: 30 };
19
+ const defaultConfig: ReturnConfig = {
20
+ orderId: '',
21
+ reason: '',
22
+ status: 'requested',
23
+ refundAmount: 0,
24
+ returnWindowDays: 30,
25
+ };
8
26
 
9
27
  export function createReturnHandler(): TraitHandler<ReturnConfig> {
10
28
  return {
11
- name: 'return', defaultConfig,
12
- onAttach(node: HSPlusNode, config: ReturnConfig, ctx: TraitContext) { node.__returnState = { ...config, requestedAt: Date.now() }; ctx.emit?.('return:created', { orderId: config.orderId }); },
13
- onDetach(node: HSPlusNode, _c: ReturnConfig, ctx: TraitContext) { delete node.__returnState; ctx.emit?.('return:cancelled'); },
29
+ name: 'return',
30
+ defaultConfig,
31
+ onAttach(node: HSPlusNode, config: ReturnConfig, ctx: TraitContext) {
32
+ node.__returnState = { ...config, requestedAt: Date.now() };
33
+ ctx.emit?.('return:created', { orderId: config.orderId });
34
+ },
35
+ onDetach(node: HSPlusNode, _c: ReturnConfig, ctx: TraitContext) {
36
+ delete node.__returnState;
37
+ ctx.emit?.('return:cancelled');
38
+ },
14
39
  onUpdate() {},
15
40
  onEvent(node: HSPlusNode, _c: ReturnConfig, ctx: TraitContext, event: TraitEvent) {
16
- const s = node.__returnState as Record<string, unknown> | undefined; if (!s) return;
17
- if (event.type === 'return:approve') { s.status = 'approved'; ctx.emit?.('return:approved'); }
18
- if (event.type === 'return:ship') { s.status = 'shipped'; ctx.emit?.('return:shipped'); }
19
- if (event.type === 'return:receive') { s.status = 'received'; ctx.emit?.('return:received'); }
20
- if (event.type === 'return:refund') { s.status = 'refunded'; ctx.emit?.('return:refunded', { amount: s.refundAmount }); }
41
+ const s = node.__returnState as Record<string, unknown> | undefined;
42
+ if (!s) return;
43
+ if (event.type === 'return:approve') {
44
+ s.status = 'approved';
45
+ ctx.emit?.('return:approved');
46
+ }
47
+ if (event.type === 'return:ship') {
48
+ s.status = 'shipped';
49
+ ctx.emit?.('return:shipped');
50
+ }
51
+ if (event.type === 'return:receive') {
52
+ s.status = 'received';
53
+ ctx.emit?.('return:received');
54
+ }
55
+ if (event.type === 'return:refund') {
56
+ s.status = 'refunded';
57
+ ctx.emit?.('return:refunded', { amount: s.refundAmount });
58
+ }
21
59
  },
22
60
  };
23
61
  }
@@ -2,18 +2,49 @@
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
4
  export type ShippingMethod = 'standard' | 'express' | 'overnight' | 'freight' | 'pickup';
5
- export interface ShippingRateConfig { carrier: string; method: ShippingMethod; weightKg: number; dimensions: { lengthCm: number; widthCm: number; heightCm: number; }; origin: string; destination: string; rate: number; estimatedDays: number; }
5
+ export interface ShippingRateConfig {
6
+ carrier: string;
7
+ method: ShippingMethod;
8
+ weightKg: number;
9
+ dimensions: { lengthCm: number; widthCm: number; heightCm: number };
10
+ origin: string;
11
+ destination: string;
12
+ rate: number;
13
+ estimatedDays: number;
14
+ }
6
15
 
7
- const defaultConfig: ShippingRateConfig = { carrier: '', method: 'standard', weightKg: 0, dimensions: { lengthCm: 0, widthCm: 0, heightCm: 0 }, origin: '', destination: '', rate: 0, estimatedDays: 5 };
16
+ const defaultConfig: ShippingRateConfig = {
17
+ carrier: '',
18
+ method: 'standard',
19
+ weightKg: 0,
20
+ dimensions: { lengthCm: 0, widthCm: 0, heightCm: 0 },
21
+ origin: '',
22
+ destination: '',
23
+ rate: 0,
24
+ estimatedDays: 5,
25
+ };
8
26
 
9
27
  export function createShippingRateHandler(): TraitHandler<ShippingRateConfig> {
10
28
  return {
11
- name: 'shipping_rate', defaultConfig,
12
- onAttach(node: HSPlusNode, config: ShippingRateConfig, ctx: TraitContext) { node.__shippingState = { calculatedRate: config.rate, carrier: config.carrier }; ctx.emit?.('shipping:rate_set', { rate: config.rate, method: config.method }); },
13
- onDetach(node: HSPlusNode, _c: ShippingRateConfig, ctx: TraitContext) { delete node.__shippingState; ctx.emit?.('shipping:cleared'); },
29
+ name: 'shipping_rate',
30
+ defaultConfig,
31
+ onAttach(node: HSPlusNode, config: ShippingRateConfig, ctx: TraitContext) {
32
+ node.__shippingState = { calculatedRate: config.rate, carrier: config.carrier };
33
+ ctx.emit?.('shipping:rate_set', { rate: config.rate, method: config.method });
34
+ },
35
+ onDetach(node: HSPlusNode, _c: ShippingRateConfig, ctx: TraitContext) {
36
+ delete node.__shippingState;
37
+ ctx.emit?.('shipping:cleared');
38
+ },
14
39
  onUpdate() {},
15
40
  onEvent(_n: HSPlusNode, config: ShippingRateConfig, ctx: TraitContext, event: TraitEvent) {
16
- if (event.type === 'shipping:calculate') { ctx.emit?.('shipping:calculated', { rate: config.rate, estimatedDays: config.estimatedDays, carrier: config.carrier }); }
41
+ if (event.type === 'shipping:calculate') {
42
+ ctx.emit?.('shipping:calculated', {
43
+ rate: config.rate,
44
+ estimatedDays: config.estimatedDays,
45
+ carrier: config.carrier,
46
+ });
47
+ }
17
48
  },
18
49
  };
19
50
  }
@@ -1,4 +1,25 @@
1
- export interface HSPlusNode { id?: string; properties?: Record<string, unknown>; [key: string]: unknown; }
2
- export interface TraitContext { emit?: (event: string, payload?: unknown) => void; getState?: () => Record<string, unknown>; setState?: (updates: Record<string, unknown>) => void; [key: string]: unknown; }
3
- export interface TraitEvent { type: string; source?: string; payload?: Record<string, unknown>; [key: string]: unknown; }
4
- export interface TraitHandler<TConfig = unknown> { name: string; defaultConfig: TConfig; onAttach(node: HSPlusNode, config: TConfig, ctx: TraitContext): void; onDetach(node: HSPlusNode, config: TConfig, ctx: TraitContext): void; onUpdate(node: HSPlusNode, config: TConfig, ctx: TraitContext, delta: number): void; onEvent(node: HSPlusNode, config: TConfig, ctx: TraitContext, event: 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
+ getState?: () => Record<string, unknown>;
9
+ setState?: (updates: Record<string, unknown>) => void;
10
+ [key: string]: unknown;
11
+ }
12
+ export interface TraitEvent {
13
+ type: string;
14
+ source?: string;
15
+ payload?: Record<string, unknown>;
16
+ [key: string]: unknown;
17
+ }
18
+ export interface TraitHandler<TConfig = unknown> {
19
+ name: string;
20
+ defaultConfig: TConfig;
21
+ onAttach(node: HSPlusNode, config: TConfig, ctx: TraitContext): void;
22
+ onDetach(node: HSPlusNode, config: TConfig, ctx: TraitContext): void;
23
+ onUpdate(node: HSPlusNode, config: TConfig, ctx: TraitContext, delta: number): void;
24
+ onEvent(node: HSPlusNode, config: TConfig, ctx: TraitContext, event: TraitEvent): void;
25
+ }
package/tsconfig.json CHANGED
@@ -1 +1,10 @@
1
- { "extends": "../../../tsconfig.json", "compilerOptions": { "outDir": "dist", "rootDir": "src", "declaration": true, "declarationMap": true }, "include": ["src"] }
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "declaration": true,
7
+ "declarationMap": true
8
+ },
9
+ "include": ["src"]
10
+ }
package/vitest.config.ts CHANGED
@@ -1,13 +1,6 @@
1
1
  import { defineConfig } from 'vitest/config';
2
- import { resolve } from 'path';
3
2
 
4
3
  export default defineConfig({
5
- resolve: {
6
- alias: {
7
- '@holoscript/engine': resolve(__dirname, '../../engine/src'),
8
- '@holoscript/core': resolve(__dirname, '../../core/src'),
9
- },
10
- },
11
4
  test: {
12
5
  globals: true,
13
6
  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.