@logg/signals 0.2.0 → 0.4.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/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  Universal event tracking SDK for Logg Signals. Track events from web, React Native, and Node.js applications.
4
4
 
5
- **Version 0.2.0**
5
+ Also ships an embeddable on-device recommender as a separate entry point — see [On-Device Recommender](#on-device-recommender-loggsignalsreco).
6
+
7
+ **Version 0.3.0**
6
8
 
7
9
  ## Features
8
10
 
@@ -12,7 +14,7 @@ Universal event tracking SDK for Logg Signals. Track events from web, React Nati
12
14
  ✅ **Persistent storage** - Uses localStorage, AsyncStorage, or memory as fallback
13
15
  ✅ **Retry logic** - Exponential backoff for failed requests
14
16
  ✅ **Auto metadata** - Automatically collects browser/device information
15
- ✅ **Small bundle** - <5KB gzipped
17
+ ✅ **Small bundle** - <5KB gzipped (tracking entry; recommender is a separate opt-in entry)
16
18
 
17
19
  ## Installation
18
20
 
@@ -455,6 +457,96 @@ function trackUserEvent(event: Omit<EventData, 'userId'>) {
455
457
  }
456
458
  ```
457
459
 
460
+ ## On-Device Recommender (`@logg/signals/reco`)
461
+
462
+ A pure-TypeScript, zero-dependency recommendation engine that runs entirely on
463
+ the client — ship it inside a React Native bundle, a web app, or a Node
464
+ process. The backend hands the app a flat catalog (a few thousand items × a
465
+ few feature columns); the device ranks it live as the user scrolls, dwells,
466
+ taps, and favourites.
467
+
468
+ It lives at its own entry point so tracking-only consumers don't pay for it:
469
+
470
+ ```typescript
471
+ import { Recommender, DualBucketRecommender, SIGNALS } from '@logg/signals/reco';
472
+ import type { BaseItem, Schema } from '@logg/signals/reco';
473
+ ```
474
+
475
+ Two engines ship behind the same surface — pick at construction time:
476
+
477
+ | Engine | Class | Best for |
478
+ |---|---|---|
479
+ | v1 | `Recommender` | Probabilistic exploration, smoother defaults |
480
+ | v2 | `DualBucketRecommender` | Explicit liked/disliked separation, tighter exploit |
481
+
482
+ The library is **domain-agnostic and generic over your item shape**. You bring
483
+ an item type extending `BaseItem` (only `id` is required) and a `Schema<T>`
484
+ that extracts categorical feature values from each item — the engine has no
485
+ built-in vocabulary of brands, prices, or categories.
486
+
487
+ ```typescript
488
+ import { Recommender, SIGNALS, logDecadeBucket, type BaseItem, type Schema } from '@logg/signals/reco';
489
+
490
+ interface Listing extends BaseItem {
491
+ brand: string | null;
492
+ category: string | null;
493
+ price_cents: number | null;
494
+ popularity: number;
495
+ }
496
+
497
+ const schema = {
498
+ brand: { extract: (i: Listing) => i.brand, capacity: 4, weight: 0.4 },
499
+ category: { extract: (i: Listing) => i.category, capacity: 2, weight: 0.2 },
500
+ price_band: {
501
+ extract: (i: Listing) => (i.price_cents != null ? logDecadeBucket(i.price_cents / 100) : null),
502
+ weight: 0.4,
503
+ },
504
+ } satisfies Schema<Listing>;
505
+
506
+ const reco = new Recommender(catalog, {
507
+ schema,
508
+ popularity: (i) => i.popularity, // optional cold-start prior
509
+ });
510
+
511
+ reco.prime(usersCollection); // pre-warm from a known collection
512
+ reco.setOwned(['id-1', 'id-2']); // excluded from results
513
+
514
+ reco.engage(item, SIGNALS.view); // saw and scrolled past
515
+ reco.engage(item, SIGNALS.collect); // added to collection
516
+ reco.engage(item, -3); // strong negative — caller picks any magnitude
517
+
518
+ const { items, scores, diagnostics } = reco.recommend(20);
519
+ ```
520
+
521
+ Methods shared by both engines: `prime()`, `engage()`, `recommend()`,
522
+ `setOwned()`, `clearSeen()`, `reset()`, `seenCount()`. The dual engine adds
523
+ `setSampleSize(k)` to tune explore vs. exploit.
524
+
525
+ ### Weighted scoring (dual engine) — `0.4.0`
526
+
527
+ By default `DualBucketRecommender` ranks candidates by the sum of raw slot
528
+ powers, which ignores each feature's schema `weight` and lets a
529
+ low-cardinality feature (e.g. one dominant `category`) outweigh a
530
+ high-cardinality one (e.g. `brand`) purely by accumulated mass. Pass
531
+ `weightedScoring: true` to instead weight each feature by its schema `weight`
532
+ and normalize a value's power to its share of the bucket (an ε-floored share,
533
+ the same scheme the v1 engine uses):
534
+
535
+ ```typescript
536
+ const reco = new DualBucketRecommender(catalog, { schema, weightedScoring: true });
537
+ ```
538
+
539
+ This makes schema weights authoritative and removes the cardinality bias. It
540
+ is **off by default** to preserve existing v2 ranking. Note the scale change:
541
+ with the flag on, `net` / `diagnostics.bestNet` move from unbounded raw power
542
+ to roughly `[−Σweight, +Σweight]` (≈ `[−1, 1]` when weights sum to 1), so any
543
+ consumer comparing `bestNet` against a raw-power threshold must rescale.
544
+
545
+ Advanced primitives (slot tables, interest state, bucket admission, the
546
+ weighted sampler, the dual-bucket `bucketContribution`, and `seededRng` for
547
+ deterministic tests) are exported from the same entry for callers building
548
+ custom pipelines or CLIs on top.
549
+
458
550
  ## License
459
551
 
460
552
  MIT
@@ -0,0 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ export {
9
+ __require
10
+ };
package/dist/index.js CHANGED
@@ -18,8 +18,8 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
 
20
20
  // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
23
  AsyncStorageAdapter: () => AsyncStorageAdapter,
24
24
  EventQueue: () => EventQueue,
25
25
  LocalStorageAdapter: () => LocalStorageAdapter,
@@ -27,7 +27,7 @@ __export(index_exports, {
27
27
  Signals: () => Signals,
28
28
  getDefaultStorageAdapter: () => getDefaultStorageAdapter
29
29
  });
30
- module.exports = __toCommonJS(index_exports);
30
+ module.exports = __toCommonJS(src_exports);
31
31
 
32
32
  // src/utils/helpers.ts
33
33
  function uuid() {
package/dist/index.mjs CHANGED
@@ -1,9 +1,6 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
1
+ import {
2
+ __require
3
+ } from "./chunk-Y6FXYEAI.mjs";
7
4
 
8
5
  // src/utils/helpers.ts
9
6
  function uuid() {
@@ -0,0 +1,219 @@
1
+ interface BaseItem {
2
+ id: string;
3
+ }
4
+ interface FeatureConfig<T extends BaseItem> {
5
+ extract: (item: T) => string | null;
6
+ capacity?: number;
7
+ challengerCapacity?: number;
8
+ epsilon?: number;
9
+ weight?: number;
10
+ decrementCap?: number;
11
+ }
12
+ type Schema<T extends BaseItem> = Record<string, FeatureConfig<T>>;
13
+ declare const DEFAULT_CAPACITY = 3;
14
+ declare const DEFAULT_DECREMENT_CAP = 4;
15
+ declare const DEFAULT_EPSILON = 0.1;
16
+ declare const SIGNALS: {
17
+ readonly view: 0.5;
18
+ readonly click: 2;
19
+ readonly dwell: 3;
20
+ readonly collect: 8;
21
+ readonly skip: -1;
22
+ };
23
+ type SignalName = keyof typeof SIGNALS;
24
+ interface Slot {
25
+ value: string;
26
+ power: number;
27
+ }
28
+
29
+ interface SlotTableConfig {
30
+ capacity: number;
31
+ challengerCapacity: number;
32
+ epsilon: number;
33
+ }
34
+ declare const DEFAULT_SLOT_CONFIG: SlotTableConfig;
35
+ interface SlotTableState {
36
+ slots: Slot[];
37
+ challengers: Slot[];
38
+ }
39
+ declare function emptyState(): SlotTableState;
40
+ declare function observe(state: SlotTableState, value: string, weight: number, config?: SlotTableConfig): void;
41
+ declare function decay(state: SlotTableState, factor: number): void;
42
+ declare function contribution$1(state: SlotTableState, value: string | null, config?: SlotTableConfig): number;
43
+ declare function isEmpty$1(state: SlotTableState): boolean;
44
+
45
+ interface InterestConfig<T extends BaseItem> {
46
+ schema: Schema<T>;
47
+ popularity?: (item: T) => number;
48
+ popularityWeight: number;
49
+ decayFactor: number;
50
+ decayEvery: number;
51
+ }
52
+ declare const DEFAULT_INTEREST_CONFIG: Omit<InterestConfig<BaseItem>, 'schema'>;
53
+ type InterestState = {
54
+ tables: Record<string, SlotTableState>;
55
+ eventsSinceDecay: number;
56
+ totalEvents: number;
57
+ };
58
+ declare function emptyInterest<T extends BaseItem>(schema: Schema<T>): InterestState;
59
+ declare function applyEvent<T extends BaseItem>(state: InterestState, item: T, magnitude: number, config: InterestConfig<T>): void;
60
+ declare function scoreItem$1<T extends BaseItem>(state: InterestState, item: T, popularityNorm: number, config: InterestConfig<T>): number;
61
+ declare function isCold$1(state: InterestState): boolean;
62
+ declare function describe$1(state: InterestState, top?: number): Record<string, Array<[string, number]>>;
63
+
64
+ type RNG = () => number;
65
+ declare const defaultRng: RNG;
66
+ declare function weightedSampleWithoutReplacement<T>(pool: T[], weight: (item: T) => number, count: number, rng?: RNG): T[];
67
+ declare function seededRng(seed: number): RNG;
68
+
69
+ interface RecommenderConfig<T extends BaseItem> {
70
+ schema: Schema<T>;
71
+ popularity?: (item: T) => number;
72
+ popularityWeight?: number;
73
+ decayFactor?: number;
74
+ decayEvery?: number;
75
+ seenWindow?: number;
76
+ batchSize?: number;
77
+ rng?: RNG;
78
+ }
79
+ interface ResolvedConfig$1<T extends BaseItem> {
80
+ interest: InterestConfig<T>;
81
+ popularity: ((item: T) => number) | null;
82
+ seenWindow: number;
83
+ batchSize: number;
84
+ rng: RNG;
85
+ }
86
+ interface RecommendDiagnostics {
87
+ cold: boolean;
88
+ candidates: number;
89
+ excluded_seen: number;
90
+ excluded_owned: number;
91
+ }
92
+ interface RecommendResult<T extends BaseItem> {
93
+ items: T[];
94
+ scores: number[];
95
+ diagnostics: RecommendDiagnostics;
96
+ }
97
+ declare class Recommender<T extends BaseItem> {
98
+ readonly catalog: T[];
99
+ readonly byId: Map<string, T>;
100
+ readonly interest: InterestState;
101
+ readonly config: ResolvedConfig$1<T>;
102
+ readonly maxPopularity: number;
103
+ private ownedIds;
104
+ private seenOrder;
105
+ private seen;
106
+ constructor(catalog: T[], config: RecommenderConfig<T>);
107
+ setOwned(ids: Iterable<string>): void;
108
+ prime(items: T[], magnitudePerItem?: number): void;
109
+ engage(item: T, score: number): void;
110
+ recommend(count?: number): RecommendResult<T>;
111
+ seenCount(): number;
112
+ clearSeen(): void;
113
+ private markSeen;
114
+ reset(): void;
115
+ }
116
+
117
+ interface BucketState {
118
+ slots: Slot[];
119
+ pointer: number;
120
+ }
121
+ interface BucketConfig {
122
+ capacity: number;
123
+ decrementCap: number;
124
+ }
125
+ declare function emptyBucket(): BucketState;
126
+ declare function admit(state: BucketState, value: string, score: number, config?: BucketConfig): void;
127
+ declare function powerOf(state: BucketState, value: string | null): number;
128
+ declare function contribution(state: BucketState, value: string | null, epsilon: number): number;
129
+ declare function isEmpty(state: BucketState): boolean;
130
+
131
+ interface DualInterestConfig<T extends BaseItem> {
132
+ schema: Schema<T>;
133
+ weightedScoring?: boolean;
134
+ }
135
+ interface DualBucketState {
136
+ liked: Record<string, BucketState>;
137
+ disliked: Record<string, BucketState>;
138
+ totalEvents: number;
139
+ }
140
+ declare function emptyDualState<T extends BaseItem>(schema: Schema<T>): DualBucketState;
141
+ declare function engagePositive<T extends BaseItem>(state: DualBucketState, item: T, magnitude: number, config: DualInterestConfig<T>): void;
142
+ declare function engageNegative<T extends BaseItem>(state: DualBucketState, item: T, magnitude: number, config: DualInterestConfig<T>): void;
143
+ interface ItemScore {
144
+ liked: number;
145
+ disliked: number;
146
+ net: number;
147
+ byFeature: Array<{
148
+ key: string;
149
+ value: string | null;
150
+ liked: number;
151
+ disliked: number;
152
+ }>;
153
+ }
154
+ declare function scoreItem<T extends BaseItem>(state: DualBucketState, item: T, config: DualInterestConfig<T>): ItemScore;
155
+ declare function isCold(state: DualBucketState): boolean;
156
+ declare function describe(state: DualBucketState, top?: number): {
157
+ liked: Record<string, [string, number][]>;
158
+ disliked: Record<string, [string, number][]>;
159
+ };
160
+
161
+ interface DualRecommenderConfig<T extends BaseItem> {
162
+ schema: Schema<T>;
163
+ sampleSize?: number;
164
+ shuffleThreshold?: number;
165
+ weightedScoring?: boolean;
166
+ rng?: RNG;
167
+ }
168
+ interface ResolvedConfig<T extends BaseItem> {
169
+ interest: DualInterestConfig<T>;
170
+ sampleSize: number;
171
+ shuffleThreshold: number;
172
+ rng: RNG;
173
+ }
174
+ interface DualRecommendDiagnostics<T extends BaseItem> {
175
+ cold: boolean;
176
+ candidatesPool: number;
177
+ sampleSize: number;
178
+ bestNet: number;
179
+ bestLiked: number;
180
+ bestDisliked: number;
181
+ freshPagesIssued: number;
182
+ breakdown: ItemScore['byFeature'] | null;
183
+ runnersUp: Array<{
184
+ item: T;
185
+ net: number;
186
+ liked: number;
187
+ disliked: number;
188
+ }>;
189
+ }
190
+ interface DualRecommendResult<T extends BaseItem> {
191
+ items: T[];
192
+ scores: number[];
193
+ diagnostics: DualRecommendDiagnostics<T>;
194
+ }
195
+ declare class DualBucketRecommender<T extends BaseItem> {
196
+ readonly catalog: T[];
197
+ readonly byId: Map<string, T>;
198
+ readonly interest: DualBucketState;
199
+ readonly config: ResolvedConfig<T>;
200
+ private ownedIds;
201
+ private seen;
202
+ private freshPagesIssued;
203
+ constructor(catalog: T[], config: DualRecommenderConfig<T>);
204
+ setOwned(ids: Iterable<string>): void;
205
+ setSampleSize(k: number): void;
206
+ prime(items: T[], magnitudePerItem?: number): void;
207
+ engage(item: T, score: number): void;
208
+ recommend(count?: number): DualRecommendResult<T>;
209
+ private pickOne;
210
+ private candidatePool;
211
+ seenCount(): number;
212
+ clearSeen(): void;
213
+ reset(): void;
214
+ }
215
+
216
+ declare function logDecadeBucket(value: number | null, multiplier?: number, prefix?: string): string | null;
217
+ declare function yearBucket(year: number | null, bandSize?: number): string | null;
218
+
219
+ export { type BaseItem, type BucketConfig, type BucketState, DEFAULT_CAPACITY, DEFAULT_DECREMENT_CAP, DEFAULT_EPSILON, DEFAULT_INTEREST_CONFIG, DEFAULT_SLOT_CONFIG, DualBucketRecommender, type DualBucketState, type DualInterestConfig, type ItemScore as DualItemScore, type DualRecommendDiagnostics, type DualRecommendResult, type DualRecommenderConfig, type FeatureConfig, type InterestConfig, type InterestState, type RNG, type RecommendDiagnostics, type RecommendResult, Recommender, type RecommenderConfig, SIGNALS, type Schema, type SignalName, type Slot, type SlotTableConfig, type SlotTableState, admit, applyEvent, contribution as bucketContribution, contribution$1 as contribution, decay, defaultRng, describe$1 as describe, describe as describeDual, emptyBucket, emptyDualState, emptyInterest, emptyState, engageNegative, engagePositive, isCold$1 as isCold, isCold as isColdDual, isEmpty$1 as isEmpty, isEmpty as isEmptyBucket, logDecadeBucket, observe, powerOf, scoreItem$1 as scoreItem, scoreItem as scoreItemDual, seededRng, weightedSampleWithoutReplacement, yearBucket };
package/dist/reco.d.ts ADDED
@@ -0,0 +1,219 @@
1
+ interface BaseItem {
2
+ id: string;
3
+ }
4
+ interface FeatureConfig<T extends BaseItem> {
5
+ extract: (item: T) => string | null;
6
+ capacity?: number;
7
+ challengerCapacity?: number;
8
+ epsilon?: number;
9
+ weight?: number;
10
+ decrementCap?: number;
11
+ }
12
+ type Schema<T extends BaseItem> = Record<string, FeatureConfig<T>>;
13
+ declare const DEFAULT_CAPACITY = 3;
14
+ declare const DEFAULT_DECREMENT_CAP = 4;
15
+ declare const DEFAULT_EPSILON = 0.1;
16
+ declare const SIGNALS: {
17
+ readonly view: 0.5;
18
+ readonly click: 2;
19
+ readonly dwell: 3;
20
+ readonly collect: 8;
21
+ readonly skip: -1;
22
+ };
23
+ type SignalName = keyof typeof SIGNALS;
24
+ interface Slot {
25
+ value: string;
26
+ power: number;
27
+ }
28
+
29
+ interface SlotTableConfig {
30
+ capacity: number;
31
+ challengerCapacity: number;
32
+ epsilon: number;
33
+ }
34
+ declare const DEFAULT_SLOT_CONFIG: SlotTableConfig;
35
+ interface SlotTableState {
36
+ slots: Slot[];
37
+ challengers: Slot[];
38
+ }
39
+ declare function emptyState(): SlotTableState;
40
+ declare function observe(state: SlotTableState, value: string, weight: number, config?: SlotTableConfig): void;
41
+ declare function decay(state: SlotTableState, factor: number): void;
42
+ declare function contribution$1(state: SlotTableState, value: string | null, config?: SlotTableConfig): number;
43
+ declare function isEmpty$1(state: SlotTableState): boolean;
44
+
45
+ interface InterestConfig<T extends BaseItem> {
46
+ schema: Schema<T>;
47
+ popularity?: (item: T) => number;
48
+ popularityWeight: number;
49
+ decayFactor: number;
50
+ decayEvery: number;
51
+ }
52
+ declare const DEFAULT_INTEREST_CONFIG: Omit<InterestConfig<BaseItem>, 'schema'>;
53
+ type InterestState = {
54
+ tables: Record<string, SlotTableState>;
55
+ eventsSinceDecay: number;
56
+ totalEvents: number;
57
+ };
58
+ declare function emptyInterest<T extends BaseItem>(schema: Schema<T>): InterestState;
59
+ declare function applyEvent<T extends BaseItem>(state: InterestState, item: T, magnitude: number, config: InterestConfig<T>): void;
60
+ declare function scoreItem$1<T extends BaseItem>(state: InterestState, item: T, popularityNorm: number, config: InterestConfig<T>): number;
61
+ declare function isCold$1(state: InterestState): boolean;
62
+ declare function describe$1(state: InterestState, top?: number): Record<string, Array<[string, number]>>;
63
+
64
+ type RNG = () => number;
65
+ declare const defaultRng: RNG;
66
+ declare function weightedSampleWithoutReplacement<T>(pool: T[], weight: (item: T) => number, count: number, rng?: RNG): T[];
67
+ declare function seededRng(seed: number): RNG;
68
+
69
+ interface RecommenderConfig<T extends BaseItem> {
70
+ schema: Schema<T>;
71
+ popularity?: (item: T) => number;
72
+ popularityWeight?: number;
73
+ decayFactor?: number;
74
+ decayEvery?: number;
75
+ seenWindow?: number;
76
+ batchSize?: number;
77
+ rng?: RNG;
78
+ }
79
+ interface ResolvedConfig$1<T extends BaseItem> {
80
+ interest: InterestConfig<T>;
81
+ popularity: ((item: T) => number) | null;
82
+ seenWindow: number;
83
+ batchSize: number;
84
+ rng: RNG;
85
+ }
86
+ interface RecommendDiagnostics {
87
+ cold: boolean;
88
+ candidates: number;
89
+ excluded_seen: number;
90
+ excluded_owned: number;
91
+ }
92
+ interface RecommendResult<T extends BaseItem> {
93
+ items: T[];
94
+ scores: number[];
95
+ diagnostics: RecommendDiagnostics;
96
+ }
97
+ declare class Recommender<T extends BaseItem> {
98
+ readonly catalog: T[];
99
+ readonly byId: Map<string, T>;
100
+ readonly interest: InterestState;
101
+ readonly config: ResolvedConfig$1<T>;
102
+ readonly maxPopularity: number;
103
+ private ownedIds;
104
+ private seenOrder;
105
+ private seen;
106
+ constructor(catalog: T[], config: RecommenderConfig<T>);
107
+ setOwned(ids: Iterable<string>): void;
108
+ prime(items: T[], magnitudePerItem?: number): void;
109
+ engage(item: T, score: number): void;
110
+ recommend(count?: number): RecommendResult<T>;
111
+ seenCount(): number;
112
+ clearSeen(): void;
113
+ private markSeen;
114
+ reset(): void;
115
+ }
116
+
117
+ interface BucketState {
118
+ slots: Slot[];
119
+ pointer: number;
120
+ }
121
+ interface BucketConfig {
122
+ capacity: number;
123
+ decrementCap: number;
124
+ }
125
+ declare function emptyBucket(): BucketState;
126
+ declare function admit(state: BucketState, value: string, score: number, config?: BucketConfig): void;
127
+ declare function powerOf(state: BucketState, value: string | null): number;
128
+ declare function contribution(state: BucketState, value: string | null, epsilon: number): number;
129
+ declare function isEmpty(state: BucketState): boolean;
130
+
131
+ interface DualInterestConfig<T extends BaseItem> {
132
+ schema: Schema<T>;
133
+ weightedScoring?: boolean;
134
+ }
135
+ interface DualBucketState {
136
+ liked: Record<string, BucketState>;
137
+ disliked: Record<string, BucketState>;
138
+ totalEvents: number;
139
+ }
140
+ declare function emptyDualState<T extends BaseItem>(schema: Schema<T>): DualBucketState;
141
+ declare function engagePositive<T extends BaseItem>(state: DualBucketState, item: T, magnitude: number, config: DualInterestConfig<T>): void;
142
+ declare function engageNegative<T extends BaseItem>(state: DualBucketState, item: T, magnitude: number, config: DualInterestConfig<T>): void;
143
+ interface ItemScore {
144
+ liked: number;
145
+ disliked: number;
146
+ net: number;
147
+ byFeature: Array<{
148
+ key: string;
149
+ value: string | null;
150
+ liked: number;
151
+ disliked: number;
152
+ }>;
153
+ }
154
+ declare function scoreItem<T extends BaseItem>(state: DualBucketState, item: T, config: DualInterestConfig<T>): ItemScore;
155
+ declare function isCold(state: DualBucketState): boolean;
156
+ declare function describe(state: DualBucketState, top?: number): {
157
+ liked: Record<string, [string, number][]>;
158
+ disliked: Record<string, [string, number][]>;
159
+ };
160
+
161
+ interface DualRecommenderConfig<T extends BaseItem> {
162
+ schema: Schema<T>;
163
+ sampleSize?: number;
164
+ shuffleThreshold?: number;
165
+ weightedScoring?: boolean;
166
+ rng?: RNG;
167
+ }
168
+ interface ResolvedConfig<T extends BaseItem> {
169
+ interest: DualInterestConfig<T>;
170
+ sampleSize: number;
171
+ shuffleThreshold: number;
172
+ rng: RNG;
173
+ }
174
+ interface DualRecommendDiagnostics<T extends BaseItem> {
175
+ cold: boolean;
176
+ candidatesPool: number;
177
+ sampleSize: number;
178
+ bestNet: number;
179
+ bestLiked: number;
180
+ bestDisliked: number;
181
+ freshPagesIssued: number;
182
+ breakdown: ItemScore['byFeature'] | null;
183
+ runnersUp: Array<{
184
+ item: T;
185
+ net: number;
186
+ liked: number;
187
+ disliked: number;
188
+ }>;
189
+ }
190
+ interface DualRecommendResult<T extends BaseItem> {
191
+ items: T[];
192
+ scores: number[];
193
+ diagnostics: DualRecommendDiagnostics<T>;
194
+ }
195
+ declare class DualBucketRecommender<T extends BaseItem> {
196
+ readonly catalog: T[];
197
+ readonly byId: Map<string, T>;
198
+ readonly interest: DualBucketState;
199
+ readonly config: ResolvedConfig<T>;
200
+ private ownedIds;
201
+ private seen;
202
+ private freshPagesIssued;
203
+ constructor(catalog: T[], config: DualRecommenderConfig<T>);
204
+ setOwned(ids: Iterable<string>): void;
205
+ setSampleSize(k: number): void;
206
+ prime(items: T[], magnitudePerItem?: number): void;
207
+ engage(item: T, score: number): void;
208
+ recommend(count?: number): DualRecommendResult<T>;
209
+ private pickOne;
210
+ private candidatePool;
211
+ seenCount(): number;
212
+ clearSeen(): void;
213
+ reset(): void;
214
+ }
215
+
216
+ declare function logDecadeBucket(value: number | null, multiplier?: number, prefix?: string): string | null;
217
+ declare function yearBucket(year: number | null, bandSize?: number): string | null;
218
+
219
+ export { type BaseItem, type BucketConfig, type BucketState, DEFAULT_CAPACITY, DEFAULT_DECREMENT_CAP, DEFAULT_EPSILON, DEFAULT_INTEREST_CONFIG, DEFAULT_SLOT_CONFIG, DualBucketRecommender, type DualBucketState, type DualInterestConfig, type ItemScore as DualItemScore, type DualRecommendDiagnostics, type DualRecommendResult, type DualRecommenderConfig, type FeatureConfig, type InterestConfig, type InterestState, type RNG, type RecommendDiagnostics, type RecommendResult, Recommender, type RecommenderConfig, SIGNALS, type Schema, type SignalName, type Slot, type SlotTableConfig, type SlotTableState, admit, applyEvent, contribution as bucketContribution, contribution$1 as contribution, decay, defaultRng, describe$1 as describe, describe as describeDual, emptyBucket, emptyDualState, emptyInterest, emptyState, engageNegative, engagePositive, isCold$1 as isCold, isCold as isColdDual, isEmpty$1 as isEmpty, isEmpty as isEmptyBucket, logDecadeBucket, observe, powerOf, scoreItem$1 as scoreItem, scoreItem as scoreItemDual, seededRng, weightedSampleWithoutReplacement, yearBucket };