@vue-skuilder/db 0.2.7 → 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.
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "0.2.7",
7
+ "version": "0.2.8",
8
8
  "description": "Database layer for vue-skuilder",
9
9
  "main": "dist/index.js",
10
10
  "module": "dist/index.mjs",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@nilock2/pouchdb-authentication": "^1.0.2",
51
- "@vue-skuilder/common": "0.2.7",
51
+ "@vue-skuilder/common": "0.2.8",
52
52
  "cross-fetch": "^4.1.0",
53
53
  "moment": "^2.29.4",
54
54
  "pouchdb": "^9.0.0",
@@ -62,5 +62,5 @@
62
62
  "vite": "^8.0.0",
63
63
  "vitest": "^4.1.0"
64
64
  },
65
- "stableVersion": "0.2.7"
65
+ "stableVersion": "0.2.8"
66
66
  }
@@ -255,7 +255,22 @@ function logCardProvenance(cards: WeightedCard[], maxCards: number = 3): void {
255
255
  * const cards = await pipeline.getWeightedCards(20);
256
256
  * ```
257
257
  */
258
- export class Pipeline extends ContentNavigator {
258
+
259
+ /**
260
+ * Narrow capability surface for out-of-band, commit-free reads against a live
261
+ * pipeline (see {@link getActivePipeline}). Kept minimal on purpose — consumers
262
+ * get the forecast capability, not the whole `Pipeline` class.
263
+ */
264
+ export interface PipelineForecaster {
265
+ forecast(opts?: {
266
+ hints?: ReplanHints;
267
+ unseenOnly?: boolean;
268
+ threshold?: number;
269
+ limit?: number;
270
+ }): Promise<WeightedCard[]>;
271
+ }
272
+
273
+ export class Pipeline extends ContentNavigator implements PipelineForecaster {
259
274
  private generator: CardGenerator;
260
275
  private filters: CardFilter[];
261
276
 
@@ -890,6 +905,83 @@ export class Pipeline extends ContentNavigator {
890
905
  // Card-space diagnostic
891
906
  // ---------------------------------------------------------------------------
892
907
 
908
+ /**
909
+ * Commit-free forecast: score the user's full card space through the filter
910
+ * chain and return the cards that are currently *reachable* (score >=
911
+ * threshold), optionally nudged by caller-supplied hints and/or restricted
912
+ * to cards the user hasn't seen yet.
913
+ *
914
+ * This is a GENERIC primitive — it returns scored, tag-hydrated cards and
915
+ * stops there. It has no knowledge of any particular tag convention; callers
916
+ * decide what the surviving cards mean (e.g. filter to their own "intro"
917
+ * tag family). Nothing is written and no session is started.
918
+ *
919
+ * The optional `hints` are the "out-of-band kick": they run through the same
920
+ * {@link applyHints} path a live replan uses, so the two semantics carry over —
921
+ * - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
922
+ * stays out), and
923
+ * - `requireTags`/`requireCards` inject from the full pre-filter pool,
924
+ * *bypassing* gating (use when you want a card regardless of reachability).
925
+ * Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
926
+ * user has already seen — pass `unseenOnly: false` if that matters.
927
+ *
928
+ * Cost note: like {@link diagnoseCardSpace}, this scans every card through the
929
+ * filters, so it's heavier than a normal replan. Intended for one-shot
930
+ * out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
931
+ *
932
+ * @param opts.hints Optional ephemeral hints to apply after the filter chain.
933
+ * @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
934
+ * @param opts.threshold Min score to count as reachable (default 0.10).
935
+ * @param opts.limit Optional cap on results (already sorted desc).
936
+ */
937
+ async forecast(opts?: {
938
+ hints?: ReplanHints;
939
+ unseenOnly?: boolean;
940
+ threshold?: number;
941
+ limit?: number;
942
+ }): Promise<WeightedCard[]> {
943
+ const threshold = opts?.threshold ?? 0.10;
944
+ const unseenOnly = opts?.unseenOnly ?? true;
945
+
946
+ const courseId = this.course!.getCourseID();
947
+ const allCardIds = await this.course!.getAllCardIds();
948
+
949
+ let cards: WeightedCard[] = allCardIds.map((cardId) => ({
950
+ cardId,
951
+ courseId,
952
+ score: 1.0,
953
+ provenance: [],
954
+ }));
955
+
956
+ cards = await this.hydrateTags(cards);
957
+ // Snapshot the full pool before filtering, for require-injection in applyHints.
958
+ const fullPool = cards.slice();
959
+
960
+ const context = await this.buildContext();
961
+ for (const filter of this.filters) {
962
+ cards = await filter.transform(cards, context);
963
+ }
964
+
965
+ if (opts?.hints) {
966
+ cards = this.applyHints(cards, opts.hints, fullPool);
967
+ }
968
+
969
+ cards = cards.filter((c) => c.score >= threshold);
970
+
971
+ if (unseenOnly) {
972
+ let encountered: Set<string>;
973
+ try {
974
+ encountered = new Set(await this.user!.getSeenCards(courseId));
975
+ } catch {
976
+ encountered = new Set();
977
+ }
978
+ cards = cards.filter((c) => !encountered.has(c.cardId));
979
+ }
980
+
981
+ cards.sort((a, b) => b.score - a.score);
982
+ return opts?.limit ? cards.slice(0, opts.limit) : cards;
983
+ }
984
+
893
985
  /**
894
986
  * Scan every card in the course through the filter chain and report
895
987
  * how many are "well indicated" (score >= threshold) for the current user.
@@ -8,7 +8,7 @@ import {
8
8
  isFilter,
9
9
  } from './index';
10
10
  import { logger } from '../../util/logger';
11
- import type { Pipeline, CardSpaceDiagnosis } from './Pipeline';
11
+ import type { Pipeline, CardSpaceDiagnosis, PipelineForecaster } from './Pipeline';
12
12
  import type { ReplanHints } from './generators/types';
13
13
 
14
14
  /**
@@ -25,6 +25,16 @@ export function registerPipelineForDebug(pipeline: Pipeline): void {
25
25
  _activePipeline = pipeline;
26
26
  }
27
27
 
28
+ /**
29
+ * The most recently constructed pipeline for the current session, or null if
30
+ * none has been built yet. This is the supported, non-debug accessor for
31
+ * out-of-band reads against the live pipeline (e.g. a commit-free
32
+ * `forecast()`), replacing reach-ins through `window.skuilder.pipeline`.
33
+ */
34
+ export function getActivePipeline(): PipelineForecaster | null {
35
+ return _activePipeline;
36
+ }
37
+
28
38
  // ============================================================================
29
39
  // PIPELINE DEBUGGER
30
40
  // ============================================================================
@@ -19,11 +19,15 @@ import type { GeneratorResult, ReplanHints } from './generators/types';
19
19
  export {
20
20
  pipelineDebugAPI,
21
21
  mountPipelineDebugger,
22
+ getActivePipeline,
22
23
  type PipelineRunReport,
23
24
  type GeneratorSummary,
24
25
  type FilterImpact,
25
26
  } from './PipelineDebugger';
26
27
 
28
+ // Re-export the commit-free forecast capability surface.
29
+ export type { PipelineForecaster } from './Pipeline';
30
+
27
31
  import { LearnableWeight } from '../types/contentNavigationStrategy';
28
32
  export type { ContentNavigationStrategyData, LearnableWeight } from '../types/contentNavigationStrategy';
29
33
  import type { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';