@thanh01.pmt/curriculum-kit 1.4.50 → 1.4.51

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
@@ -11072,6 +11072,15 @@ function cutBackToCheckpoint(current, nodeById, spec, warnings) {
11072
11072
  current.nodeIds = current.entries.map((e) => e.id);
11073
11073
  return displacedEntries.map((e) => nodeById.get(e.id)).filter((n) => n != null);
11074
11074
  }
11075
+ function canCoalesceWithinZpdBudget(curr, next, nodeById, maxNew) {
11076
+ const seen = /* @__PURE__ */ new Set();
11077
+ for (const s of [curr, next]) {
11078
+ for (const nid of s.nodeIds) {
11079
+ for (const c of nodeById.get(nid)?.concept_codes || []) seen.add(c);
11080
+ }
11081
+ }
11082
+ return seen.size <= maxNew;
11083
+ }
11075
11084
  function packUnitSessions(group, nodeById, spec, warnings) {
11076
11085
  const sessions = [];
11077
11086
  const fresh = () => ({ groupId: group.key, nodeIds: [], entries: [], knowledgeMinutes: 0, practiceMinutes: 0, oversized: false });
@@ -11295,10 +11304,10 @@ function checkSessionZpd(sessionIndex, nodeIds, nodeById, allSeenConcepts, allSe
11295
11304
  }
11296
11305
  if (sessionIndex > 0) {
11297
11306
  if (concepts.length > 1 && knownConcepts.length === 0) {
11298
- verdict = "NO_ZPD_BRIDGE";
11307
+ if (verdict !== "TOO_MANY_NEW") verdict = "NO_ZPD_BRIDGE";
11299
11308
  issues.push("No known concepts from previous sessions to bridge new learning");
11300
11309
  } else if (concepts.length === 0 && allKeywords.length > 2 && knownKeywords.length === 0) {
11301
- verdict = "NO_ZPD_BRIDGE";
11310
+ if (verdict !== "TOO_MANY_NEW") verdict = "NO_ZPD_BRIDGE";
11302
11311
  issues.push("No known keywords from previous sessions to bridge new learning");
11303
11312
  }
11304
11313
  }
@@ -11507,7 +11516,8 @@ async function buildCurriculumPlan(rawGraph, options) {
11507
11516
  const currMins = curr.knowledgeMinutes + curr.practiceMinutes;
11508
11517
  const nextMins = next.knowledgeMinutes + next.practiceMinutes;
11509
11518
  const totalContent = currMins + nextMins;
11510
- if (currMins <= 45 || nextMins <= 45 || totalContent <= spec.contentBudget * 1.5) {
11519
+ const maxNew = spec.maxNewConceptsPerSession ?? Infinity;
11520
+ if ((currMins <= 45 || nextMins <= 45 || totalContent <= spec.contentBudget * 1.5) && canCoalesceWithinZpdBudget(curr, next, nodeById, maxNew)) {
11511
11521
  const scale = totalContent > spec.contentBudget ? spec.contentBudget / totalContent : 1;
11512
11522
  curr.nodeIds.push(...next.nodeIds);
11513
11523
  for (const e of next.entries) {
@@ -11576,9 +11586,13 @@ async function buildCurriculumPlan(rawGraph, options) {
11576
11586
  if (options.llmFn) {
11577
11587
  const sys = "You write ONE concise learning objective sentence (max 40 words) for a training session. Output the sentence only.";
11578
11588
  const usr = "Session: " + lessonCode + " \u2014 " + nodeNames.join("; ") + ". Unit: " + unit.name + ". Course goals: " + (constraints.course_goals.join("; ") || "n/a") + ".";
11579
- const out = (await options.llmFn(sys, usr)).trim();
11580
- if (out.length < 10) throw new Error("LLM prose objective too short for " + lessonCode + ': "' + out.slice(0, 40) + '"');
11581
- proseObjective = out;
11589
+ try {
11590
+ const out = (await options.llmFn(sys, usr)).trim();
11591
+ if (out.length < 10) throw new Error("LLM prose objective too short for " + lessonCode + ': "' + out.slice(0, 40) + '"');
11592
+ proseObjective = out;
11593
+ } catch (e) {
11594
+ warnings.push("prose_objective_llm_failed:" + (e instanceof Error ? e.message.slice(0, 80) : String(e).slice(0, 80)));
11595
+ }
11582
11596
  }
11583
11597
  const ctx = { lessonCode, sessionIndex: i, nodeIds: s.nodeIds, packedMinutes: s.knowledgeMinutes + s.practiceMinutes, contentBudget: spec.contentBudget };
11584
11598
  const nodeKeywords = (ids) => [...new Set(ids.flatMap((id) => nodeById.get(id).keywords))];
@@ -11676,6 +11690,32 @@ ${sessionSummaries}`;
11676
11690
  warnings.push("differentiation_llm_failed:" + (e instanceof Error ? e.message.slice(0, 80) : String(e).slice(0, 80)));
11677
11691
  }
11678
11692
  }
11693
+ if (options.llmFn && sessions.length > 0) {
11694
+ try {
11695
+ const eSys = 'You write OBSERVABLE exit evidence for curriculum sessions. Output ONLY a JSON object mapping session-id to an array of 1-3 short strings. Each string must name what a bystander can SEE or VERIFY at the end of the session: a running screen, a passing test, a saved file, correct printed output, a working interaction. NEVER write "Student understands/explains/applies X" \u2014 describe the artifact or observable result instead. Same language as the session objectives.';
11696
+ const eUsr = `Age band: ${constraints.age_band[0]}-${constraints.age_band[1]}. Entry level: ${constraints.entry_level}.
11697
+ Sessions (current exit evidence to improve):
11698
+ ${sessions.map((s) => `${s.id}|objective: ${s.prose_objective.slice(0, 100)}|current: ${s.exit_evidence.join(" ; ")}`).join("\n")}`;
11699
+ const raw = (await options.llmFn(eSys, eUsr)).trim();
11700
+ const match = raw.match(/\{[\s\S]*\}/);
11701
+ if (match) {
11702
+ const parsed = JSON.parse(match[0]);
11703
+ let upgraded = 0;
11704
+ for (const s of sessions) {
11705
+ const arr = parsed[s.id];
11706
+ if (Array.isArray(arr) && arr.length > 0 && arr.every((x) => typeof x === "string" && x.trim().length >= 8)) {
11707
+ s.exit_evidence = arr.map((x) => x.trim());
11708
+ upgraded++;
11709
+ }
11710
+ }
11711
+ if (upgraded < sessions.length) warnings.push(`exit_evidence_llm_partial:${upgraded}/${sessions.length}`);
11712
+ } else {
11713
+ warnings.push("exit_evidence_llm_unparseable");
11714
+ }
11715
+ } catch (e) {
11716
+ warnings.push("exit_evidence_llm_failed:" + (e instanceof Error ? e.message.slice(0, 80) : String(e).slice(0, 80)));
11717
+ }
11718
+ }
11679
11719
  const glossaryScope = sessions.map((s) => ({
11680
11720
  session_id: s.id,
11681
11721
  terms: [...new Set(s.node_ids.flatMap((id) => nodeById.get(id).keywords))]
@@ -31997,6 +32037,7 @@ exports.buildStandardsContextBlock = buildStandardsContextBlock;
31997
32037
  exports.buildTeacherGuidePrompt = buildTeacherGuidePrompt;
31998
32038
  exports.buildWalkingSkeleton = buildWalkingSkeleton;
31999
32039
  exports.buildWorksheetPrompt = buildWorksheetPrompt;
32040
+ exports.canCoalesceWithinZpdBudget = canCoalesceWithinZpdBudget;
32000
32041
  exports.checkSessionZpd = checkSessionZpd;
32001
32042
  exports.classifyDepthCandidates = classifyDepthCandidates;
32002
32043
  exports.closeTruncatedJson = closeTruncatedJson;