@vue-skuilder/db 0.2.5 → 0.2.8

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.
@@ -5,17 +5,29 @@ export type { CardFilter, FilterContext, CardFilterFactory } from './filters/typ
5
5
 
6
6
  // Re-export generator types
7
7
  export type { CardGenerator, GeneratorContext, CardGeneratorFactory, GeneratorResult, ReplanHints } from './generators/types';
8
+
9
+ // Re-export the diversity re-rank stage (pipeline stage 3)
10
+ export {
11
+ diversityRerank,
12
+ DIVERSITY_STRENGTH,
13
+ DIVERSITY_FLOOR,
14
+ type DiversityRerankOptions,
15
+ } from './diversityRerank';
8
16
  import type { GeneratorResult, ReplanHints } from './generators/types';
9
17
 
10
18
  // Re-export pipeline debugger API
11
19
  export {
12
20
  pipelineDebugAPI,
13
21
  mountPipelineDebugger,
22
+ getActivePipeline,
14
23
  type PipelineRunReport,
15
24
  type GeneratorSummary,
16
25
  type FilterImpact,
17
26
  } from './PipelineDebugger';
18
27
 
28
+ // Re-export the commit-free forecast capability surface.
29
+ export type { PipelineForecaster } from './Pipeline';
30
+
19
31
  import { LearnableWeight } from '../types/contentNavigationStrategy';
20
32
  export type { ContentNavigationStrategyData, LearnableWeight } from '../types/contentNavigationStrategy';
21
33
  import type { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ItemQueue } from './ItemQueue';
3
+
4
+ type Item = { cardID: string };
5
+ const id = (i: Item) => i.cardID;
6
+ const item = (cardID: string): Item => ({ cardID });
7
+ const ids = (q: ItemQueue<Item>): string[] =>
8
+ Array.from({ length: q.length }, (_, i) => q.peek(i).cardID);
9
+
10
+ describe('ItemQueue.mergeToFront', () => {
11
+ it('adds new items to the front, preserving batch order', () => {
12
+ const q = new ItemQueue<Item>();
13
+ q.addAll([item('a'), item('b')], id);
14
+
15
+ const added = q.mergeToFront([item('x'), item('y')], id);
16
+
17
+ expect(added).toBe(2);
18
+ expect(ids(q)).toEqual(['x', 'y', 'a', 'b']);
19
+ });
20
+
21
+ it('skips an ordinary duplicate, leaving it in place', () => {
22
+ const q = new ItemQueue<Item>();
23
+ q.addAll([item('a'), item('b'), item('c')], id);
24
+
25
+ // 'b' already queued and not mandatory → left where it is; 'x' fronted.
26
+ const added = q.mergeToFront([item('x'), item('b')], id);
27
+
28
+ expect(added).toBe(1);
29
+ expect(ids(q)).toEqual(['x', 'a', 'b', 'c']);
30
+ });
31
+
32
+ it('re-fronts an already-queued mandatory card instead of burying it', () => {
33
+ // Repro of the require-card burial: 'req' was fronted by a prior burst
34
+ // replan, then an additive merge brings fresh non-required cards. Without
35
+ // the mandatory re-front, 'x'/'y' would leapfrog 'req' and sink it.
36
+ const q = new ItemQueue<Item>();
37
+ q.addAll([item('req'), item('a'), item('b')], id);
38
+
39
+ const added = q.mergeToFront(
40
+ [item('req'), item('x'), item('y')],
41
+ id,
42
+ new Set(['req'])
43
+ );
44
+
45
+ // 'req' is not a *new* add, so it isn't counted...
46
+ expect(added).toBe(2);
47
+ // ...but it leads the queue (ahead of the freshly merged 'x'/'y').
48
+ expect(ids(q)).toEqual(['req', 'x', 'y', 'a', 'b']);
49
+ // and isn't duplicated.
50
+ expect(ids(q).filter((c) => c === 'req')).toHaveLength(1);
51
+ });
52
+
53
+ it('keeps a mandatory card already at the front at the front', () => {
54
+ const q = new ItemQueue<Item>();
55
+ q.addAll([item('req'), item('a')], id);
56
+
57
+ q.mergeToFront([item('req'), item('x')], id, new Set(['req']));
58
+
59
+ expect(ids(q)).toEqual(['req', 'x', 'a']);
60
+ });
61
+
62
+ it('without forceFrontIds, preserves the legacy skip-duplicate behavior', () => {
63
+ const q = new ItemQueue<Item>();
64
+ q.addAll([item('req'), item('a')], id);
65
+
66
+ // No mandatory set → 'req' stays put and is buried behind the merged 'x'.
67
+ q.mergeToFront([item('req'), item('x')], id);
68
+
69
+ expect(ids(q)).toEqual(['x', 'req', 'a']);
70
+ });
71
+ });
@@ -73,8 +73,21 @@ export class ItemQueue<T> {
73
73
  * Merge new items into the front of the queue, skipping duplicates.
74
74
  * Used by additive replans to inject high-quality candidates without
75
75
  * discarding the existing queue contents.
76
+ *
77
+ * `forceFrontIds` carries the mandatory (`+INF`) cards in this batch — a
78
+ * durable `requireCard`/`requireTag` re-asserted by every replan. An ordinary
79
+ * duplicate is left in place (skip), but a mandatory one that's *already*
80
+ * queued is pulled out of its current slot so it rejoins at the front in batch
81
+ * order. Without this, an additive merge unshifts fresh non-required cards
82
+ * ahead of an already-present required card, steadily burying it until it never
83
+ * gets drawn — defeating the "must appear" guarantee. Returns the count of
84
+ * genuinely new cards added (re-fronted duplicates are not counted).
76
85
  */
77
- public mergeToFront(items: T[], cardIdExtractor: (item: T) => string): number {
86
+ public mergeToFront(
87
+ items: T[],
88
+ cardIdExtractor: (item: T) => string,
89
+ forceFrontIds?: ReadonlySet<string>
90
+ ): number {
78
91
  let added = 0;
79
92
  const toInsert: T[] = [];
80
93
  for (const item of items) {
@@ -83,6 +96,11 @@ export class ItemQueue<T> {
83
96
  this.seenCardIds.push(cardId);
84
97
  toInsert.push(item);
85
98
  added++;
99
+ } else if (forceFrontIds?.has(cardId)) {
100
+ const idx = this.q.findIndex((qi) => cardIdExtractor(qi) === cardId);
101
+ if (idx >= 0) {
102
+ toInsert.push(...this.q.splice(idx, 1));
103
+ }
86
104
  }
87
105
  }
88
106
  this.q.unshift(...toInsert);
@@ -1161,9 +1161,22 @@ export class SessionController<TView = unknown> extends Loggable {
1161
1161
  // additive merge, or a stale generator candidate. This is the general guard
1162
1162
  // (see _servedCardIds); it makes re-presentation structurally impossible
1163
1163
  // rather than relying on each upstream path to exclude correctly.
1164
- const newWeighted = mixedWeighted
1165
- .filter((w) => getCardOrigin(w) === 'new' && !this._servedCardIds.has(w.cardId))
1166
- .slice(0, newLimit);
1164
+ const newCandidates = mixedWeighted.filter(
1165
+ (w) => getCardOrigin(w) === 'new' && !this._servedCardIds.has(w.cardId)
1166
+ );
1167
+ // `+INF` is the hard "include at all costs" sentinel applied by require*
1168
+ // injection (see Pipeline.applyRequirement). Partition these mandatory cards
1169
+ // to the front and exempt them from the newLimit slice, so neither the
1170
+ // mixer's source-shuffle/round-robin nor the cap can bury or drop a required
1171
+ // card before it reaches newQ. The set is also handed to mergeToFront so an
1172
+ // already-queued required card gets re-fronted rather than leapfrogged.
1173
+ const mandatoryWeighted = newCandidates.filter((w) => w.score === Number.POSITIVE_INFINITY);
1174
+ const optionalWeighted = newCandidates.filter((w) => w.score !== Number.POSITIVE_INFINITY);
1175
+ const newWeighted = [
1176
+ ...mandatoryWeighted,
1177
+ ...optionalWeighted.slice(0, Math.max(0, newLimit - mandatoryWeighted.length)),
1178
+ ];
1179
+ const mandatoryIds = new Set(mandatoryWeighted.map((w) => w.cardId));
1167
1180
 
1168
1181
  logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
1169
1182
 
@@ -1206,8 +1219,10 @@ export class SessionController<TView = unknown> extends Loggable {
1206
1219
  }
1207
1220
 
1208
1221
  if (additive) {
1209
- // Additive replan: merge new candidates into front of existing queue
1210
- const added = this.newQ.mergeToFront(newItems, (item) => item.cardID);
1222
+ // Additive replan: merge new candidates into front of existing queue.
1223
+ // Pass mandatory (+INF) ids so an already-queued required card is pulled
1224
+ // back to the front instead of being buried by fresh non-required cards.
1225
+ const added = this.newQ.mergeToFront(newItems, (item) => item.cardID, mandatoryIds);
1211
1226
  report += `Additive merge: ${added} new cards added to front of newQ\n`;
1212
1227
  } else if (replan) {
1213
1228
  // Atomic swap: replace entire newQ contents at once (no empty-queue window)