@thanh01.pmt/curriculum-kit 1.4.52 → 1.4.53

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/index.d.cts CHANGED
@@ -1145,6 +1145,10 @@ interface PackedSession {
1145
1145
  knowledgeMinutes: number;
1146
1146
  practiceMinutes: number;
1147
1147
  oversized: boolean;
1148
+ /** Distinct NEW concepts counted against the ZPD budget at flush time —
1149
+ * product sessions legitimately restate already-taught concepts, so the
1150
+ * load that matters is what is unseen, not the concept_codes union. */
1151
+ cognitiveNew: number;
1148
1152
  }
1149
1153
  /**
1150
1154
  * Coalesce eligibility (2026-09-18 regen4 audit): a merge must never break
@@ -1181,7 +1185,7 @@ declare function computeConceptSpiralProgression(sessions: Array<{
1181
1185
  * - NO_ZPD_BRIDGE: Session index > 0 with >1 new concepts and 0 known concepts from prior sessions
1182
1186
  * Self-healing: If NO_ZPD_BRIDGE, injects a recap anchor into prerequisite decisions.
1183
1187
  */
1184
- declare function checkSessionZpd(sessionIndex: number, nodeIds: string[], nodeById: Map<string, PlanningNode>, allSeenConcepts: Set<string>, allSeenKeywords: Set<string>, entryKw: Set<string>, warnings: string[]): {
1188
+ declare function checkSessionZpd(sessionIndex: number, nodeIds: string[], nodeById: Map<string, PlanningNode>, allSeenConcepts: Set<string>, allSeenKeywords: Set<string>, entryKw: Set<string>, warnings: string[], conceptPrereqMap?: Map<string, string[]>): {
1185
1189
  status: SessionZpdStatus;
1186
1190
  bridgeRepair?: {
1187
1191
  node_id: string;
package/dist/index.d.ts CHANGED
@@ -1145,6 +1145,10 @@ interface PackedSession {
1145
1145
  knowledgeMinutes: number;
1146
1146
  practiceMinutes: number;
1147
1147
  oversized: boolean;
1148
+ /** Distinct NEW concepts counted against the ZPD budget at flush time —
1149
+ * product sessions legitimately restate already-taught concepts, so the
1150
+ * load that matters is what is unseen, not the concept_codes union. */
1151
+ cognitiveNew: number;
1148
1152
  }
1149
1153
  /**
1150
1154
  * Coalesce eligibility (2026-09-18 regen4 audit): a merge must never break
@@ -1181,7 +1185,7 @@ declare function computeConceptSpiralProgression(sessions: Array<{
1181
1185
  * - NO_ZPD_BRIDGE: Session index > 0 with >1 new concepts and 0 known concepts from prior sessions
1182
1186
  * Self-healing: If NO_ZPD_BRIDGE, injects a recap anchor into prerequisite decisions.
1183
1187
  */
1184
- declare function checkSessionZpd(sessionIndex: number, nodeIds: string[], nodeById: Map<string, PlanningNode>, allSeenConcepts: Set<string>, allSeenKeywords: Set<string>, entryKw: Set<string>, warnings: string[]): {
1188
+ declare function checkSessionZpd(sessionIndex: number, nodeIds: string[], nodeById: Map<string, PlanningNode>, allSeenConcepts: Set<string>, allSeenKeywords: Set<string>, entryKw: Set<string>, warnings: string[], conceptPrereqMap?: Map<string, string[]>): {
1185
1189
  status: SessionZpdStatus;
1186
1190
  bridgeRepair?: {
1187
1191
  node_id: string;
package/dist/index.mjs CHANGED
@@ -11793,10 +11793,16 @@ function canCoalesceWithinZpdBudget(curr, next, nodeById, maxNew) {
11793
11793
  }
11794
11794
  function packUnitSessions(group, nodeById, spec, warnings) {
11795
11795
  const sessions = [];
11796
- const fresh = () => ({ groupId: group.key, nodeIds: [], entries: [], knowledgeMinutes: 0, practiceMinutes: 0, oversized: false });
11796
+ const fresh = () => ({ groupId: group.key, nodeIds: [], entries: [], knowledgeMinutes: 0, practiceMinutes: 0, oversized: false, cognitiveNew: 0 });
11797
11797
  let current = fresh();
11798
+ const seenConcepts = /* @__PURE__ */ new Set();
11798
11799
  const flush = () => {
11799
- if (current.entries.length > 0) sessions.push(current);
11800
+ if (current.entries.length > 0) {
11801
+ for (const nid of current.nodeIds) {
11802
+ for (const c of nodeById.get(nid)?.concept_codes || []) seenConcepts.add(c);
11803
+ }
11804
+ sessions.push(current);
11805
+ }
11800
11806
  current = fresh();
11801
11807
  };
11802
11808
  const filled = () => current.knowledgeMinutes + current.practiceMinutes;
@@ -11838,14 +11844,11 @@ function packUnitSessions(group, nodeById, spec, warnings) {
11838
11844
  continue;
11839
11845
  }
11840
11846
  const minutes = node.estimated_minutes;
11841
- if (spec.maxNewConceptsPerSession && spec.maxNewConceptsPerSession > 0 && current.entries.length > 0) {
11842
- const currentConcepts = new Set(current.nodeIds.flatMap((nid) => nodeById.get(nid)?.concept_codes || []));
11843
- const incoming = nodeById.get(id)?.concept_codes || [];
11844
- const wouldAdd = incoming.filter((c) => !currentConcepts.has(c)).length;
11845
- if (currentConcepts.size + wouldAdd > spec.maxNewConceptsPerSession) {
11846
- warnings.push(`ZPD pack flush: session ${group.key}#${sessions.length + 1} cut at ${currentConcepts.size} new concepts (budget ${spec.maxNewConceptsPerSession})`);
11847
- flush();
11848
- }
11847
+ const localConcepts = new Set(current.nodeIds.flatMap((nid) => nodeById.get(nid)?.concept_codes || []));
11848
+ const nodeNew = node.concept_codes.filter((c) => !seenConcepts.has(c) && !localConcepts.has(c)).length;
11849
+ if (spec.maxNewConceptsPerSession && spec.maxNewConceptsPerSession > 0 && current.entries.length > 0 && current.cognitiveNew + nodeNew > spec.maxNewConceptsPerSession) {
11850
+ warnings.push(`ZPD pack flush: session ${group.key}#${sessions.length + 1} cut at ${current.cognitiveNew} new concepts (budget ${spec.maxNewConceptsPerSession})`);
11851
+ flush();
11849
11852
  }
11850
11853
  if (current.entries.length > 0 && filled() + minutes > spec.contentBudget) {
11851
11854
  const displaced = cutBackToCheckpoint(current, nodeById, spec, warnings);
@@ -11857,11 +11860,33 @@ function packUnitSessions(group, nodeById, spec, warnings) {
11857
11860
  }
11858
11861
  flush();
11859
11862
  }
11860
- if (isKnowledge) current.knowledgeMinutes += minutes;
11861
- else current.practiceMinutes += minutes;
11863
+ if (isKnowledge) {
11864
+ current.knowledgeMinutes += minutes;
11865
+ current.cognitiveNew += nodeNew;
11866
+ } else {
11867
+ current.practiceMinutes += minutes;
11868
+ current.cognitiveNew += nodeNew;
11869
+ }
11862
11870
  current.entries.push({ id, minutes, part: 1, totalParts: 1 });
11863
11871
  current.nodeIds.push(id);
11864
- if (filled() >= spec.contentBudget && current.entries.length >= MAX_SESSION_NODES) flush();
11872
+ if (filled() >= spec.contentBudget && current.entries.length >= MAX_SESSION_NODES) {
11873
+ flush();
11874
+ } else if (!isKnowledge && current.entries.length < MAX_SESSION_NODES) {
11875
+ while (ids.length > 0 && current.entries.length < MAX_SESSION_NODES) {
11876
+ const nxtId = ids[0];
11877
+ const nxt = nodeById.get(nxtId);
11878
+ if (!nxt || nxt.kind !== "concept" || nxt.estimated_minutes > spec.contentBudget) break;
11879
+ const nxtLocal = new Set(current.nodeIds.flatMap((nid) => nodeById.get(nid)?.concept_codes || []));
11880
+ const nxtNew = nxt.concept_codes.filter((c) => !seenConcepts.has(c) && !nxtLocal.has(c)).length;
11881
+ if (spec.maxNewConceptsPerSession && spec.maxNewConceptsPerSession > 0 && current.cognitiveNew + nxtNew > spec.maxNewConceptsPerSession) break;
11882
+ if (filled() + nxt.estimated_minutes > spec.contentBudget) break;
11883
+ ids.shift();
11884
+ current.knowledgeMinutes += nxt.estimated_minutes;
11885
+ current.cognitiveNew += nxtNew;
11886
+ current.entries.push({ id: nxtId, minutes: nxt.estimated_minutes, part: 1, totalParts: 1 });
11887
+ current.nodeIds.push(nxtId);
11888
+ }
11889
+ }
11865
11890
  }
11866
11891
  flush();
11867
11892
  return sessions;
@@ -11996,7 +12021,7 @@ function computeConceptSpiralProgression(sessions, nodeById) {
11996
12021
  }
11997
12022
  return encounters;
11998
12023
  }
11999
- function checkSessionZpd(sessionIndex, nodeIds, nodeById, allSeenConcepts, allSeenKeywords, entryKw, warnings) {
12024
+ function checkSessionZpd(sessionIndex, nodeIds, nodeById, allSeenConcepts, allSeenKeywords, entryKw, warnings, conceptPrereqMap) {
12000
12025
  const concepts = [...new Set(nodeIds.flatMap((id) => nodeById.get(id)?.concept_codes || []))];
12001
12026
  const newConcepts = concepts.filter((c) => !allSeenConcepts.has(c));
12002
12027
  const knownConcepts = concepts.filter((c) => allSeenConcepts.has(c));
@@ -12015,7 +12040,15 @@ function checkSessionZpd(sessionIndex, nodeIds, nodeById, allSeenConcepts, allSe
12015
12040
  if (sessionIndex > 0) {
12016
12041
  if (concepts.length > 1 && knownConcepts.length === 0) {
12017
12042
  if (verdict !== "TOO_MANY_NEW") verdict = "NO_ZPD_BRIDGE";
12018
- issues.push("No known concepts from previous sessions to bridge new learning");
12043
+ const seenList = [...allSeenConcepts];
12044
+ const prevConcept = seenList[seenList.length - 1];
12045
+ const bridgedByPrereq = prevConcept !== void 0 && concepts.some((c) => (conceptPrereqMap?.get(c) || []).includes(prevConcept));
12046
+ if (bridgedByPrereq) {
12047
+ verdict = verdict === "TOO_MANY_NEW" ? verdict : "OK";
12048
+ issues.push(`Bridge satisfied via prerequisite: prior session ended with recap of ${prevConcept}, a declared prerequisite of this session's concepts`);
12049
+ } else {
12050
+ issues.push("No known concepts from previous sessions to bridge new learning");
12051
+ }
12019
12052
  } else if (concepts.length === 0 && allKeywords.length > 2 && knownKeywords.length === 0) {
12020
12053
  if (verdict !== "TOO_MANY_NEW") verdict = "NO_ZPD_BRIDGE";
12021
12054
  issues.push("No known keywords from previous sessions to bridge new learning");
@@ -12272,6 +12305,17 @@ async function buildCurriculumPlan(rawGraph, options) {
12272
12305
  const allSeenConcepts = /* @__PURE__ */ new Set();
12273
12306
  const allSeenKeywords = /* @__PURE__ */ new Set();
12274
12307
  const conceptEncounterMap = /* @__PURE__ */ new Map();
12308
+ const conceptPrereqMap = /* @__PURE__ */ new Map();
12309
+ for (const e of graph.edges) {
12310
+ for (const to of nodeById.get(e.to)?.concept_codes || []) {
12311
+ for (const from of nodeById.get(e.from)?.concept_codes || []) {
12312
+ if (to === from) continue;
12313
+ const list = conceptPrereqMap.get(to) || [];
12314
+ if (!list.includes(from)) list.push(from);
12315
+ conceptPrereqMap.set(to, list);
12316
+ }
12317
+ }
12318
+ }
12275
12319
  for (let i = 0; i < packed.length; i++) {
12276
12320
  const s = packed[i];
12277
12321
  for (const id of s.nodeIds) {
@@ -12325,7 +12369,7 @@ async function buildCurriculumPlan(rawGraph, options) {
12325
12369
  const curCount = (conceptEncounterMap.get(cCode) || 0) + 1;
12326
12370
  conceptEncounterMap.set(cCode, curCount);
12327
12371
  }
12328
- const zpdResult = checkSessionZpd(i, s.nodeIds, nodeById, allSeenConcepts, allSeenKeywords, entryKw, warnings);
12372
+ const zpdResult = checkSessionZpd(i, s.nodeIds, nodeById, allSeenConcepts, allSeenKeywords, entryKw, warnings, conceptPrereqMap);
12329
12373
  const prereqDecisions = decidePrerequisites(ctx, nodeById, graph.edges, sessionIndexByNode, sessionOrderByIndex, constraints);
12330
12374
  if (zpdResult.bridgeRepair) {
12331
12375
  prereqDecisions.unshift(zpdResult.bridgeRepair);