@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.cjs CHANGED
@@ -11805,10 +11805,16 @@ function canCoalesceWithinZpdBudget(curr, next, nodeById, maxNew) {
11805
11805
  }
11806
11806
  function packUnitSessions(group, nodeById, spec, warnings) {
11807
11807
  const sessions = [];
11808
- const fresh = () => ({ groupId: group.key, nodeIds: [], entries: [], knowledgeMinutes: 0, practiceMinutes: 0, oversized: false });
11808
+ const fresh = () => ({ groupId: group.key, nodeIds: [], entries: [], knowledgeMinutes: 0, practiceMinutes: 0, oversized: false, cognitiveNew: 0 });
11809
11809
  let current = fresh();
11810
+ const seenConcepts = /* @__PURE__ */ new Set();
11810
11811
  const flush = () => {
11811
- if (current.entries.length > 0) sessions.push(current);
11812
+ if (current.entries.length > 0) {
11813
+ for (const nid of current.nodeIds) {
11814
+ for (const c of nodeById.get(nid)?.concept_codes || []) seenConcepts.add(c);
11815
+ }
11816
+ sessions.push(current);
11817
+ }
11812
11818
  current = fresh();
11813
11819
  };
11814
11820
  const filled = () => current.knowledgeMinutes + current.practiceMinutes;
@@ -11850,14 +11856,11 @@ function packUnitSessions(group, nodeById, spec, warnings) {
11850
11856
  continue;
11851
11857
  }
11852
11858
  const minutes = node.estimated_minutes;
11853
- if (spec.maxNewConceptsPerSession && spec.maxNewConceptsPerSession > 0 && current.entries.length > 0) {
11854
- const currentConcepts = new Set(current.nodeIds.flatMap((nid) => nodeById.get(nid)?.concept_codes || []));
11855
- const incoming = nodeById.get(id)?.concept_codes || [];
11856
- const wouldAdd = incoming.filter((c) => !currentConcepts.has(c)).length;
11857
- if (currentConcepts.size + wouldAdd > spec.maxNewConceptsPerSession) {
11858
- warnings.push(`ZPD pack flush: session ${group.key}#${sessions.length + 1} cut at ${currentConcepts.size} new concepts (budget ${spec.maxNewConceptsPerSession})`);
11859
- flush();
11860
- }
11859
+ const localConcepts = new Set(current.nodeIds.flatMap((nid) => nodeById.get(nid)?.concept_codes || []));
11860
+ const nodeNew = node.concept_codes.filter((c) => !seenConcepts.has(c) && !localConcepts.has(c)).length;
11861
+ if (spec.maxNewConceptsPerSession && spec.maxNewConceptsPerSession > 0 && current.entries.length > 0 && current.cognitiveNew + nodeNew > spec.maxNewConceptsPerSession) {
11862
+ warnings.push(`ZPD pack flush: session ${group.key}#${sessions.length + 1} cut at ${current.cognitiveNew} new concepts (budget ${spec.maxNewConceptsPerSession})`);
11863
+ flush();
11861
11864
  }
11862
11865
  if (current.entries.length > 0 && filled() + minutes > spec.contentBudget) {
11863
11866
  const displaced = cutBackToCheckpoint(current, nodeById, spec, warnings);
@@ -11869,11 +11872,33 @@ function packUnitSessions(group, nodeById, spec, warnings) {
11869
11872
  }
11870
11873
  flush();
11871
11874
  }
11872
- if (isKnowledge) current.knowledgeMinutes += minutes;
11873
- else current.practiceMinutes += minutes;
11875
+ if (isKnowledge) {
11876
+ current.knowledgeMinutes += minutes;
11877
+ current.cognitiveNew += nodeNew;
11878
+ } else {
11879
+ current.practiceMinutes += minutes;
11880
+ current.cognitiveNew += nodeNew;
11881
+ }
11874
11882
  current.entries.push({ id, minutes, part: 1, totalParts: 1 });
11875
11883
  current.nodeIds.push(id);
11876
- if (filled() >= spec.contentBudget && current.entries.length >= MAX_SESSION_NODES) flush();
11884
+ if (filled() >= spec.contentBudget && current.entries.length >= MAX_SESSION_NODES) {
11885
+ flush();
11886
+ } else if (!isKnowledge && current.entries.length < MAX_SESSION_NODES) {
11887
+ while (ids.length > 0 && current.entries.length < MAX_SESSION_NODES) {
11888
+ const nxtId = ids[0];
11889
+ const nxt = nodeById.get(nxtId);
11890
+ if (!nxt || nxt.kind !== "concept" || nxt.estimated_minutes > spec.contentBudget) break;
11891
+ const nxtLocal = new Set(current.nodeIds.flatMap((nid) => nodeById.get(nid)?.concept_codes || []));
11892
+ const nxtNew = nxt.concept_codes.filter((c) => !seenConcepts.has(c) && !nxtLocal.has(c)).length;
11893
+ if (spec.maxNewConceptsPerSession && spec.maxNewConceptsPerSession > 0 && current.cognitiveNew + nxtNew > spec.maxNewConceptsPerSession) break;
11894
+ if (filled() + nxt.estimated_minutes > spec.contentBudget) break;
11895
+ ids.shift();
11896
+ current.knowledgeMinutes += nxt.estimated_minutes;
11897
+ current.cognitiveNew += nxtNew;
11898
+ current.entries.push({ id: nxtId, minutes: nxt.estimated_minutes, part: 1, totalParts: 1 });
11899
+ current.nodeIds.push(nxtId);
11900
+ }
11901
+ }
11877
11902
  }
11878
11903
  flush();
11879
11904
  return sessions;
@@ -12008,7 +12033,7 @@ function computeConceptSpiralProgression(sessions, nodeById) {
12008
12033
  }
12009
12034
  return encounters;
12010
12035
  }
12011
- function checkSessionZpd(sessionIndex, nodeIds, nodeById, allSeenConcepts, allSeenKeywords, entryKw, warnings) {
12036
+ function checkSessionZpd(sessionIndex, nodeIds, nodeById, allSeenConcepts, allSeenKeywords, entryKw, warnings, conceptPrereqMap) {
12012
12037
  const concepts = [...new Set(nodeIds.flatMap((id) => nodeById.get(id)?.concept_codes || []))];
12013
12038
  const newConcepts = concepts.filter((c) => !allSeenConcepts.has(c));
12014
12039
  const knownConcepts = concepts.filter((c) => allSeenConcepts.has(c));
@@ -12027,7 +12052,15 @@ function checkSessionZpd(sessionIndex, nodeIds, nodeById, allSeenConcepts, allSe
12027
12052
  if (sessionIndex > 0) {
12028
12053
  if (concepts.length > 1 && knownConcepts.length === 0) {
12029
12054
  if (verdict !== "TOO_MANY_NEW") verdict = "NO_ZPD_BRIDGE";
12030
- issues.push("No known concepts from previous sessions to bridge new learning");
12055
+ const seenList = [...allSeenConcepts];
12056
+ const prevConcept = seenList[seenList.length - 1];
12057
+ const bridgedByPrereq = prevConcept !== void 0 && concepts.some((c) => (conceptPrereqMap?.get(c) || []).includes(prevConcept));
12058
+ if (bridgedByPrereq) {
12059
+ verdict = verdict === "TOO_MANY_NEW" ? verdict : "OK";
12060
+ issues.push(`Bridge satisfied via prerequisite: prior session ended with recap of ${prevConcept}, a declared prerequisite of this session's concepts`);
12061
+ } else {
12062
+ issues.push("No known concepts from previous sessions to bridge new learning");
12063
+ }
12031
12064
  } else if (concepts.length === 0 && allKeywords.length > 2 && knownKeywords.length === 0) {
12032
12065
  if (verdict !== "TOO_MANY_NEW") verdict = "NO_ZPD_BRIDGE";
12033
12066
  issues.push("No known keywords from previous sessions to bridge new learning");
@@ -12284,6 +12317,17 @@ async function buildCurriculumPlan(rawGraph, options) {
12284
12317
  const allSeenConcepts = /* @__PURE__ */ new Set();
12285
12318
  const allSeenKeywords = /* @__PURE__ */ new Set();
12286
12319
  const conceptEncounterMap = /* @__PURE__ */ new Map();
12320
+ const conceptPrereqMap = /* @__PURE__ */ new Map();
12321
+ for (const e of graph.edges) {
12322
+ for (const to of nodeById.get(e.to)?.concept_codes || []) {
12323
+ for (const from of nodeById.get(e.from)?.concept_codes || []) {
12324
+ if (to === from) continue;
12325
+ const list = conceptPrereqMap.get(to) || [];
12326
+ if (!list.includes(from)) list.push(from);
12327
+ conceptPrereqMap.set(to, list);
12328
+ }
12329
+ }
12330
+ }
12287
12331
  for (let i = 0; i < packed.length; i++) {
12288
12332
  const s = packed[i];
12289
12333
  for (const id of s.nodeIds) {
@@ -12337,7 +12381,7 @@ async function buildCurriculumPlan(rawGraph, options) {
12337
12381
  const curCount = (conceptEncounterMap.get(cCode) || 0) + 1;
12338
12382
  conceptEncounterMap.set(cCode, curCount);
12339
12383
  }
12340
- const zpdResult = checkSessionZpd(i, s.nodeIds, nodeById, allSeenConcepts, allSeenKeywords, entryKw, warnings);
12384
+ const zpdResult = checkSessionZpd(i, s.nodeIds, nodeById, allSeenConcepts, allSeenKeywords, entryKw, warnings, conceptPrereqMap);
12341
12385
  const prereqDecisions = decidePrerequisites(ctx, nodeById, graph.edges, sessionIndexByNode, sessionOrderByIndex, constraints);
12342
12386
  if (zpdResult.bridgeRepair) {
12343
12387
  prereqDecisions.unshift(zpdResult.bridgeRepair);