@logg/signals 0.1.5 → 0.3.0

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/dist/reco.mjs ADDED
@@ -0,0 +1,602 @@
1
+ import "./chunk-Y6FXYEAI.mjs";
2
+
3
+ // src/reco/types.ts
4
+ var DEFAULT_CAPACITY = 3;
5
+ var DEFAULT_DECREMENT_CAP = 4;
6
+ var DEFAULT_EPSILON = 0.1;
7
+ var SIGNALS = {
8
+ view: 0.5,
9
+ click: 2,
10
+ dwell: 3,
11
+ collect: 8,
12
+ skip: -1
13
+ };
14
+
15
+ // src/reco/slots.ts
16
+ var DEFAULT_SLOT_CONFIG = {
17
+ capacity: DEFAULT_CAPACITY,
18
+ challengerCapacity: DEFAULT_CAPACITY * 2,
19
+ epsilon: DEFAULT_EPSILON
20
+ };
21
+ function emptyState() {
22
+ return { slots: [], challengers: [] };
23
+ }
24
+ function reorder(arr) {
25
+ arr.sort((a, b) => b.power - a.power || a.value.localeCompare(b.value));
26
+ }
27
+ function observe(state, value, weight, config = DEFAULT_SLOT_CONFIG) {
28
+ if (!value) return;
29
+ const slot = state.slots.find((s) => s.value === value);
30
+ if (slot) {
31
+ slot.power = Math.max(0, slot.power + weight);
32
+ if (slot.power === 0) {
33
+ state.slots = state.slots.filter((s) => s !== slot);
34
+ }
35
+ reorder(state.slots);
36
+ return;
37
+ }
38
+ if (weight <= 0) return;
39
+ const challenger = state.challengers.find((c) => c.value === value);
40
+ if (challenger) {
41
+ challenger.power += weight;
42
+ } else {
43
+ state.challengers.push({ value, power: weight });
44
+ }
45
+ reorder(state.challengers);
46
+ if (state.challengers.length > config.challengerCapacity) {
47
+ state.challengers.length = config.challengerCapacity;
48
+ }
49
+ if (state.slots.length < config.capacity) {
50
+ const top2 = state.challengers.shift();
51
+ if (top2) {
52
+ state.slots.push(top2);
53
+ reorder(state.slots);
54
+ }
55
+ return;
56
+ }
57
+ const top = state.challengers[0];
58
+ const weakest = state.slots[state.slots.length - 1];
59
+ if (top && weakest && top.power > weakest.power) {
60
+ state.slots[state.slots.length - 1] = top;
61
+ state.challengers[0] = weakest;
62
+ reorder(state.slots);
63
+ reorder(state.challengers);
64
+ }
65
+ }
66
+ function decay(state, factor) {
67
+ if (factor <= 0 || factor >= 1) return;
68
+ for (const s of state.slots) s.power *= factor;
69
+ for (const c of state.challengers) c.power *= factor;
70
+ state.slots = state.slots.filter((s) => s.power > 0.01);
71
+ state.challengers = state.challengers.filter((c) => c.power > 0.01);
72
+ reorder(state.slots);
73
+ reorder(state.challengers);
74
+ }
75
+ function contribution(state, value, config = DEFAULT_SLOT_CONFIG) {
76
+ if (!value) return config.epsilon;
77
+ const slot = state.slots.find((s) => s.value === value);
78
+ if (!slot) return config.epsilon;
79
+ const total = state.slots.reduce((sum, s) => sum + s.power, 0);
80
+ if (total <= 0) return config.epsilon;
81
+ return config.epsilon + (1 - config.epsilon) * (slot.power / total);
82
+ }
83
+ function isEmpty(state) {
84
+ return state.slots.length === 0 && state.challengers.length === 0;
85
+ }
86
+
87
+ // src/reco/interest.ts
88
+ var DEFAULT_INTEREST_CONFIG = {
89
+ popularityWeight: 0.1,
90
+ decayFactor: 0.97,
91
+ decayEvery: 25
92
+ };
93
+ function emptyInterest(schema) {
94
+ const tables = {};
95
+ for (const k of Object.keys(schema)) tables[k] = emptyState();
96
+ return { tables, eventsSinceDecay: 0, totalEvents: 0 };
97
+ }
98
+ function tableConfig(fc) {
99
+ const capacity = fc.capacity ?? DEFAULT_CAPACITY;
100
+ return {
101
+ capacity,
102
+ challengerCapacity: fc.challengerCapacity ?? capacity * 2,
103
+ epsilon: fc.epsilon ?? DEFAULT_EPSILON
104
+ };
105
+ }
106
+ function featureWeight(fc, config, featureCount) {
107
+ if (typeof fc.weight === "number") return fc.weight;
108
+ const budget = Math.max(0, 1 - config.popularityWeight);
109
+ return featureCount > 0 ? budget / featureCount : 0;
110
+ }
111
+ function applyEvent(state, item, magnitude, config) {
112
+ if (!Number.isFinite(magnitude) || magnitude === 0) return;
113
+ for (const [key, fc] of Object.entries(config.schema)) {
114
+ const value = fc.extract(item);
115
+ if (!value) continue;
116
+ observe(state.tables[key], value, magnitude, tableConfig(fc));
117
+ }
118
+ state.totalEvents += 1;
119
+ state.eventsSinceDecay += 1;
120
+ if (state.eventsSinceDecay >= config.decayEvery) {
121
+ for (const key of Object.keys(config.schema)) decay(state.tables[key], config.decayFactor);
122
+ state.eventsSinceDecay = 0;
123
+ }
124
+ }
125
+ function scoreItem(state, item, popularityNorm, config) {
126
+ const entries = Object.entries(config.schema);
127
+ let score = config.popularityWeight * popularityNorm;
128
+ for (const [key, fc] of entries) {
129
+ const value = fc.extract(item);
130
+ const c = contribution(state.tables[key], value, tableConfig(fc));
131
+ score += featureWeight(fc, config, entries.length) * c;
132
+ }
133
+ return score;
134
+ }
135
+ function isCold(state) {
136
+ for (const t of Object.values(state.tables)) {
137
+ if (!isEmpty(t)) return false;
138
+ }
139
+ return true;
140
+ }
141
+ function describe(state, top = 3) {
142
+ const out = {};
143
+ for (const [key, t] of Object.entries(state.tables)) {
144
+ out[key] = t.slots.slice(0, top).map((s) => [s.value, Math.round(s.power * 100) / 100]);
145
+ }
146
+ return out;
147
+ }
148
+
149
+ // src/reco/sample.ts
150
+ var defaultRng = Math.random;
151
+ function weightedSampleWithoutReplacement(pool, weight, count, rng = defaultRng) {
152
+ if (count <= 0 || pool.length === 0) return [];
153
+ const heap = [];
154
+ const heapPush = (e) => {
155
+ heap.push(e);
156
+ let i = heap.length - 1;
157
+ while (i > 0) {
158
+ const parent = i - 1 >> 1;
159
+ if (heap[parent].key <= heap[i].key) break;
160
+ [heap[parent], heap[i]] = [heap[i], heap[parent]];
161
+ i = parent;
162
+ }
163
+ };
164
+ const heapPopMin = () => {
165
+ if (heap.length === 0) return void 0;
166
+ const min = heap[0];
167
+ const last = heap.pop();
168
+ if (heap.length > 0) {
169
+ heap[0] = last;
170
+ let i = 0;
171
+ while (true) {
172
+ const l = 2 * i + 1;
173
+ const r = 2 * i + 2;
174
+ let smallest = i;
175
+ if (l < heap.length && heap[l].key < heap[smallest].key) smallest = l;
176
+ if (r < heap.length && heap[r].key < heap[smallest].key) smallest = r;
177
+ if (smallest === i) break;
178
+ [heap[smallest], heap[i]] = [heap[i], heap[smallest]];
179
+ i = smallest;
180
+ }
181
+ }
182
+ return min;
183
+ };
184
+ for (const item of pool) {
185
+ const w = Math.max(weight(item), 1e-9);
186
+ const u = Math.max(rng(), 1e-12);
187
+ const key = Math.exp(Math.log(u) / w);
188
+ if (heap.length < count) {
189
+ heapPush({ item, key });
190
+ } else if (heap[0].key < key) {
191
+ heapPopMin();
192
+ heapPush({ item, key });
193
+ }
194
+ }
195
+ return heap.sort((a, b) => b.key - a.key).map((e) => e.item);
196
+ }
197
+ function seededRng(seed) {
198
+ let s = seed >>> 0;
199
+ return () => {
200
+ s = s + 1831565813 >>> 0;
201
+ let t = s;
202
+ t = Math.imul(t ^ t >>> 15, t | 1);
203
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
204
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
205
+ };
206
+ }
207
+
208
+ // src/reco/recommender.ts
209
+ function resolve(input) {
210
+ return {
211
+ interest: {
212
+ schema: input.schema,
213
+ popularity: input.popularity,
214
+ popularityWeight: input.popularityWeight ?? DEFAULT_INTEREST_CONFIG.popularityWeight,
215
+ decayFactor: input.decayFactor ?? DEFAULT_INTEREST_CONFIG.decayFactor,
216
+ decayEvery: input.decayEvery ?? DEFAULT_INTEREST_CONFIG.decayEvery
217
+ },
218
+ popularity: input.popularity ?? null,
219
+ seenWindow: input.seenWindow ?? 300,
220
+ batchSize: input.batchSize ?? 20,
221
+ rng: input.rng ?? defaultRng
222
+ };
223
+ }
224
+ var Recommender = class {
225
+ constructor(catalog, config) {
226
+ this.ownedIds = /* @__PURE__ */ new Set();
227
+ this.seenOrder = [];
228
+ this.seen = /* @__PURE__ */ new Set();
229
+ this.catalog = catalog;
230
+ this.byId = new Map(catalog.map((i) => [i.id, i]));
231
+ this.config = resolve(config);
232
+ this.interest = emptyInterest(this.config.interest.schema);
233
+ if (this.config.popularity) {
234
+ const pop = this.config.popularity;
235
+ this.maxPopularity = Math.max(1, ...catalog.map((i) => pop(i)));
236
+ } else {
237
+ this.maxPopularity = 1;
238
+ }
239
+ }
240
+ setOwned(ids) {
241
+ this.ownedIds = new Set(ids);
242
+ }
243
+ /** Apply a positive prior from a list of items the user already
244
+ * "loves" (their collection, recent favourites, etc.). Defaults to
245
+ * `SIGNALS.collect` per item. */
246
+ prime(items, magnitudePerItem = SIGNALS.collect) {
247
+ for (const item of items) this.engage(item, magnitudePerItem);
248
+ }
249
+ engage(item, score) {
250
+ applyEvent(this.interest, item, score, this.config.interest);
251
+ }
252
+ recommend(count = this.config.batchSize) {
253
+ let excluded_seen = 0;
254
+ let excluded_owned = 0;
255
+ const candidates = [];
256
+ for (const item of this.catalog) {
257
+ if (this.ownedIds.has(item.id)) {
258
+ excluded_owned++;
259
+ continue;
260
+ }
261
+ if (this.seen.has(item.id)) {
262
+ excluded_seen++;
263
+ continue;
264
+ }
265
+ candidates.push(item);
266
+ }
267
+ const cold = isCold(this.interest);
268
+ const pop = this.config.popularity;
269
+ const popMax = this.maxPopularity;
270
+ const items = weightedSampleWithoutReplacement(
271
+ candidates,
272
+ (item) => {
273
+ const popNorm = pop ? pop(item) / popMax : 0;
274
+ if (cold) {
275
+ return Math.max(0.05, popNorm);
276
+ }
277
+ return scoreItem(this.interest, item, popNorm, this.config.interest);
278
+ },
279
+ count,
280
+ this.config.rng
281
+ );
282
+ const scores = items.map((item) => {
283
+ const popNorm = pop ? pop(item) / popMax : 0;
284
+ return cold ? Math.max(0.05, popNorm) : scoreItem(this.interest, item, popNorm, this.config.interest);
285
+ });
286
+ for (const item of items) this.markSeen(item.id);
287
+ return {
288
+ items,
289
+ scores,
290
+ diagnostics: { cold, candidates: candidates.length, excluded_seen, excluded_owned }
291
+ };
292
+ }
293
+ seenCount() {
294
+ return this.seenOrder.length;
295
+ }
296
+ /** Clear only the seen FIFO — interest profile + owned set survive. */
297
+ clearSeen() {
298
+ this.seen.clear();
299
+ this.seenOrder.length = 0;
300
+ }
301
+ markSeen(id) {
302
+ if (this.seen.has(id)) return;
303
+ this.seen.add(id);
304
+ this.seenOrder.push(id);
305
+ while (this.seenOrder.length > this.config.seenWindow) {
306
+ const evicted = this.seenOrder.shift();
307
+ if (evicted != null) this.seen.delete(evicted);
308
+ }
309
+ }
310
+ /** Full wipe (interest + seen). */
311
+ reset() {
312
+ const fresh = emptyInterest(this.config.interest.schema);
313
+ this.interest.tables = fresh.tables;
314
+ this.interest.eventsSinceDecay = 0;
315
+ this.interest.totalEvents = 0;
316
+ this.seen.clear();
317
+ this.seenOrder.length = 0;
318
+ }
319
+ };
320
+
321
+ // src/reco/dual-bucket/buckets.ts
322
+ var FALLBACK_CONFIG = {
323
+ capacity: DEFAULT_CAPACITY,
324
+ decrementCap: DEFAULT_DECREMENT_CAP
325
+ };
326
+ function emptyBucket() {
327
+ return { slots: [], pointer: 0 };
328
+ }
329
+ function admit(state, value, score, config = FALLBACK_CONFIG) {
330
+ if (!value || !Number.isFinite(score) || score <= 0) return;
331
+ const existing = state.slots.find((s) => s.value === value);
332
+ if (existing) {
333
+ existing.power += score;
334
+ return;
335
+ }
336
+ if (state.slots.length < config.capacity) {
337
+ state.slots.push({ value, power: score });
338
+ return;
339
+ }
340
+ const ptr = state.pointer % state.slots.length;
341
+ const decrement = Math.min(score, config.decrementCap);
342
+ state.slots[ptr].power -= decrement;
343
+ if (state.slots[ptr].power <= 0) {
344
+ state.slots[ptr] = { value, power: score };
345
+ }
346
+ state.pointer = (ptr + 1) % state.slots.length;
347
+ }
348
+ function powerOf(state, value) {
349
+ if (!value) return 0;
350
+ const slot = state.slots.find((s) => s.value === value);
351
+ return slot ? slot.power : 0;
352
+ }
353
+ function isEmpty2(state) {
354
+ return state.slots.length === 0;
355
+ }
356
+
357
+ // src/reco/dual-bucket/interest.ts
358
+ function emptyTables(schema) {
359
+ const out = {};
360
+ for (const k of Object.keys(schema)) out[k] = emptyBucket();
361
+ return out;
362
+ }
363
+ function emptyDualState(schema) {
364
+ return {
365
+ liked: emptyTables(schema),
366
+ disliked: emptyTables(schema),
367
+ totalEvents: 0
368
+ };
369
+ }
370
+ function bucketCfg(fc) {
371
+ return {
372
+ capacity: fc.capacity ?? DEFAULT_CAPACITY,
373
+ decrementCap: fc.decrementCap ?? DEFAULT_DECREMENT_CAP
374
+ };
375
+ }
376
+ function admitAcrossFeatures(state, side, item, magnitude, schema) {
377
+ const tables = state[side];
378
+ for (const [key, fc] of Object.entries(schema)) {
379
+ const value = fc.extract(item);
380
+ if (value) admit(tables[key], value, magnitude, bucketCfg(fc));
381
+ }
382
+ state.totalEvents += 1;
383
+ }
384
+ function engagePositive(state, item, magnitude, config) {
385
+ if (!Number.isFinite(magnitude) || magnitude <= 0) return;
386
+ admitAcrossFeatures(state, "liked", item, magnitude, config.schema);
387
+ }
388
+ function engageNegative(state, item, magnitude, config) {
389
+ if (!Number.isFinite(magnitude) || magnitude <= 0) return;
390
+ admitAcrossFeatures(state, "disliked", item, magnitude, config.schema);
391
+ }
392
+ function scoreItem2(state, item, config) {
393
+ let liked = 0;
394
+ let disliked = 0;
395
+ const byFeature = [];
396
+ for (const [key, fc] of Object.entries(config.schema)) {
397
+ const value = fc.extract(item);
398
+ const l = powerOf(state.liked[key], value);
399
+ const d = powerOf(state.disliked[key], value);
400
+ liked += l;
401
+ disliked += d;
402
+ byFeature.push({ key, value, liked: l, disliked: d });
403
+ }
404
+ return { liked, disliked, net: liked - disliked, byFeature };
405
+ }
406
+ function isCold2(state) {
407
+ if (state.totalEvents > 0) return false;
408
+ for (const t of Object.values(state.liked)) if (!isEmpty2(t)) return false;
409
+ for (const t of Object.values(state.disliked)) if (!isEmpty2(t)) return false;
410
+ return true;
411
+ }
412
+ function describe2(state, top = 4) {
413
+ const snap = (side) => {
414
+ const out = {};
415
+ for (const [k, t] of Object.entries(side)) {
416
+ out[k] = t.slots.slice().sort((a, b) => b.power - a.power).slice(0, top).map((s) => [s.value, Math.round(s.power * 100) / 100]);
417
+ }
418
+ return out;
419
+ };
420
+ return { liked: snap(state.liked), disliked: snap(state.disliked) };
421
+ }
422
+
423
+ // src/reco/dual-bucket/recommender.ts
424
+ function resolve2(input) {
425
+ return {
426
+ interest: { schema: input.schema },
427
+ // 100 is a heavy exploit setting; drop to 20–30 if exploration feels
428
+ // weak. Live-tunable via setSampleSize().
429
+ sampleSize: input.sampleSize ?? 100,
430
+ shuffleThreshold: input.shuffleThreshold ?? 100,
431
+ rng: input.rng ?? defaultRng
432
+ };
433
+ }
434
+ var DualBucketRecommender = class {
435
+ constructor(catalog, config) {
436
+ this.ownedIds = /* @__PURE__ */ new Set();
437
+ this.seen = /* @__PURE__ */ new Set();
438
+ this.freshPagesIssued = 0;
439
+ this.catalog = catalog;
440
+ this.byId = new Map(catalog.map((i) => [i.id, i]));
441
+ this.config = resolve2(config);
442
+ this.interest = emptyDualState(this.config.interest.schema);
443
+ }
444
+ setOwned(ids) {
445
+ this.ownedIds = new Set(ids);
446
+ }
447
+ /** Live-tune the best-of-K size. */
448
+ setSampleSize(k) {
449
+ if (!Number.isFinite(k) || k < 1) return;
450
+ this.config.sampleSize = Math.floor(k);
451
+ }
452
+ prime(items, magnitudePerItem = SIGNALS.collect) {
453
+ for (const item of items) this.engage(item, magnitudePerItem);
454
+ }
455
+ engage(item, score) {
456
+ if (!Number.isFinite(score) || score === 0) return;
457
+ if (score > 0) engagePositive(this.interest, item, score, this.config.interest);
458
+ else engageNegative(this.interest, item, -score, this.config.interest);
459
+ }
460
+ recommend(count = 1) {
461
+ const items = [];
462
+ const scores = [];
463
+ let lastDiag = null;
464
+ for (let i = 0; i < count; i++) {
465
+ const result = this.pickOne();
466
+ if (!result) break;
467
+ items.push(result.item);
468
+ scores.push(result.diagnostics.bestNet);
469
+ lastDiag = result.diagnostics;
470
+ this.seen.add(result.item.id);
471
+ }
472
+ return {
473
+ items,
474
+ scores,
475
+ diagnostics: lastDiag ?? {
476
+ cold: isCold2(this.interest),
477
+ candidatesPool: 0,
478
+ sampleSize: 0,
479
+ bestNet: 0,
480
+ bestLiked: 0,
481
+ bestDisliked: 0,
482
+ freshPagesIssued: this.freshPagesIssued,
483
+ breakdown: null,
484
+ runnersUp: []
485
+ }
486
+ };
487
+ }
488
+ pickOne() {
489
+ let pool = this.candidatePool();
490
+ if (pool.length < this.config.shuffleThreshold) {
491
+ this.seen.clear();
492
+ this.freshPagesIssued += 1;
493
+ pool = this.candidatePool();
494
+ }
495
+ if (pool.length === 0) return null;
496
+ const k = Math.min(this.config.sampleSize, pool.length);
497
+ const sample = randomSample(pool, k, this.config.rng);
498
+ const scored = sample.map((item) => ({ item, s: scoreItem2(this.interest, item, this.config.interest) }));
499
+ scored.sort((a, b) => b.s.net - a.s.net);
500
+ let winner = scored[0];
501
+ if (winner.s.net < 0) {
502
+ const zero = scored.find((x) => x.s.net === 0);
503
+ if (zero) winner = zero;
504
+ }
505
+ const runnersUp = scored.filter((x) => x.item.id !== winner.item.id).map((x) => ({
506
+ item: x.item,
507
+ net: x.s.net,
508
+ liked: x.s.liked,
509
+ disliked: x.s.disliked
510
+ }));
511
+ return {
512
+ item: winner.item,
513
+ diagnostics: {
514
+ cold: isCold2(this.interest),
515
+ candidatesPool: pool.length,
516
+ sampleSize: k,
517
+ bestNet: winner.s.net,
518
+ bestLiked: winner.s.liked,
519
+ bestDisliked: winner.s.disliked,
520
+ freshPagesIssued: this.freshPagesIssued,
521
+ breakdown: winner.s.byFeature,
522
+ runnersUp
523
+ }
524
+ };
525
+ }
526
+ candidatePool() {
527
+ return this.catalog.filter((i) => !this.ownedIds.has(i.id) && !this.seen.has(i.id));
528
+ }
529
+ seenCount() {
530
+ return this.seen.size;
531
+ }
532
+ clearSeen() {
533
+ this.seen.clear();
534
+ this.freshPagesIssued = 0;
535
+ }
536
+ reset() {
537
+ const fresh = emptyDualState(this.config.interest.schema);
538
+ this.interest.liked = fresh.liked;
539
+ this.interest.disliked = fresh.disliked;
540
+ this.interest.totalEvents = 0;
541
+ this.seen.clear();
542
+ this.freshPagesIssued = 0;
543
+ }
544
+ };
545
+ function randomSample(pool, k, rng) {
546
+ if (k >= pool.length) return pool.slice();
547
+ const arr = pool.slice();
548
+ const n = arr.length;
549
+ const out = [];
550
+ for (let i = 0; i < k; i++) {
551
+ const j = i + Math.floor(rng() * (n - i));
552
+ [arr[i], arr[j]] = [arr[j], arr[i]];
553
+ out.push(arr[i]);
554
+ }
555
+ return out;
556
+ }
557
+
558
+ // src/reco/buckets.ts
559
+ function logDecadeBucket(value, multiplier = 2, prefix = "lb") {
560
+ if (value == null || value <= 0) return null;
561
+ return `${prefix}${Math.floor(Math.log10(value) * multiplier)}`;
562
+ }
563
+ function yearBucket(year, bandSize = 5) {
564
+ if (year == null || year <= 0 || bandSize <= 0) return null;
565
+ const start = Math.floor(year / bandSize) * bandSize;
566
+ return `${start}-${start + bandSize - 1}`;
567
+ }
568
+ export {
569
+ DEFAULT_CAPACITY,
570
+ DEFAULT_DECREMENT_CAP,
571
+ DEFAULT_EPSILON,
572
+ DEFAULT_INTEREST_CONFIG,
573
+ DEFAULT_SLOT_CONFIG,
574
+ DualBucketRecommender,
575
+ Recommender,
576
+ SIGNALS,
577
+ admit,
578
+ applyEvent,
579
+ contribution,
580
+ decay,
581
+ defaultRng,
582
+ describe,
583
+ describe2 as describeDual,
584
+ emptyBucket,
585
+ emptyDualState,
586
+ emptyInterest,
587
+ emptyState,
588
+ engageNegative,
589
+ engagePositive,
590
+ isCold,
591
+ isCold2 as isColdDual,
592
+ isEmpty,
593
+ isEmpty2 as isEmptyBucket,
594
+ logDecadeBucket,
595
+ observe,
596
+ powerOf,
597
+ scoreItem,
598
+ scoreItem2 as scoreItemDual,
599
+ seededRng,
600
+ weightedSampleWithoutReplacement,
601
+ yearBucket
602
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@logg/signals",
3
- "version": "0.1.5",
4
- "description": "Universal event tracking SDK for Logg Signals",
3
+ "version": "0.3.0",
4
+ "description": "Universal event tracking SDK for Logg Signals, with an embeddable on-device recommender",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",
@@ -10,14 +10,19 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.mjs",
12
12
  "require": "./dist/index.js"
13
+ },
14
+ "./reco": {
15
+ "types": "./dist/reco.d.ts",
16
+ "import": "./dist/reco.mjs",
17
+ "require": "./dist/reco.js"
13
18
  }
14
19
  },
15
20
  "files": [
16
21
  "dist"
17
22
  ],
18
23
  "scripts": {
19
- "build": "tsup src/index.ts --format cjs,esm --dts --clean",
20
- "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
24
+ "build": "tsup --entry.index src/index.ts --entry.reco src/reco/index.ts --format cjs,esm --dts --clean",
25
+ "dev": "tsup --entry.index src/index.ts --entry.reco src/reco/index.ts --format cjs,esm --dts --watch",
21
26
  "test": "vitest",
22
27
  "test:ui": "vitest --ui",
23
28
  "type-check": "tsc --noEmit",
@@ -30,6 +35,8 @@
30
35
  "tracking",
31
36
  "logg",
32
37
  "signals",
38
+ "recommendations",
39
+ "recommender",
33
40
  "react-native",
34
41
  "web",
35
42
  "nodejs"