@thanh01.pmt/curriculum-kit 1.4.46 → 1.4.48

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
@@ -4019,7 +4019,25 @@ var PlanningNodeSchema = zod.z.object({
4019
4019
  file: zod.z.string(),
4020
4020
  evidence: zod.z.string().default("")
4021
4021
  })).default([]),
4022
- phase_id: zod.z.string().default("")
4022
+ phase_id: zod.z.string().default(""),
4023
+ // P52/T2.1: tangible user-visible outcome projected from the graph step
4024
+ // (checkpoint candidate for session cutting). Empty = not a checkpoint.
4025
+ user_visible_deliverable: zod.z.string().default(""),
4026
+ // P52/T3.1 — depth-reconcile inputs (mirrored from the feed; optional so
4027
+ // raw graphs without variants keep planning unchanged):
4028
+ // • depth_variants: minutes to teach this node at each depth level (ULO ≤
4029
+ // CIO ≤ SIO) — the reconciler's price list for downgrades.
4030
+ // • depth_scaffold_candidates: parallel to scaffold_candidates but acting
4031
+ // on DEPTH (SIO→CIO, CIO→ULO) instead of lesson time.
4032
+ // • is_core: Master-Tree core concept — never downgraded; escalate instead.
4033
+ depth_variants: zod.z.object({ ulo: zod.z.number().int().nonnegative(), cio: zod.z.number().int().nonnegative(), sio: zod.z.number().int().nonnegative() }).optional(),
4034
+ depth_scaffold_candidates: zod.z.array(zod.z.object({
4035
+ from_depth: DepthLevelSchema,
4036
+ to_depth: DepthLevelSchema,
4037
+ minutes_saved: zod.z.number().int().nonnegative(),
4038
+ reason: zod.z.string().default("")
4039
+ })).optional(),
4040
+ is_core: zod.z.boolean().default(false)
4023
4041
  });
4024
4042
  var DependencyEdgeSchema = zod.z.object({
4025
4043
  from: zod.z.string(),
@@ -5436,6 +5454,7 @@ var MAX_NEW_CONCEPTS_PER_SESSION_K12 = 2;
5436
5454
  var MAX_NEW_CONCEPTS_PER_SESSION_ADULT = 3;
5437
5455
  var DEFAULT_OVERHEAD_RATIO = 0.15;
5438
5456
  var MAX_IN_SESSION_SETUP_MINUTES = 15;
5457
+ var SESSION_CUT_TOLERANCE = 0.15;
5439
5458
  var ENVIRONMENT_PROVISIONING_MODELS = [
5440
5459
  "PRE_INSTALLED_LAB",
5441
5460
  "CLOUD_MANAGED",
@@ -10547,6 +10566,98 @@ async function ensureExpositionForLesson(storage, projectId, lessonCode, options
10547
10566
  return { relPath: result.relPath, context: buildExpositionContext(result.content), reused: result.reused, rawContent: result.content };
10548
10567
  }
10549
10568
  init_errors();
10569
+
10570
+ // src/services/depthReconciler.ts
10571
+ function tierOf(node, isLeaf) {
10572
+ if (node.phase_id !== "" && /__ADV\d{2}$/.test(node.id)) return 1;
10573
+ const depth = node.depth_hint ?? "cio";
10574
+ if (depth === "sio") return isLeaf ? 2 : 3;
10575
+ if (depth === "cio") return isLeaf ? 4 : 5;
10576
+ return -1;
10577
+ }
10578
+ function classifyDepthCandidates(nodes, edges) {
10579
+ const baseConcept = (id) => id.replace(/__(ADV|PREV)\d{2}$/, "");
10580
+ const conceptIds = new Set(nodes.filter((n) => n.kind === "concept").map((n) => baseConcept(n.id)));
10581
+ const hasDependent = /* @__PURE__ */ new Set();
10582
+ for (const e of edges) {
10583
+ if (e.kind !== "knowledge") continue;
10584
+ const from = baseConcept(e.from);
10585
+ const to = baseConcept(e.to);
10586
+ if (conceptIds.has(from) && conceptIds.has(to)) hasDependent.add(from);
10587
+ }
10588
+ const isLeaf = /* @__PURE__ */ new Map();
10589
+ for (const n of nodes) {
10590
+ if (n.kind !== "concept") continue;
10591
+ const base = baseConcept(n.id);
10592
+ if (!isLeaf.has(base)) isLeaf.set(base, !hasDependent.has(base));
10593
+ }
10594
+ return isLeaf;
10595
+ }
10596
+ function reconcilePlanDepth(nodes, edges, spec, budgetMinutes) {
10597
+ const MAX_ROUNDS = 5;
10598
+ const total = () => nodes.reduce((acc, n) => acc + n.estimated_minutes, 0);
10599
+ const isLeaf = classifyDepthCandidates(nodes, edges);
10600
+ const baseConcept = (id) => id.replace(/__(ADV|PREV)\d{2}$/, "");
10601
+ const applied = [];
10602
+ const mutated = /* @__PURE__ */ new Set();
10603
+ let rounds = 0;
10604
+ while (total() > budgetMinutes && rounds < MAX_ROUNDS) {
10605
+ rounds++;
10606
+ let overBy = total() - budgetMinutes;
10607
+ const candidates = [];
10608
+ for (let i = 0; i < nodes.length; i++) {
10609
+ const n = nodes[i];
10610
+ if (n.kind !== "concept") continue;
10611
+ if (n.is_core) continue;
10612
+ if (n.depth_hint == null) continue;
10613
+ const base = baseConcept(n.id);
10614
+ const leaf = isLeaf.get(base) ?? true;
10615
+ const tier = tierOf(n, leaf);
10616
+ if (tier < 0) continue;
10617
+ const depth = n.depth_hint ?? "cio";
10618
+ const toDepth = depth === "sio" ? "cio" : "ulo";
10619
+ const variants = n.depth_variants;
10620
+ const target = variants ? variants[toDepth] : Math.max(5, Math.round(n.estimated_minutes * 0.6));
10621
+ const saved = n.estimated_minutes - target;
10622
+ if (saved <= 0) continue;
10623
+ candidates.push({
10624
+ nodeId: n.id,
10625
+ fromDepth: depth,
10626
+ toDepth,
10627
+ minutesSaved: saved,
10628
+ reason: n.depth_scaffold_candidates?.find((c) => c.to_depth === toDepth)?.reason ?? `depth downgrade ${depth}\u2192${toDepth} (tier ${tier}${leaf ? ", leaf" : ""})`,
10629
+ node: n,
10630
+ tier,
10631
+ index: i
10632
+ });
10633
+ }
10634
+ if (candidates.length === 0) break;
10635
+ candidates.sort((a, b) => a.tier - b.tier || b.minutesSaved - a.minutesSaved);
10636
+ for (const c of candidates) {
10637
+ if (overBy <= 0) break;
10638
+ const n = c.node;
10639
+ const target = n.depth_variants ? n.depth_variants[c.toDepth] : Math.max(5, Math.round(n.estimated_minutes * 0.6));
10640
+ const saved = n.estimated_minutes - target;
10641
+ if (saved <= 0) continue;
10642
+ n.estimated_minutes = target;
10643
+ n.depth_hint = c.toDepth;
10644
+ applied.push({ nodeId: n.id, fromDepth: c.fromDepth, toDepth: c.toDepth, minutesSaved: saved, reason: c.reason });
10645
+ mutated.add(n.id);
10646
+ overBy -= saved;
10647
+ }
10648
+ }
10649
+ const finalTotal = total();
10650
+ const deficitMinutes = Math.max(0, finalTotal - budgetMinutes);
10651
+ return {
10652
+ unchanged: applied.length === 0,
10653
+ applied,
10654
+ mutatedNodeIds: [...mutated],
10655
+ escalate: deficitMinutes > 0,
10656
+ deficitMinutes
10657
+ };
10658
+ }
10659
+
10660
+ // src/services/curriculumPlannerService.ts
10550
10661
  var asArr = (v) => Array.isArray(v) ? v : [];
10551
10662
  var asStr = (v, dflt = "") => typeof v === "string" ? v : dflt;
10552
10663
  var asNum = (v, dflt) => typeof v === "number" && Number.isFinite(v) ? v : dflt;
@@ -10586,7 +10697,20 @@ function normalizePlanningGraph(raw) {
10586
10697
  suggest: asStr(c.suggest, "provide_full")
10587
10698
  })),
10588
10699
  references: asArr(n.references).map((ref) => ({ file: asStr(ref.file), evidence: asStr(ref.evidence) })).filter((ref) => ref.file),
10589
- phase_id: asStr(n.phase_id)
10700
+ phase_id: asStr(n.phase_id),
10701
+ user_visible_deliverable: asStr(n.user_visible_deliverable),
10702
+ depth_variants: (() => {
10703
+ const dv = n.depth_variants;
10704
+ if (!dv) return void 0;
10705
+ return { ulo: Math.max(0, Math.round(asNum(dv.ulo, 0))), cio: Math.max(0, Math.round(asNum(dv.cio, 0))), sio: Math.max(0, Math.round(asNum(dv.sio, 0))) };
10706
+ })(),
10707
+ depth_scaffold_candidates: asArr(n.depth_scaffold_candidates).map((c) => ({
10708
+ from_depth: asStr(c.from_depth, "sio"),
10709
+ to_depth: asStr(c.to_depth, "cio"),
10710
+ minutes_saved: Math.max(0, Math.round(asNum(c.minutes_saved, 0))),
10711
+ reason: asStr(c.reason)
10712
+ })),
10713
+ is_core: n.is_core === true
10590
10714
  });
10591
10715
  }
10592
10716
  for (const e of asArr(r.dependency_edges)) {
@@ -10630,7 +10754,9 @@ function normalizePlanningGraph(raw) {
10630
10754
  completion_level: "standard",
10631
10755
  scaffold_candidates: [],
10632
10756
  references: [],
10633
- phase_id: ""
10757
+ phase_id: "",
10758
+ user_visible_deliverable: "",
10759
+ is_core: false
10634
10760
  });
10635
10761
  for (const p of asStrArr(c.prerequisites)) {
10636
10762
  edges.push({ from: p, to: asStr(c.id), kind: "knowledge", reason: "concept prerequisite" });
@@ -10667,7 +10793,9 @@ function normalizePlanningGraph(raw) {
10667
10793
  completion_level: "standard",
10668
10794
  scaffold_candidates: [],
10669
10795
  references: [],
10670
- phase_id: ""
10796
+ phase_id: "",
10797
+ user_visible_deliverable: "",
10798
+ is_core: false
10671
10799
  });
10672
10800
  if (prev) edges.push({ from: prev, to: id, kind: "task", reason: "roadmap sequence" });
10673
10801
  prev = id;
@@ -10703,13 +10831,15 @@ function normalizePlanningGraph(raw) {
10703
10831
  completion_level: "standard",
10704
10832
  scaffold_candidates: [],
10705
10833
  references: [],
10706
- phase_id: ""
10834
+ phase_id: "",
10835
+ user_visible_deliverable: "",
10836
+ is_core: false
10707
10837
  });
10708
10838
  if (prev) edges.push({ from: prev, to: id, kind: "task", reason: "feature build order" });
10709
10839
  prev = id;
10710
10840
  }
10711
10841
  if (stepCount === 0 && fid) {
10712
- nodes.push({ id: fid, kind: "skill", name: asStr(f.name, fid), concept_codes: [], keywords: [], new_keywords: [], prerequisite_keywords: [], estimated_minutes: 45, bloom_hint: "Apply", depth_hint: null, completion_level: "standard", scaffold_candidates: [], references: [], phase_id: "" });
10842
+ nodes.push({ id: fid, kind: "skill", name: asStr(f.name, fid), concept_codes: [], keywords: [], new_keywords: [], prerequisite_keywords: [], estimated_minutes: 45, bloom_hint: "Apply", depth_hint: null, completion_level: "standard", scaffold_candidates: [], references: [], phase_id: "", user_visible_deliverable: "", is_core: false });
10713
10843
  }
10714
10844
  }
10715
10845
  }
@@ -10871,6 +11001,39 @@ function computePackingSpec(constraints) {
10871
11001
  sessionDurationMinutes: constraints.session_duration_minutes
10872
11002
  };
10873
11003
  }
11004
+ var filledOf = (s) => s.knowledgeMinutes + s.practiceMinutes;
11005
+ function cutBackToCheckpoint(current, nodeById, spec, warnings) {
11006
+ const budget = spec.contentBudget;
11007
+ const minFill = budget * (1 - SESSION_CUT_TOLERANCE);
11008
+ const hasPartAfter = new Array(current.entries.length).fill(false);
11009
+ let seenPart = false;
11010
+ for (let i = current.entries.length - 1; i >= 0; i--) {
11011
+ hasPartAfter[i] = seenPart;
11012
+ if (current.entries[i].totalParts > 1) seenPart = true;
11013
+ }
11014
+ let bestIdx = -1;
11015
+ let cumulative = 0;
11016
+ for (let i = 0; i < current.entries.length; i++) {
11017
+ const e = current.entries[i];
11018
+ cumulative += e.minutes;
11019
+ if (hasPartAfter[i] || e.totalParts > 1) continue;
11020
+ const n = nodeById.get(e.id);
11021
+ if (!n || n.user_visible_deliverable.trim().length === 0) continue;
11022
+ if (cumulative > budget || cumulative < minFill) continue;
11023
+ bestIdx = i;
11024
+ }
11025
+ if (bestIdx < 0) {
11026
+ warnings.push("no-checkpoint-within-tolerance: session cut at " + filledOf(current) + "m without a user-visible deliverable");
11027
+ return [];
11028
+ }
11029
+ const displacedEntries = current.entries.splice(bestIdx + 1);
11030
+ for (const e of displacedEntries) {
11031
+ if (nodeById.get(e.id)?.kind === "concept") current.knowledgeMinutes -= e.minutes;
11032
+ else current.practiceMinutes -= e.minutes;
11033
+ }
11034
+ current.nodeIds = current.entries.map((e) => e.id);
11035
+ return displacedEntries.map((e) => nodeById.get(e.id)).filter((n) => n != null);
11036
+ }
10874
11037
  function packUnitSessions(group, nodeById, spec, warnings) {
10875
11038
  const sessions = [];
10876
11039
  const fresh = () => ({ groupId: group.key, nodeIds: [], entries: [], knowledgeMinutes: 0, practiceMinutes: 0, oversized: false });
@@ -10880,7 +11043,9 @@ function packUnitSessions(group, nodeById, spec, warnings) {
10880
11043
  current = fresh();
10881
11044
  };
10882
11045
  const filled = () => current.knowledgeMinutes + current.practiceMinutes;
10883
- for (const id of group.orderedIds) {
11046
+ const ids = [...group.orderedIds];
11047
+ while (ids.length > 0) {
11048
+ const id = ids.shift();
10884
11049
  const node = nodeById.get(id);
10885
11050
  const isKnowledge = node.kind === "concept";
10886
11051
  if (node.estimated_minutes > spec.contentBudget) {
@@ -10916,7 +11081,16 @@ function packUnitSessions(group, nodeById, spec, warnings) {
10916
11081
  continue;
10917
11082
  }
10918
11083
  const minutes = node.estimated_minutes;
10919
- if (current.entries.length > 0 && filled() + minutes > spec.contentBudget) flush();
11084
+ if (current.entries.length > 0 && filled() + minutes > spec.contentBudget) {
11085
+ const displaced = cutBackToCheckpoint(current, nodeById, spec, warnings);
11086
+ if (displaced.length > 0) {
11087
+ ids.unshift(id);
11088
+ for (let di = displaced.length - 1; di >= 0; di--) ids.unshift(displaced[di].id);
11089
+ flush();
11090
+ continue;
11091
+ }
11092
+ flush();
11093
+ }
10920
11094
  if (isKnowledge) current.knowledgeMinutes += minutes;
10921
11095
  else current.practiceMinutes += minutes;
10922
11096
  current.entries.push({ id, minutes, part: 1, totalParts: 1 });
@@ -11226,6 +11400,19 @@ async function buildCurriculumPlan(rawGraph, options) {
11226
11400
  const graph = normalizePlanningGraph(rawGraph);
11227
11401
  warnings.push(...graph.warnings);
11228
11402
  assertAcyclic(graph.nodes, graph.edges);
11403
+ let reconcileSummary = null;
11404
+ if (typeof constraints.total_sessions === "number" && constraints.total_sessions > 0) {
11405
+ const spec0 = computePackingSpec(constraints);
11406
+ const budgetMinutes = constraints.total_sessions * spec0.contentBudget;
11407
+ reconcileSummary = reconcilePlanDepth(graph.nodes, graph.edges, spec0, budgetMinutes);
11408
+ if (!reconcileSummary.unchanged) {
11409
+ const saved = reconcileSummary.applied.reduce((a, x) => a + x.minutesSaved, 0);
11410
+ warnings.push(`depth reconcile: ${reconcileSummary.applied.length} downgrade(s), saved ${saved}m across ${reconcileSummary.mutatedNodeIds.length} node(s)`);
11411
+ }
11412
+ if (reconcileSummary.escalate) {
11413
+ warnings.push(`budget_escalation: deficit ${reconcileSummary.deficitMinutes}m after exhausting non-core depth candidates (budget ${constraints.total_sessions * computePackingSpec(constraints).contentBudget}m) \u2014 human review required`);
11414
+ }
11415
+ }
11229
11416
  const orderedIds = topoSort(graph.nodes, graph.edges);
11230
11417
  const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
11231
11418
  const spec = computePackingSpec(constraints);
@@ -31673,6 +31860,7 @@ exports.RotationStationSchema = RotationStationSchema;
31673
31860
  exports.RubricCriteriaSchema = RubricCriteriaSchema;
31674
31861
  exports.RubricSchema = RubricSchema;
31675
31862
  exports.SATELLITE_LESSON_PRIORITIES = SATELLITE_LESSON_PRIORITIES;
31863
+ exports.SESSION_CUT_TOLERANCE = SESSION_CUT_TOLERANCE;
31676
31864
  exports.SLIDE_CANONICAL_METHODOLOGY = SLIDE_CANONICAL_METHODOLOGY;
31677
31865
  exports.STANDARD_REF_REGEX = STANDARD_REF_REGEX;
31678
31866
  exports.STANDARD_SOT_FILES = STANDARD_SOT_FILES;
@@ -31762,6 +31950,7 @@ exports.buildTeacherGuidePrompt = buildTeacherGuidePrompt;
31762
31950
  exports.buildWalkingSkeleton = buildWalkingSkeleton;
31763
31951
  exports.buildWorksheetPrompt = buildWorksheetPrompt;
31764
31952
  exports.checkSessionZpd = checkSessionZpd;
31953
+ exports.classifyDepthCandidates = classifyDepthCandidates;
31765
31954
  exports.closeTruncatedJson = closeTruncatedJson;
31766
31955
  exports.computeConceptSpiralProgression = computeConceptSpiralProgression;
31767
31956
  exports.computeContentHash = computeContentHash;
@@ -31855,6 +32044,7 @@ exports.loadAuthoringTemplate = loadAuthoringTemplate;
31855
32044
  exports.loadCurriculumTemplate = loadCurriculumTemplate;
31856
32045
  exports.loadSotTemplate = loadSotTemplate;
31857
32046
  exports.normalizePlanningGraph = normalizePlanningGraph;
32047
+ exports.packUnitSessions = packUnitSessions;
31858
32048
  exports.packagerTools = packagerTools;
31859
32049
  exports.parseAllSessions = parseAllSessions;
31860
32050
  exports.parseGateSettings = parseGateSettings;
@@ -31865,6 +32055,7 @@ exports.produceSingleLesson = produceSingleLesson;
31865
32055
  exports.publishToGitHub = publishToGitHub;
31866
32056
  exports.publishToSupabase = publishToSupabase;
31867
32057
  exports.rankGenCandidates = rankGenCandidates;
32058
+ exports.reconcilePlanDepth = reconcilePlanDepth;
31868
32059
  exports.renderAssignedRowsTable = renderAssignedRowsTable;
31869
32060
  exports.renderEntitySlotsForType = renderEntitySlotsForType;
31870
32061
  exports.renderFrameworkFromPlan = renderFrameworkFromPlan;