@thanh01.pmt/curriculum-kit 1.4.49 → 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
@@ -11030,10 +11030,13 @@ function buildUnitGroupings(graph, orderedIds) {
11030
11030
  }
11031
11031
  function computePackingSpec(constraints) {
11032
11032
  const overheadMinutes = Math.round(constraints.session_duration_minutes * constraints.overhead_ratio);
11033
+ const upperAge = constraints.age_band?.[1];
11034
+ const maxNew = upperAge !== void 0 && upperAge > 15 ? MAX_NEW_CONCEPTS_PER_SESSION_ADULT : MAX_NEW_CONCEPTS_PER_SESSION_K12;
11033
11035
  return {
11034
11036
  contentBudget: constraints.session_duration_minutes - overheadMinutes,
11035
11037
  overheadMinutes,
11036
- sessionDurationMinutes: constraints.session_duration_minutes
11038
+ sessionDurationMinutes: constraints.session_duration_minutes,
11039
+ maxNewConceptsPerSession: maxNew
11037
11040
  };
11038
11041
  }
11039
11042
  var filledOf = (s) => s.knowledgeMinutes + s.practiceMinutes;
@@ -11069,6 +11072,15 @@ function cutBackToCheckpoint(current, nodeById, spec, warnings) {
11069
11072
  current.nodeIds = current.entries.map((e) => e.id);
11070
11073
  return displacedEntries.map((e) => nodeById.get(e.id)).filter((n) => n != null);
11071
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
+ }
11072
11084
  function packUnitSessions(group, nodeById, spec, warnings) {
11073
11085
  const sessions = [];
11074
11086
  const fresh = () => ({ groupId: group.key, nodeIds: [], entries: [], knowledgeMinutes: 0, practiceMinutes: 0, oversized: false });
@@ -11116,6 +11128,15 @@ function packUnitSessions(group, nodeById, spec, warnings) {
11116
11128
  continue;
11117
11129
  }
11118
11130
  const minutes = node.estimated_minutes;
11131
+ if (spec.maxNewConceptsPerSession && spec.maxNewConceptsPerSession > 0 && current.entries.length > 0) {
11132
+ const currentConcepts = new Set(current.nodeIds.flatMap((nid) => nodeById.get(nid)?.concept_codes || []));
11133
+ const incoming = nodeById.get(id)?.concept_codes || [];
11134
+ const wouldAdd = incoming.filter((c) => !currentConcepts.has(c)).length;
11135
+ if (currentConcepts.size + wouldAdd > spec.maxNewConceptsPerSession) {
11136
+ warnings.push(`ZPD pack flush: session ${group.key}#${sessions.length + 1} cut at ${currentConcepts.size} new concepts (budget ${spec.maxNewConceptsPerSession})`);
11137
+ flush();
11138
+ }
11139
+ }
11119
11140
  if (current.entries.length > 0 && filled() + minutes > spec.contentBudget) {
11120
11141
  const displaced = cutBackToCheckpoint(current, nodeById, spec, warnings);
11121
11142
  if (displaced.length > 0) {
@@ -11274,19 +11295,19 @@ function checkSessionZpd(sessionIndex, nodeIds, nodeById, allSeenConcepts, allSe
11274
11295
  const knownKeywords = allKeywords.filter((k) => allSeenKeywords.has(k.toLowerCase()) || entryKw.has(k.toLowerCase()));
11275
11296
  const issues = [];
11276
11297
  let verdict = "OK";
11277
- if (concepts.length > 0 && newConcepts.length > 2) {
11298
+ if (newConcepts.length > MAX_NEW_CONCEPTS_PER_SESSION_K12) {
11278
11299
  verdict = "TOO_MANY_NEW";
11279
- issues.push(`${newConcepts.length} new concepts exceed ZPD limit (max 2)`);
11280
- } else if (concepts.length === 0 && newKeywords.length > 4) {
11281
- verdict = "TOO_MANY_NEW";
11282
- issues.push(`${newKeywords.length} new keywords exceed ZPD limit (max 4)`);
11300
+ issues.push(`${newConcepts.length} new concepts exceed ZPD limit (max ${MAX_NEW_CONCEPTS_PER_SESSION_K12})`);
11301
+ }
11302
+ if (newKeywords.length > 4) {
11303
+ issues.push(`Evidence for judge: ${newKeywords.length} new keywords in session (noisy proxy, not a gate)`);
11283
11304
  }
11284
11305
  if (sessionIndex > 0) {
11285
11306
  if (concepts.length > 1 && knownConcepts.length === 0) {
11286
- verdict = "NO_ZPD_BRIDGE";
11307
+ if (verdict !== "TOO_MANY_NEW") verdict = "NO_ZPD_BRIDGE";
11287
11308
  issues.push("No known concepts from previous sessions to bridge new learning");
11288
11309
  } else if (concepts.length === 0 && allKeywords.length > 2 && knownKeywords.length === 0) {
11289
- verdict = "NO_ZPD_BRIDGE";
11310
+ if (verdict !== "TOO_MANY_NEW") verdict = "NO_ZPD_BRIDGE";
11290
11311
  issues.push("No known keywords from previous sessions to bridge new learning");
11291
11312
  }
11292
11313
  }
@@ -11495,7 +11516,8 @@ async function buildCurriculumPlan(rawGraph, options) {
11495
11516
  const currMins = curr.knowledgeMinutes + curr.practiceMinutes;
11496
11517
  const nextMins = next.knowledgeMinutes + next.practiceMinutes;
11497
11518
  const totalContent = currMins + nextMins;
11498
- 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)) {
11499
11521
  const scale = totalContent > spec.contentBudget ? spec.contentBudget / totalContent : 1;
11500
11522
  curr.nodeIds.push(...next.nodeIds);
11501
11523
  for (const e of next.entries) {
@@ -11564,9 +11586,13 @@ async function buildCurriculumPlan(rawGraph, options) {
11564
11586
  if (options.llmFn) {
11565
11587
  const sys = "You write ONE concise learning objective sentence (max 40 words) for a training session. Output the sentence only.";
11566
11588
  const usr = "Session: " + lessonCode + " \u2014 " + nodeNames.join("; ") + ". Unit: " + unit.name + ". Course goals: " + (constraints.course_goals.join("; ") || "n/a") + ".";
11567
- const out = (await options.llmFn(sys, usr)).trim();
11568
- if (out.length < 10) throw new Error("LLM prose objective too short for " + lessonCode + ': "' + out.slice(0, 40) + '"');
11569
- 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
+ }
11570
11596
  }
11571
11597
  const ctx = { lessonCode, sessionIndex: i, nodeIds: s.nodeIds, packedMinutes: s.knowledgeMinutes + s.practiceMinutes, contentBudget: spec.contentBudget };
11572
11598
  const nodeKeywords = (ids) => [...new Set(ids.flatMap((id) => nodeById.get(id).keywords))];
@@ -11664,6 +11690,32 @@ ${sessionSummaries}`;
11664
11690
  warnings.push("differentiation_llm_failed:" + (e instanceof Error ? e.message.slice(0, 80) : String(e).slice(0, 80)));
11665
11691
  }
11666
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
+ }
11667
11719
  const glossaryScope = sessions.map((s) => ({
11668
11720
  session_id: s.id,
11669
11721
  terms: [...new Set(s.node_ids.flatMap((id) => nodeById.get(id).keywords))]
@@ -29951,14 +30003,14 @@ var DeterministicStructuralLinter = class {
29951
30003
  let score = 100;
29952
30004
  for (const s of plan.sessions) {
29953
30005
  if (s.zpd_status?.verdict === "TOO_MANY_NEW") {
29954
- score -= 10;
30006
+ score -= 25;
29955
30007
  findings.push({
29956
30008
  id: `PLAN_TOO_MANY_NEW_${s.id}`,
29957
30009
  dimension: "CONSTRUCTIVE_ALIGNMENT",
29958
- severity: "MINOR",
30010
+ severity: "CRITICAL",
29959
30011
  title: `Cognitive Overload in Session ${s.id}`,
29960
- description: s.zpd_status.issues.join("; ") || `Session introduces too many new concepts/keywords, exceeding ZPD capacity.`,
29961
- remediationAdvice: `Distribute new concepts across multiple sessions or introduce via scaffolding.`,
30012
+ description: s.zpd_status.issues.join("; ") || `Session introduces more new concepts than the ZPD limit allows.`,
30013
+ remediationAdvice: `Split the session: move overflow concepts to a later session (the packer flushes on concept budget automatically).`,
29962
30014
  affectedElement: s.id
29963
30015
  });
29964
30016
  } else if (s.zpd_status?.verdict === "NO_ZPD_BRIDGE") {
@@ -31985,6 +32037,7 @@ exports.buildStandardsContextBlock = buildStandardsContextBlock;
31985
32037
  exports.buildTeacherGuidePrompt = buildTeacherGuidePrompt;
31986
32038
  exports.buildWalkingSkeleton = buildWalkingSkeleton;
31987
32039
  exports.buildWorksheetPrompt = buildWorksheetPrompt;
32040
+ exports.canCoalesceWithinZpdBudget = canCoalesceWithinZpdBudget;
31988
32041
  exports.checkSessionZpd = checkSessionZpd;
31989
32042
  exports.classifyDepthCandidates = classifyDepthCandidates;
31990
32043
  exports.closeTruncatedJson = closeTruncatedJson;