@vue-skuilder/db 0.2.2 → 0.2.3
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/{contentSource-Ht3N2f-y.d.ts → contentSource-Cplhv3bJ.d.ts} +1 -1
- package/dist/{contentSource-BMlMwSiG.d.cts → contentSource-kI9_jwTu.d.cts} +1 -1
- package/dist/core/index.d.cts +5 -5
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +2 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +2 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BEqB8VBR.d.cts → dataLayerProvider-CiA2Rr0v.d.cts} +1 -1
- package/dist/{dataLayerProvider-DObSXjnf.d.ts → dataLayerProvider-DrBqOUa3.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +3 -3
- package/dist/impl/couch/index.d.ts +3 -3
- package/dist/impl/couch/index.js +2 -1
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +2 -1
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +4 -4
- package/dist/impl/static/index.d.ts +4 -4
- package/dist/impl/static/index.js +2 -1
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +2 -1
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-BWvO-_rJ.d.ts → index-BLLT5BYE.d.ts} +1 -1
- package/dist/{index-Ba7hYbHj.d.cts → index-k9NFHpS1.d.cts} +1 -1
- package/dist/index.d.cts +164 -10
- package/dist/index.d.ts +164 -10
- package/dist/index.js +141 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +141 -8
- package/dist/index.mjs.map +1 -1
- package/dist/{types-W8n-B6HG.d.cts → types-BFUa1pa3.d.cts} +1 -1
- package/dist/{types-CJrLM1Ew.d.ts → types-CHgpWQAY.d.ts} +1 -1
- package/dist/{types-legacy-JXDxinpU.d.cts → types-legacy-4tlwHnXo.d.cts} +1 -1
- package/dist/{types-legacy-JXDxinpU.d.ts → types-legacy-4tlwHnXo.d.ts} +1 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/package.json +3 -3
- package/src/core/navigators/Pipeline.ts +1 -1
- package/src/study/SessionController.ts +262 -13
|
@@ -157,4 +157,4 @@ interface QuestionRecord extends CardRecord, Evaluation {
|
|
|
157
157
|
priorAttemps: number;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
export { type CardHistory as C, DocType as D, type Field as F, GuestUsername as G, type QualifiedCardID as Q, type SkuilderCourseData as S, type TagStub as T, type Tag as a, DocTypePrefixes as b, type CardRecord as c, type
|
|
160
|
+
export { type CardHistory as C, DocType as D, type Field as F, GuestUsername as G, type QualifiedCardID as Q, type SkuilderCourseData as S, type TagStub as T, type Tag as a, DocTypePrefixes as b, type CardRecord as c, type QuestionRecord as d, type CardData as e, type CourseListData as f, type DisplayableData as g, type DataShapeData as h, type QuestionData as i, log as l };
|
|
@@ -157,4 +157,4 @@ interface QuestionRecord extends CardRecord, Evaluation {
|
|
|
157
157
|
priorAttemps: number;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
export { type CardHistory as C, DocType as D, type Field as F, GuestUsername as G, type QualifiedCardID as Q, type SkuilderCourseData as S, type TagStub as T, type Tag as a, DocTypePrefixes as b, type CardRecord as c, type
|
|
160
|
+
export { type CardHistory as C, DocType as D, type Field as F, GuestUsername as G, type QualifiedCardID as Q, type SkuilderCourseData as S, type TagStub as T, type Tag as a, DocTypePrefixes as b, type CardRecord as c, type QuestionRecord as d, type CardData as e, type CourseListData as f, type DisplayableData as g, type DataShapeData as h, type QuestionData as i, log as l };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-
|
|
2
|
-
export { C as CouchDBToStaticPacker } from '../../index-
|
|
1
|
+
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-BFUa1pa3.cjs';
|
|
2
|
+
export { C as CouchDBToStaticPacker } from '../../index-k9NFHpS1.cjs';
|
|
3
3
|
import '@vue-skuilder/common';
|
|
4
|
-
import '../../types-legacy-
|
|
4
|
+
import '../../types-legacy-4tlwHnXo.cjs';
|
|
5
5
|
import 'moment';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-
|
|
2
|
-
export { C as CouchDBToStaticPacker } from '../../index-
|
|
1
|
+
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-CHgpWQAY.js';
|
|
2
|
+
export { C as CouchDBToStaticPacker } from '../../index-BLLT5BYE.js';
|
|
3
3
|
import '@vue-skuilder/common';
|
|
4
|
-
import '../../types-legacy-
|
|
4
|
+
import '../../types-legacy-4tlwHnXo.js';
|
|
5
5
|
import 'moment';
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.2.
|
|
7
|
+
"version": "0.2.3",
|
|
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.
|
|
51
|
+
"@vue-skuilder/common": "0.2.3",
|
|
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.
|
|
65
|
+
"stableVersion": "0.2.3"
|
|
66
66
|
}
|
|
@@ -48,7 +48,7 @@ function cardMatchesTagPattern(card: WeightedCard, pattern: string): boolean {
|
|
|
48
48
|
return (card.tags ?? []).some((tag) => globMatch(tag, pattern));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function mergeHints(allHints: Array<ReplanHints | null | undefined>): ReplanHints | undefined {
|
|
51
|
+
export function mergeHints(allHints: Array<ReplanHints | null | undefined>): ReplanHints | undefined {
|
|
52
52
|
const defined = allHints.filter((h): h is ReplanHints => h !== null && h !== undefined);
|
|
53
53
|
if (defined.length === 0) return undefined;
|
|
54
54
|
|
|
@@ -12,11 +12,12 @@ import {
|
|
|
12
12
|
StudySessionReviewItem,
|
|
13
13
|
} from '@db/impl/couch';
|
|
14
14
|
|
|
15
|
-
import { CardRecord, CardHistory, CourseRegistrationDoc, QuestionRecord } from '@db/core';
|
|
15
|
+
import { CardRecord, CardHistory, CourseRegistrationDoc, QuestionRecord, isQuestionRecord } from '@db/core';
|
|
16
16
|
import { recordUserOutcome } from '@db/core/orchestration/recording';
|
|
17
17
|
import { Loggable } from '@db/util';
|
|
18
18
|
import { getCardOrigin } from '@db/core/navigators';
|
|
19
19
|
import { ReplanHints } from '@db/core/navigators/generators/types';
|
|
20
|
+
import { mergeHints } from '@db/core/navigators/Pipeline';
|
|
20
21
|
import { SourceMixer, QuotaRoundRobinMixer, SourceBatch } from './SourceMixer';
|
|
21
22
|
import { captureMixerRun } from './MixerDebugger';
|
|
22
23
|
import { startSessionTracking, recordCardPresentation, snapshotQueues, endSessionTracking } from './SessionDebugger';
|
|
@@ -35,6 +36,28 @@ export type { ReplanHints } from '@db/core/navigators/generators/types';
|
|
|
35
36
|
export interface ReplanOptions {
|
|
36
37
|
/** Scoring hints forwarded to the pipeline (boost/exclude/require). */
|
|
37
38
|
hints?: ReplanHints;
|
|
39
|
+
/**
|
|
40
|
+
* Session-durable scoring hints. Unlike `hints` (one-shot, applied to
|
|
41
|
+
* exactly the run this replan triggers), `sessionHints` are stashed on
|
|
42
|
+
* the controller and re-merged into *every* subsequent pipeline run for
|
|
43
|
+
* the remainder of the session — including the bare auto-replans
|
|
44
|
+
* (depletion/quality) that carry no caller hints, and the wedge-breaker.
|
|
45
|
+
*
|
|
46
|
+
* Use for "emphasis that should outlive a single queue rebuild" — e.g.
|
|
47
|
+
* boosting a just-failed concept tag, or a post-lesson concept boost set
|
|
48
|
+
* at session start. Without this, a one-shot `hints` boost evaporates on
|
|
49
|
+
* the next replan and the freshly-rebuilt (replace-mode) queue clobbers
|
|
50
|
+
* whatever it surfaced.
|
|
51
|
+
*
|
|
52
|
+
* Semantics (KISS): setting `sessionHints` *replaces* the prior session
|
|
53
|
+
* hints wholesale (caller beware — no accumulation, no decay). They live
|
|
54
|
+
* until session end or until explicitly overwritten. Normal usage applies
|
|
55
|
+
* a fixed boost, so repeated identical requests are no-ops.
|
|
56
|
+
*
|
|
57
|
+
* Merged with per-run `hints` via the pipeline's `mergeHints` (boosts
|
|
58
|
+
* multiply, require/exclude lists concatenate).
|
|
59
|
+
*/
|
|
60
|
+
sessionHints?: ReplanHints;
|
|
38
61
|
/**
|
|
39
62
|
* Maximum number of new cards to return from the pipeline.
|
|
40
63
|
* Default: 20 (the standard session batch size).
|
|
@@ -102,6 +125,64 @@ export interface ResponseResult {
|
|
|
102
125
|
shouldClearFeedbackShadow: boolean;
|
|
103
126
|
}
|
|
104
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Read-only snapshot of a single processed response, handed to every
|
|
130
|
+
* registered {@link OutcomeObserver} after ELO/SRS have been recorded.
|
|
131
|
+
*
|
|
132
|
+
* Only emitted for question records (non-question dismisses are skipped).
|
|
133
|
+
*/
|
|
134
|
+
export interface SessionOutcome {
|
|
135
|
+
/** The user's response. Includes `isCorrect`, `performance`, `priorAttemps`. */
|
|
136
|
+
readonly record: QuestionRecord;
|
|
137
|
+
/**
|
|
138
|
+
* The card that was answered, including its `tags` — the primary key an
|
|
139
|
+
* observer matches against (e.g. `gpc:exercise:*`). `card_elo` reflects
|
|
140
|
+
* pre-update state; the ELO write for this response is already in flight.
|
|
141
|
+
*/
|
|
142
|
+
readonly card: StudySessionRecord['card'];
|
|
143
|
+
/** The navigation decision produced for this response (read-only). */
|
|
144
|
+
readonly result: Readonly<ResponseResult>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The narrow capability surface handed to an {@link OutcomeObserver}. This is
|
|
149
|
+
* the *only* way an observer can affect the session — it cannot touch ELO,
|
|
150
|
+
* the queues, the timer, or mutate the `ResponseResult`. A misbehaving
|
|
151
|
+
* observer degrades to "wrong boost", never "corrupted session".
|
|
152
|
+
*/
|
|
153
|
+
export interface SessionControls {
|
|
154
|
+
/** Current session-durable hints, or null. For read-modify-write. */
|
|
155
|
+
getSessionHints(): ReplanHints | null;
|
|
156
|
+
/** Replace the session-durable hints wholesale (no decay). */
|
|
157
|
+
setSessionHints(hints: ReplanHints | null): void;
|
|
158
|
+
/**
|
|
159
|
+
* Merge `hints` into the existing session-durable hints via the pipeline's
|
|
160
|
+
* `mergeHints` (boosts multiply, require/exclude lists concat-dedup).
|
|
161
|
+
* Convenience for the common "add a boost on top of what's there" case.
|
|
162
|
+
* Note: multiplicative + no decay — clamp boost factors yourself if a
|
|
163
|
+
* repeatedly-failed tag could compound unboundedly.
|
|
164
|
+
*/
|
|
165
|
+
mergeSessionHints(hints: ReplanHints): void;
|
|
166
|
+
/** Request a replan (e.g. `{ mode: 'merge' }` for immediate visibility). */
|
|
167
|
+
requestReplan(opts?: ReplanOptions): Promise<void>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A consumer-supplied hook invoked after each question response is processed.
|
|
172
|
+
*
|
|
173
|
+
* Fires on *every* question response (gate inside on `record.isCorrect` /
|
|
174
|
+
* `result.nextCardAction` as needed). Awaited but isolated: a throwing
|
|
175
|
+
* observer is caught and logged, never wedging the session. Keep the
|
|
176
|
+
* synchronous body cheap and `void` any long work (e.g. a triggered replan)
|
|
177
|
+
* so you don't stall navigation.
|
|
178
|
+
*
|
|
179
|
+
* Registered via `StudySessionConfig.outcomeObservers` → constructor options.
|
|
180
|
+
*/
|
|
181
|
+
export type OutcomeObserver = (
|
|
182
|
+
outcome: SessionOutcome,
|
|
183
|
+
controls: SessionControls
|
|
184
|
+
) => void | Promise<void>;
|
|
185
|
+
|
|
105
186
|
interface SessionServices {
|
|
106
187
|
response: ResponseProcessor;
|
|
107
188
|
}
|
|
@@ -177,6 +258,35 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
177
258
|
*/
|
|
178
259
|
private _minCardsGuarantee: number = 0;
|
|
179
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Session-durable scoring hints. Re-merged into every pipeline run for
|
|
263
|
+
* the rest of the session (initial plan + every replan, including bare
|
|
264
|
+
* auto-replans and the wedge-breaker), via `_applyHintsToSources`.
|
|
265
|
+
*
|
|
266
|
+
* Set by `setSessionHints()` (e.g. session-start post-lesson boost) or by
|
|
267
|
+
* any replan carrying `ReplanOptions.sessionHints` (e.g. a just-failed
|
|
268
|
+
* concept boost). Replace semantics, no decay — lives until overwritten
|
|
269
|
+
* or session end. See `ReplanOptions.sessionHints` for rationale.
|
|
270
|
+
*
|
|
271
|
+
* Note: the controller-managed auto-excludes (current card, session
|
|
272
|
+
* record, imminent draw) are intentionally NOT folded in here — those are
|
|
273
|
+
* recomputed per-run in `_runReplan` and would otherwise go stale.
|
|
274
|
+
*/
|
|
275
|
+
private _sessionHints: ReplanHints | null = null;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Consumer-supplied hooks invoked after each question response is processed.
|
|
279
|
+
* Seeded from constructor options (threaded from
|
|
280
|
+
* `StudySessionConfig.outcomeObservers`). See {@link OutcomeObserver}.
|
|
281
|
+
*/
|
|
282
|
+
private _outcomeObservers: OutcomeObserver[] = [];
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Lazily-built, stable capability object handed to observers. Bound to
|
|
286
|
+
* `this`; constructed once so observers can rely on referential identity.
|
|
287
|
+
*/
|
|
288
|
+
private _sessionControls: SessionControls | null = null;
|
|
289
|
+
|
|
180
290
|
private startTime: Date;
|
|
181
291
|
private endTime: Date;
|
|
182
292
|
private _secondsRemaining: number;
|
|
@@ -219,7 +329,11 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
219
329
|
dataLayer: DataLayerProvider,
|
|
220
330
|
getViewComponent: (viewId: string) => TView,
|
|
221
331
|
mixer?: SourceMixer,
|
|
222
|
-
options?: {
|
|
332
|
+
options?: {
|
|
333
|
+
defaultBatchLimit?: number;
|
|
334
|
+
initialReviewCap?: number;
|
|
335
|
+
outcomeObservers?: OutcomeObserver[];
|
|
336
|
+
}
|
|
223
337
|
) {
|
|
224
338
|
super();
|
|
225
339
|
|
|
@@ -249,6 +363,9 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
249
363
|
if (options?.initialReviewCap !== undefined) {
|
|
250
364
|
this._initialReviewCap = options.initialReviewCap;
|
|
251
365
|
}
|
|
366
|
+
if (options?.outcomeObservers?.length) {
|
|
367
|
+
this._outcomeObservers = [...options.outcomeObservers];
|
|
368
|
+
}
|
|
252
369
|
|
|
253
370
|
this.log(`Session constructed:
|
|
254
371
|
startTime: ${this.startTime}
|
|
@@ -409,6 +526,7 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
409
526
|
if (opts.minFollowUpCards !== undefined) return true;
|
|
410
527
|
if (opts.mode && opts.mode !== 'replace') return true;
|
|
411
528
|
if (opts.hints && Object.keys(opts.hints).length > 0) return true;
|
|
529
|
+
if (opts.sessionHints !== undefined) return true;
|
|
412
530
|
return false;
|
|
413
531
|
}
|
|
414
532
|
|
|
@@ -455,17 +573,21 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
455
573
|
|
|
456
574
|
hints.excludeCards = [...excludeSet];
|
|
457
575
|
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
576
|
+
// Replace session-durable hints if this replan carries them. KISS:
|
|
577
|
+
// wholesale replace, no accumulation/decay (see ReplanOptions.sessionHints).
|
|
578
|
+
if (opts.sessionHints !== undefined) {
|
|
579
|
+
this._sessionHints = opts.sessionHints;
|
|
580
|
+
this.log(
|
|
581
|
+
`[Replan] Session hints ${opts.sessionHints ? 'set' : 'cleared'}: ` +
|
|
582
|
+
`${JSON.stringify(opts.sessionHints)}`
|
|
583
|
+
);
|
|
467
584
|
}
|
|
468
585
|
|
|
586
|
+
// Forward hints to all sources (CourseDB stashes them, Pipeline consumes
|
|
587
|
+
// them). The one-shot `opts.hints` are merged with the durable
|
|
588
|
+
// `_sessionHints` so session emphasis survives this and every later run.
|
|
589
|
+
this._applyHintsToSources(opts.hints, opts.label);
|
|
590
|
+
|
|
469
591
|
const labelTag = opts.label ? ` [${opts.label}]` : '';
|
|
470
592
|
this.log(
|
|
471
593
|
`Mid-session replan requested${labelTag}` +
|
|
@@ -487,6 +609,117 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
487
609
|
// );
|
|
488
610
|
}
|
|
489
611
|
|
|
612
|
+
/**
|
|
613
|
+
* Set the session-durable scoring hints (replace semantics, no decay).
|
|
614
|
+
*
|
|
615
|
+
* Unlike a one-shot replan hint, these are re-merged into every pipeline
|
|
616
|
+
* run for the rest of the session — including the initial plan when set
|
|
617
|
+
* before `prepareSession()`, every replan, the bare auto-replans, and the
|
|
618
|
+
* wedge-breaker. Pass `null` to clear.
|
|
619
|
+
*
|
|
620
|
+
* Typical callers:
|
|
621
|
+
* - `StudySession` at session start, threading `StudySessionConfig.initHints`
|
|
622
|
+
* (e.g. a post-lesson concept boost) — so the boost outlives the first
|
|
623
|
+
* queue rebuild instead of being clobbered by the first auto-replan.
|
|
624
|
+
* - A consumer view on a failure, boosting the just-failed concept tag.
|
|
625
|
+
*
|
|
626
|
+
* Does not itself trigger a replan; the next plan/replan picks it up.
|
|
627
|
+
*/
|
|
628
|
+
public setSessionHints(hints: ReplanHints | null): void {
|
|
629
|
+
this._sessionHints = hints;
|
|
630
|
+
this.log(`Session hints ${hints ? 'set' : 'cleared'}: ${JSON.stringify(hints)}`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Read the current session-durable hints (for read-modify-write callers,
|
|
635
|
+
* e.g. an outcome observer that clamps a compounding boost).
|
|
636
|
+
*/
|
|
637
|
+
public getSessionHints(): ReplanHints | null {
|
|
638
|
+
return this._sessionHints;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Merge `hints` into the durable session hints via the pipeline's
|
|
643
|
+
* `mergeHints` (boosts multiply, require/exclude lists concat-dedup).
|
|
644
|
+
* Convenience over get-then-set for the common additive case. Note the
|
|
645
|
+
* multiplicative, no-decay semantics — clamp boost factors at the call
|
|
646
|
+
* site if a repeatedly-emphasised tag could compound unboundedly.
|
|
647
|
+
*/
|
|
648
|
+
public mergeSessionHints(hints: ReplanHints): void {
|
|
649
|
+
this._sessionHints = mergeHints([this._sessionHints, hints]) ?? null;
|
|
650
|
+
this.log(`Session hints merged: ${JSON.stringify(this._sessionHints)}`);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Merge the durable `_sessionHints` with this run's one-shot hints and
|
|
655
|
+
* push the result to every source for consumption on the next pipeline
|
|
656
|
+
* run. Centralised so the initial plan and all replan paths apply session
|
|
657
|
+
* emphasis identically. No-op when there are no hints of either kind.
|
|
658
|
+
*/
|
|
659
|
+
private _applyHintsToSources(oneShot?: ReplanHints, label?: string): void {
|
|
660
|
+
// Thread the provenance label into the one-shot layer; mergeHints will
|
|
661
|
+
// fold it into the combined `_label`.
|
|
662
|
+
const oneShotWithLabel: ReplanHints | undefined =
|
|
663
|
+
oneShot && label ? { ...oneShot, _label: label } : oneShot;
|
|
664
|
+
|
|
665
|
+
const merged = mergeHints([this._sessionHints, oneShotWithLabel]);
|
|
666
|
+
if (!merged) return;
|
|
667
|
+
|
|
668
|
+
for (const source of this.sources) {
|
|
669
|
+
source.setEphemeralHints?.(merged);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Build (once) the stable capability object handed to outcome observers.
|
|
675
|
+
* Methods are bound to `this`; the object identity is stable across calls
|
|
676
|
+
* so observers may key off it.
|
|
677
|
+
*/
|
|
678
|
+
private _getSessionControls(): SessionControls {
|
|
679
|
+
if (!this._sessionControls) {
|
|
680
|
+
this._sessionControls = {
|
|
681
|
+
getSessionHints: () => this.getSessionHints(),
|
|
682
|
+
setSessionHints: (h) => this.setSessionHints(h),
|
|
683
|
+
mergeSessionHints: (h) => this.mergeSessionHints(h),
|
|
684
|
+
requestReplan: (opts) => this.requestReplan(opts),
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
return this._sessionControls;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Notify registered outcome observers about a processed response.
|
|
692
|
+
*
|
|
693
|
+
* Only question records are surfaced (non-question dismisses are skipped).
|
|
694
|
+
* Observers run after ELO/SRS are recorded and before navigation. Each is
|
|
695
|
+
* awaited but isolated in try/catch — a throwing observer is logged and
|
|
696
|
+
* skipped, never wedging the session. Keep observers cheap and `void` any
|
|
697
|
+
* long work (e.g. a triggered replan) to avoid stalling the draw.
|
|
698
|
+
*/
|
|
699
|
+
private async _notifyOutcomeObservers(
|
|
700
|
+
record: CardRecord,
|
|
701
|
+
currentCard: StudySessionRecord,
|
|
702
|
+
result: ResponseResult
|
|
703
|
+
): Promise<void> {
|
|
704
|
+
if (this._outcomeObservers.length === 0) return;
|
|
705
|
+
if (!isQuestionRecord(record)) return;
|
|
706
|
+
|
|
707
|
+
const outcome: SessionOutcome = {
|
|
708
|
+
record,
|
|
709
|
+
card: currentCard.card,
|
|
710
|
+
result,
|
|
711
|
+
};
|
|
712
|
+
const controls = this._getSessionControls();
|
|
713
|
+
|
|
714
|
+
for (const observer of this._outcomeObservers) {
|
|
715
|
+
try {
|
|
716
|
+
await observer(outcome, controls);
|
|
717
|
+
} catch (e) {
|
|
718
|
+
this.error('[OutcomeObserver] observer threw; ignoring', e);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
490
723
|
/**
|
|
491
724
|
* Run a replan, bypassing requestReplan()'s coalesce logic.
|
|
492
725
|
*
|
|
@@ -519,7 +752,7 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
519
752
|
if (!input) return {};
|
|
520
753
|
|
|
521
754
|
// If the input has any ReplanOptions-specific key, treat it as ReplanOptions
|
|
522
|
-
const replanKeys = ['hints', 'limit', 'mode', 'label', 'minFollowUpCards'];
|
|
755
|
+
const replanKeys = ['hints', 'sessionHints', 'limit', 'mode', 'label', 'minFollowUpCards'];
|
|
523
756
|
const inputKeys = Object.keys(input);
|
|
524
757
|
if (inputKeys.some((k) => replanKeys.includes(k))) {
|
|
525
758
|
return input as ReplanOptions;
|
|
@@ -706,6 +939,14 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
706
939
|
// never touch reviewQ, so the inflation is unnecessary there.
|
|
707
940
|
const fetchLimit = replan ? newLimit : newLimit + this._initialReviewCap;
|
|
708
941
|
|
|
942
|
+
// Initial plan: push session-durable hints to sources so the very first
|
|
943
|
+
// pipeline run reflects them (e.g. a post-lesson boost). Replans push
|
|
944
|
+
// their own session+one-shot merge via _runReplan before reaching here,
|
|
945
|
+
// so we must NOT re-apply here or we'd drop their per-run excludeCards.
|
|
946
|
+
if (!replan) {
|
|
947
|
+
this._applyHintsToSources();
|
|
948
|
+
}
|
|
949
|
+
|
|
709
950
|
// Collect batches from each source
|
|
710
951
|
const batches: SourceBatch[] = [];
|
|
711
952
|
|
|
@@ -1192,7 +1433,7 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
1192
1433
|
...currentCard.item,
|
|
1193
1434
|
};
|
|
1194
1435
|
|
|
1195
|
-
|
|
1436
|
+
const result = await this.services.response.processResponse(
|
|
1196
1437
|
cardRecord,
|
|
1197
1438
|
cardHistory,
|
|
1198
1439
|
studySessionItem,
|
|
@@ -1204,6 +1445,14 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
1204
1445
|
maxSessionViews,
|
|
1205
1446
|
sessionViews
|
|
1206
1447
|
);
|
|
1448
|
+
|
|
1449
|
+
// Surface the processed outcome to any registered observers (e.g. a
|
|
1450
|
+
// difficulty-booster that bumps session hints on a failed exercise tag).
|
|
1451
|
+
// Runs after ELO/SRS recording, before the caller navigates. Isolated so
|
|
1452
|
+
// a faulty observer can't break response handling.
|
|
1453
|
+
await this._notifyOutcomeObservers(cardRecord, currentCard, result);
|
|
1454
|
+
|
|
1455
|
+
return result;
|
|
1207
1456
|
}
|
|
1208
1457
|
|
|
1209
1458
|
private dismissCurrentCard(action: SessionAction = 'dismiss-success') {
|