@logg/signals 0.3.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
@@ -522,9 +522,30 @@ Methods shared by both engines: `prime()`, `engage()`, `recommend()`,
522
522
  `setOwned()`, `clearSeen()`, `reset()`, `seenCount()`. The dual engine adds
523
523
  `setSampleSize(k)` to tune explore vs. exploit.
524
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
+
525
545
  Advanced primitives (slot tables, interest state, bucket admission, the
526
- weighted sampler, and `seededRng` for deterministic tests) are exported from
527
- the same entry for callers building custom pipelines or CLIs on top.
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.
528
549
 
529
550
  ## License
530
551
 
package/dist/reco.d.mts CHANGED
@@ -39,7 +39,7 @@ interface SlotTableState {
39
39
  declare function emptyState(): SlotTableState;
40
40
  declare function observe(state: SlotTableState, value: string, weight: number, config?: SlotTableConfig): void;
41
41
  declare function decay(state: SlotTableState, factor: number): void;
42
- declare function contribution(state: SlotTableState, value: string | null, config?: SlotTableConfig): number;
42
+ declare function contribution$1(state: SlotTableState, value: string | null, config?: SlotTableConfig): number;
43
43
  declare function isEmpty$1(state: SlotTableState): boolean;
44
44
 
45
45
  interface InterestConfig<T extends BaseItem> {
@@ -125,10 +125,12 @@ interface BucketConfig {
125
125
  declare function emptyBucket(): BucketState;
126
126
  declare function admit(state: BucketState, value: string, score: number, config?: BucketConfig): void;
127
127
  declare function powerOf(state: BucketState, value: string | null): number;
128
+ declare function contribution(state: BucketState, value: string | null, epsilon: number): number;
128
129
  declare function isEmpty(state: BucketState): boolean;
129
130
 
130
131
  interface DualInterestConfig<T extends BaseItem> {
131
132
  schema: Schema<T>;
133
+ weightedScoring?: boolean;
132
134
  }
133
135
  interface DualBucketState {
134
136
  liked: Record<string, BucketState>;
@@ -160,6 +162,7 @@ interface DualRecommenderConfig<T extends BaseItem> {
160
162
  schema: Schema<T>;
161
163
  sampleSize?: number;
162
164
  shuffleThreshold?: number;
165
+ weightedScoring?: boolean;
163
166
  rng?: RNG;
164
167
  }
165
168
  interface ResolvedConfig<T extends BaseItem> {
@@ -213,4 +216,4 @@ declare class DualBucketRecommender<T extends BaseItem> {
213
216
  declare function logDecadeBucket(value: number | null, multiplier?: number, prefix?: string): string | null;
214
217
  declare function yearBucket(year: number | null, bandSize?: number): string | null;
215
218
 
216
- 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, 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 };
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 CHANGED
@@ -39,7 +39,7 @@ interface SlotTableState {
39
39
  declare function emptyState(): SlotTableState;
40
40
  declare function observe(state: SlotTableState, value: string, weight: number, config?: SlotTableConfig): void;
41
41
  declare function decay(state: SlotTableState, factor: number): void;
42
- declare function contribution(state: SlotTableState, value: string | null, config?: SlotTableConfig): number;
42
+ declare function contribution$1(state: SlotTableState, value: string | null, config?: SlotTableConfig): number;
43
43
  declare function isEmpty$1(state: SlotTableState): boolean;
44
44
 
45
45
  interface InterestConfig<T extends BaseItem> {
@@ -125,10 +125,12 @@ interface BucketConfig {
125
125
  declare function emptyBucket(): BucketState;
126
126
  declare function admit(state: BucketState, value: string, score: number, config?: BucketConfig): void;
127
127
  declare function powerOf(state: BucketState, value: string | null): number;
128
+ declare function contribution(state: BucketState, value: string | null, epsilon: number): number;
128
129
  declare function isEmpty(state: BucketState): boolean;
129
130
 
130
131
  interface DualInterestConfig<T extends BaseItem> {
131
132
  schema: Schema<T>;
133
+ weightedScoring?: boolean;
132
134
  }
133
135
  interface DualBucketState {
134
136
  liked: Record<string, BucketState>;
@@ -160,6 +162,7 @@ interface DualRecommenderConfig<T extends BaseItem> {
160
162
  schema: Schema<T>;
161
163
  sampleSize?: number;
162
164
  shuffleThreshold?: number;
165
+ weightedScoring?: boolean;
163
166
  rng?: RNG;
164
167
  }
165
168
  interface ResolvedConfig<T extends BaseItem> {
@@ -213,4 +216,4 @@ declare class DualBucketRecommender<T extends BaseItem> {
213
216
  declare function logDecadeBucket(value: number | null, multiplier?: number, prefix?: string): string | null;
214
217
  declare function yearBucket(year: number | null, bandSize?: number): string | null;
215
218
 
216
- 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, 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 };
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.js CHANGED
@@ -30,6 +30,7 @@ __export(reco_exports, {
30
30
  SIGNALS: () => SIGNALS,
31
31
  admit: () => admit,
32
32
  applyEvent: () => applyEvent,
33
+ bucketContribution: () => contribution2,
33
34
  contribution: () => contribution,
34
35
  decay: () => decay,
35
36
  defaultRng: () => defaultRng,
@@ -406,6 +407,14 @@ function powerOf(state, value) {
406
407
  const slot = state.slots.find((s) => s.value === value);
407
408
  return slot ? slot.power : 0;
408
409
  }
410
+ function contribution2(state, value, epsilon) {
411
+ if (!value) return epsilon;
412
+ const slot = state.slots.find((s) => s.value === value);
413
+ if (!slot) return epsilon;
414
+ const total = state.slots.reduce((sum, s) => sum + s.power, 0);
415
+ if (total <= 0) return epsilon;
416
+ return epsilon + (1 - epsilon) * (slot.power / total);
417
+ }
409
418
  function isEmpty2(state) {
410
419
  return state.slots.length === 0;
411
420
  }
@@ -445,14 +454,31 @@ function engageNegative(state, item, magnitude, config) {
445
454
  if (!Number.isFinite(magnitude) || magnitude <= 0) return;
446
455
  admitAcrossFeatures(state, "disliked", item, magnitude, config.schema);
447
456
  }
457
+ function featureWeight2(fc, featureCount) {
458
+ if (typeof fc.weight === "number") return fc.weight;
459
+ return featureCount > 0 ? 1 / featureCount : 0;
460
+ }
461
+ function epsilonOf(fc) {
462
+ return fc.epsilon ?? DEFAULT_EPSILON;
463
+ }
448
464
  function scoreItem2(state, item, config) {
449
465
  let liked = 0;
450
466
  let disliked = 0;
451
467
  const byFeature = [];
452
- for (const [key, fc] of Object.entries(config.schema)) {
468
+ const entries = Object.entries(config.schema);
469
+ for (const [key, fc] of entries) {
453
470
  const value = fc.extract(item);
454
- const l = powerOf(state.liked[key], value);
455
- const d = powerOf(state.disliked[key], value);
471
+ let l;
472
+ let d;
473
+ if (config.weightedScoring) {
474
+ const w = featureWeight2(fc, entries.length);
475
+ const eps = epsilonOf(fc);
476
+ l = w * contribution2(state.liked[key], value, eps);
477
+ d = w * contribution2(state.disliked[key], value, eps);
478
+ } else {
479
+ l = powerOf(state.liked[key], value);
480
+ d = powerOf(state.disliked[key], value);
481
+ }
456
482
  liked += l;
457
483
  disliked += d;
458
484
  byFeature.push({ key, value, liked: l, disliked: d });
@@ -479,7 +505,7 @@ function describe2(state, top = 4) {
479
505
  // src/reco/dual-bucket/recommender.ts
480
506
  function resolve2(input) {
481
507
  return {
482
- interest: { schema: input.schema },
508
+ interest: { schema: input.schema, weightedScoring: input.weightedScoring ?? false },
483
509
  // 100 is a heavy exploit setting; drop to 20–30 if exploration feels
484
510
  // weak. Live-tunable via setSampleSize().
485
511
  sampleSize: input.sampleSize ?? 100,
@@ -633,6 +659,7 @@ function yearBucket(year, bandSize = 5) {
633
659
  SIGNALS,
634
660
  admit,
635
661
  applyEvent,
662
+ bucketContribution,
636
663
  contribution,
637
664
  decay,
638
665
  defaultRng,
package/dist/reco.mjs CHANGED
@@ -350,6 +350,14 @@ function powerOf(state, value) {
350
350
  const slot = state.slots.find((s) => s.value === value);
351
351
  return slot ? slot.power : 0;
352
352
  }
353
+ function contribution2(state, value, epsilon) {
354
+ if (!value) return epsilon;
355
+ const slot = state.slots.find((s) => s.value === value);
356
+ if (!slot) return epsilon;
357
+ const total = state.slots.reduce((sum, s) => sum + s.power, 0);
358
+ if (total <= 0) return epsilon;
359
+ return epsilon + (1 - epsilon) * (slot.power / total);
360
+ }
353
361
  function isEmpty2(state) {
354
362
  return state.slots.length === 0;
355
363
  }
@@ -389,14 +397,31 @@ function engageNegative(state, item, magnitude, config) {
389
397
  if (!Number.isFinite(magnitude) || magnitude <= 0) return;
390
398
  admitAcrossFeatures(state, "disliked", item, magnitude, config.schema);
391
399
  }
400
+ function featureWeight2(fc, featureCount) {
401
+ if (typeof fc.weight === "number") return fc.weight;
402
+ return featureCount > 0 ? 1 / featureCount : 0;
403
+ }
404
+ function epsilonOf(fc) {
405
+ return fc.epsilon ?? DEFAULT_EPSILON;
406
+ }
392
407
  function scoreItem2(state, item, config) {
393
408
  let liked = 0;
394
409
  let disliked = 0;
395
410
  const byFeature = [];
396
- for (const [key, fc] of Object.entries(config.schema)) {
411
+ const entries = Object.entries(config.schema);
412
+ for (const [key, fc] of entries) {
397
413
  const value = fc.extract(item);
398
- const l = powerOf(state.liked[key], value);
399
- const d = powerOf(state.disliked[key], value);
414
+ let l;
415
+ let d;
416
+ if (config.weightedScoring) {
417
+ const w = featureWeight2(fc, entries.length);
418
+ const eps = epsilonOf(fc);
419
+ l = w * contribution2(state.liked[key], value, eps);
420
+ d = w * contribution2(state.disliked[key], value, eps);
421
+ } else {
422
+ l = powerOf(state.liked[key], value);
423
+ d = powerOf(state.disliked[key], value);
424
+ }
400
425
  liked += l;
401
426
  disliked += d;
402
427
  byFeature.push({ key, value, liked: l, disliked: d });
@@ -423,7 +448,7 @@ function describe2(state, top = 4) {
423
448
  // src/reco/dual-bucket/recommender.ts
424
449
  function resolve2(input) {
425
450
  return {
426
- interest: { schema: input.schema },
451
+ interest: { schema: input.schema, weightedScoring: input.weightedScoring ?? false },
427
452
  // 100 is a heavy exploit setting; drop to 20–30 if exploration feels
428
453
  // weak. Live-tunable via setSampleSize().
429
454
  sampleSize: input.sampleSize ?? 100,
@@ -576,6 +601,7 @@ export {
576
601
  SIGNALS,
577
602
  admit,
578
603
  applyEvent,
604
+ contribution2 as bucketContribution,
579
605
  contribution,
580
606
  decay,
581
607
  defaultRng,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logg/signals",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
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",