@thanh01.pmt/domain-kit 0.4.0 → 0.6.0
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/README.md +682 -168
- package/dist/assembly/index.d.cts +2 -2
- package/dist/assembly/index.d.ts +2 -2
- package/dist/{chunk-HILSUZOF.mjs → chunk-2OGNQUXC.mjs} +171 -31
- package/dist/chunk-2OGNQUXC.mjs.map +1 -0
- package/dist/{chunk-LZQLLAYP.mjs → chunk-5OMSYKQP.mjs} +43 -23
- package/dist/chunk-5OMSYKQP.mjs.map +1 -0
- package/dist/{chunk-BL5NBKPH.mjs → chunk-67GXK3ZE.mjs} +18 -8
- package/dist/chunk-67GXK3ZE.mjs.map +1 -0
- package/dist/{chunk-PKUEOSZ3.mjs → chunk-CGNXGVHV.mjs} +46 -4
- package/dist/chunk-CGNXGVHV.mjs.map +1 -0
- package/dist/{chunk-VCPIWJ7R.mjs → chunk-ERDTHXQA.mjs} +21 -3
- package/dist/chunk-ERDTHXQA.mjs.map +1 -0
- package/dist/chunk-OYSZQVPY.mjs +10 -0
- package/dist/chunk-OYSZQVPY.mjs.map +1 -0
- package/dist/{chunk-QJF4QCBJ.mjs → chunk-WKTH3JPA.mjs} +202 -36
- package/dist/chunk-WKTH3JPA.mjs.map +1 -0
- package/dist/{conceptEscalator-YfMKIKCZ.d.ts → conceptEscalator-BCsw0ys_.d.ts} +10 -1
- package/dist/{conceptEscalator-ltRLtPhf.d.cts → conceptEscalator-DPrs4PwC.d.cts} +10 -1
- package/dist/concepts/index.d.cts +1 -1
- package/dist/concepts/index.d.ts +1 -1
- package/dist/{curriculumFeedSchema-B66H3gEV.d.ts → curriculumFeedSchema-DYayelAA.d.cts} +212 -1
- package/dist/{curriculumFeedSchema-B66H3gEV.d.cts → curriculumFeedSchema-DYayelAA.d.ts} +212 -1
- package/dist/detector/index.cjs +21 -6
- package/dist/detector/index.cjs.map +1 -1
- package/dist/detector/index.mjs +2 -1
- package/dist/feed/index.cjs +243 -33
- package/dist/feed/index.cjs.map +1 -1
- package/dist/feed/index.d.cts +32 -2
- package/dist/feed/index.d.ts +32 -2
- package/dist/feed/index.mjs +2 -2
- package/dist/graph/index.cjs +163 -28
- package/dist/graph/index.cjs.map +1 -1
- package/dist/graph/index.d.cts +14 -5
- package/dist/graph/index.d.ts +14 -5
- package/dist/graph/index.mjs +2 -1
- package/dist/{hybridGraphPipeline-DXQDbBEj.d.ts → hybridGraphPipeline-9dRKJj7r.d.ts} +6 -2
- package/dist/{hybridGraphPipeline-dT-bio-3.d.cts → hybridGraphPipeline-CocfYe4d.d.cts} +6 -2
- package/dist/{hybridGraphSchema-nrUeSwpU.d.ts → hybridGraphSchema-C37qxdTM.d.cts} +16 -3
- package/dist/{hybridGraphSchema-nrUeSwpU.d.cts → hybridGraphSchema-C37qxdTM.d.ts} +16 -3
- package/dist/index.cjs +3742 -3362
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.mjs +7 -6
- package/dist/llmClient-CX5uUiQ1.d.cts +60 -0
- package/dist/llmClient-CX5uUiQ1.d.ts +60 -0
- package/dist/pipeline/index.cjs +1556 -1202
- package/dist/pipeline/index.cjs.map +1 -1
- package/dist/pipeline/index.d.cts +13 -5
- package/dist/pipeline/index.d.ts +13 -5
- package/dist/pipeline/index.mjs +6 -5
- package/dist/schemas/index.cjs +64 -2
- package/dist/schemas/index.cjs.map +1 -1
- package/dist/schemas/index.d.cts +2 -2
- package/dist/schemas/index.d.ts +2 -2
- package/dist/schemas/index.mjs +2 -2
- package/package.json +1 -1
- package/dist/chunk-BL5NBKPH.mjs.map +0 -1
- package/dist/chunk-HILSUZOF.mjs.map +0 -1
- package/dist/chunk-LZQLLAYP.mjs.map +0 -1
- package/dist/chunk-PKUEOSZ3.mjs.map +0 -1
- package/dist/chunk-QJF4QCBJ.mjs.map +0 -1
- package/dist/chunk-VCPIWJ7R.mjs.map +0 -1
- package/dist/curriculumFeedEmitter-6IK6PFDH.mjs +0 -4
- package/dist/curriculumFeedEmitter-6IK6PFDH.mjs.map +0 -1
- package/dist/llmClient-ysPhLjcH.d.cts +0 -16
- package/dist/llmClient-ysPhLjcH.d.ts +0 -16
package/dist/feed/index.cjs
CHANGED
|
@@ -52,6 +52,23 @@ var FeedNodeSchema = zod.z.object({
|
|
|
52
52
|
reason: zod.z.string().default("")
|
|
53
53
|
})).optional(),
|
|
54
54
|
is_core: zod.z.boolean().default(false),
|
|
55
|
+
/**
|
|
56
|
+
* Prerequisite concept ids this node needs that NO node in this feed provides —
|
|
57
|
+
* i.e. the learner must already know them before starting. Previously these were
|
|
58
|
+
* silently dropped (knowledge path) or crashed emission (both hybrid paths), so a
|
|
59
|
+
* course silently lost its entry requirements. Kept on the node so the mapping
|
|
60
|
+
* "concept X needs prior knowledge Y" survives; a course-level roll-up is derivable.
|
|
61
|
+
*/
|
|
62
|
+
external_prerequisites: zod.z.array(zod.z.string()).default([]),
|
|
63
|
+
// Depth layer text (optional, additive — old feeds validate unchanged).
|
|
64
|
+
// Carries the ULO/CIO/SIO authored text from the KnowledgeGraph concept
|
|
65
|
+
// so downstream consumers (KX generator) can inject depth-appropriate
|
|
66
|
+
// material into their prompts without a second LLM call.
|
|
67
|
+
depth_context: zod.z.object({
|
|
68
|
+
ulo: zod.z.string().default(""),
|
|
69
|
+
cio: zod.z.string().default(""),
|
|
70
|
+
sio: zod.z.array(zod.z.string()).default([])
|
|
71
|
+
}).optional(),
|
|
55
72
|
// Progressive-completion bookkeeping (feed v2, additive):
|
|
56
73
|
// phase_id = development phase that introduces/uses this node;
|
|
57
74
|
// introduce_aspect = how the phase teaches a concept node (first intro, advanced
|
|
@@ -78,12 +95,28 @@ var FeedPhaseSchema = zod.z.object({
|
|
|
78
95
|
product_completion: zod.z.string().min(1),
|
|
79
96
|
node_ids: zod.z.array(zod.z.string()).min(1)
|
|
80
97
|
});
|
|
98
|
+
var FeedDiagnosticSchema = zod.z.object({
|
|
99
|
+
code: zod.z.string().min(1),
|
|
100
|
+
severity: zod.z.enum(["info", "warning", "error"]),
|
|
101
|
+
message: zod.z.string(),
|
|
102
|
+
node_ids: zod.z.array(zod.z.string()).default([])
|
|
103
|
+
});
|
|
104
|
+
var FeedProvenanceSchema = zod.z.object({
|
|
105
|
+
providers: zod.z.array(zod.z.string()).default([]),
|
|
106
|
+
models: zod.z.array(zod.z.string()).default([]),
|
|
107
|
+
calls: zod.z.number().int().nonnegative().default(0),
|
|
108
|
+
failed_calls: zod.z.number().int().nonnegative().default(0),
|
|
109
|
+
degraded: zod.z.boolean().default(false)
|
|
110
|
+
});
|
|
81
111
|
var CurriculumFeedSchema = zod.z.object({
|
|
82
112
|
schema_version: zod.z.literal(2),
|
|
83
113
|
source: zod.z.object({
|
|
84
114
|
graph_type: zod.z.enum(["project_graph", "knowledge_graph", "hybrid_graph"]),
|
|
115
|
+
/** DERIVED from diagnostics — do not write to it directly. */
|
|
85
116
|
warnings: zod.z.array(zod.z.string()).default([]),
|
|
86
|
-
hallucination_count: zod.z.number().int().nonnegative().default(0)
|
|
117
|
+
hallucination_count: zod.z.number().int().nonnegative().default(0),
|
|
118
|
+
diagnostics: zod.z.array(FeedDiagnosticSchema).default([]),
|
|
119
|
+
provenance: FeedProvenanceSchema.optional()
|
|
87
120
|
}),
|
|
88
121
|
learning_nodes: zod.z.array(FeedNodeSchema).min(1, "curriculum feed must contain at least one learning node"),
|
|
89
122
|
dependency_edges: zod.z.array(FeedEdgeSchema).default([]),
|
|
@@ -116,6 +149,15 @@ function validateCurriculumFeed(feed) {
|
|
|
116
149
|
}
|
|
117
150
|
const phaseIds = parsed.phases.map((p) => p.id);
|
|
118
151
|
if (new Set(phaseIds).size !== phaseIds.length) throw new Error("curriculum feed contains duplicate phase ids");
|
|
152
|
+
const phaseOrders = parsed.phases.map((p) => p.order);
|
|
153
|
+
if (new Set(phaseOrders).size !== phaseOrders.length) {
|
|
154
|
+
throw new Error("curriculum feed contains duplicate phase orders: " + phaseOrders.join(", "));
|
|
155
|
+
}
|
|
156
|
+
for (let i = 1; i < phaseOrders.length; i++) {
|
|
157
|
+
if (phaseOrders[i] <= phaseOrders[i - 1]) {
|
|
158
|
+
throw new Error("curriculum feed phase orders are not ascending: " + phaseOrders.join(", "));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
119
161
|
if (parsed.phases.length > 0) {
|
|
120
162
|
const phaseIndexOf = /* @__PURE__ */ new Map();
|
|
121
163
|
parsed.phases.forEach((p, i) => {
|
|
@@ -136,18 +178,82 @@ function validateCurriculumFeed(feed) {
|
|
|
136
178
|
}
|
|
137
179
|
|
|
138
180
|
// src/feed/curriculumFeedEmitter.ts
|
|
181
|
+
var DEPTH_MINUTE_FRACTIONS = {
|
|
182
|
+
/** Awareness-only pass before the anchor teach. */
|
|
183
|
+
preview: 0.25,
|
|
184
|
+
ulo: 0.35,
|
|
185
|
+
cio: 0.65,
|
|
186
|
+
sio: 1,
|
|
187
|
+
/** An advanced revisit re-teaches the concept, not the whole thing again. */
|
|
188
|
+
advancedRevisit: 0.5,
|
|
189
|
+
/** Minutes saved when an advanced revisit drops from SIO practice to a CIO recap. */
|
|
190
|
+
advancedRecapSave: 0.3,
|
|
191
|
+
/** Floor so a tiny concept still gets a teachable awareness pass. */
|
|
192
|
+
floor: 5,
|
|
193
|
+
/** Floor for an advanced revisit — it is a real teach, not an awareness pass. */
|
|
194
|
+
advancedFloor: 15
|
|
195
|
+
};
|
|
196
|
+
var DEFAULT_STEP_MINUTES = 15;
|
|
139
197
|
var BLOOM_RANK = { Remember: 0, Understand: 1, Apply: 2, Analyze: 3, Evaluate: 4, Create: 5 };
|
|
140
198
|
var pad2 = (n) => String(n).padStart(2, "0");
|
|
199
|
+
var steplessStepIds = [];
|
|
200
|
+
var takeSteplessStepIds = () => steplessStepIds.splice(0, steplessStepIds.length);
|
|
141
201
|
var arr = (v) => Array.isArray(v) ? v : [];
|
|
142
202
|
var str = (v, d = "") => typeof v === "string" ? v : d;
|
|
143
203
|
var num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
|
|
144
204
|
var strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
145
205
|
function stepMinutes(step) {
|
|
146
206
|
const effort = step.effort ?? {};
|
|
147
|
-
return Math.max(0, Math.round(num(effort.estimated_minutes, num(step.estimated_minutes,
|
|
207
|
+
return Math.max(0, Math.round(num(effort.estimated_minutes, num(step.estimated_minutes, DEFAULT_STEP_MINUTES))));
|
|
148
208
|
}
|
|
149
|
-
function
|
|
150
|
-
|
|
209
|
+
function diagnosticsOf(opts) {
|
|
210
|
+
const list = opts.diagnostics ?? (opts.diagnostics = []);
|
|
211
|
+
for (const message of opts.warnings ?? []) {
|
|
212
|
+
if (!list.some((d) => d.code === "upstream_warning" && d.message === message)) {
|
|
213
|
+
list.push({ code: "upstream_warning", severity: "warning", message, node_ids: [] });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return list;
|
|
217
|
+
}
|
|
218
|
+
function pushDiagnostic(opts, code, severity, message, nodeIds = []) {
|
|
219
|
+
diagnosticsOf(opts).push({ code, severity, message, node_ids: nodeIds });
|
|
220
|
+
}
|
|
221
|
+
function splitPrerequisites(concept, known) {
|
|
222
|
+
const internal = [];
|
|
223
|
+
const external = [];
|
|
224
|
+
for (const p of strs(concept.prerequisites)) {
|
|
225
|
+
(known.has(p) ? internal : external).push(p);
|
|
226
|
+
}
|
|
227
|
+
return { internal, external };
|
|
228
|
+
}
|
|
229
|
+
function noteExternalPrerequisites(opts, byNode) {
|
|
230
|
+
if (byNode.size === 0) return;
|
|
231
|
+
const entries = [];
|
|
232
|
+
for (const [nodeId, prereqs] of byNode) {
|
|
233
|
+
for (const prereqId of prereqs) entries.push({ nodeId, prereqId });
|
|
234
|
+
}
|
|
235
|
+
entries.sort((a, b) => a.nodeId === b.nodeId ? a.prereqId.localeCompare(b.prereqId) : a.nodeId.localeCompare(b.nodeId));
|
|
236
|
+
pushDiagnostic(
|
|
237
|
+
opts,
|
|
238
|
+
"external_prerequisite",
|
|
239
|
+
"info",
|
|
240
|
+
"Prerequisite(s) outside this analysis \u2014 the learner must already know them: " + entries.map((e) => e.nodeId + " \u2190 " + e.prereqId).join(", "),
|
|
241
|
+
entries.map((e) => e.nodeId)
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
function buildSource(opts, graphType) {
|
|
245
|
+
const diagnostics = diagnosticsOf(opts);
|
|
246
|
+
return {
|
|
247
|
+
graph_type: graphType,
|
|
248
|
+
// Derived view — 'info' diagnostics are deliberately not surfaced as warnings.
|
|
249
|
+
warnings: diagnostics.filter((d) => d.severity !== "info").map((d) => d.message),
|
|
250
|
+
hallucination_count: opts.hallucinationCount ?? 0,
|
|
251
|
+
diagnostics,
|
|
252
|
+
provenance: opts.provenance
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function keywordKey(k) {
|
|
256
|
+
return k.normalize("NFC").toLowerCase();
|
|
151
257
|
}
|
|
152
258
|
function newKeywordsInOrder(nodes) {
|
|
153
259
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -156,9 +262,9 @@ function newKeywordsInOrder(nodes) {
|
|
|
156
262
|
n.keywords.new = [];
|
|
157
263
|
n.keywords.prerequisite = [];
|
|
158
264
|
for (const k of all) {
|
|
159
|
-
if (seen.has(k)) n.keywords.prerequisite.push(k);
|
|
265
|
+
if (seen.has(keywordKey(k))) n.keywords.prerequisite.push(k);
|
|
160
266
|
else n.keywords.new.push(k);
|
|
161
|
-
seen.add(k);
|
|
267
|
+
seen.add(keywordKey(k));
|
|
162
268
|
}
|
|
163
269
|
}
|
|
164
270
|
}
|
|
@@ -182,6 +288,7 @@ function feedNodeFromStep(step, featureId) {
|
|
|
182
288
|
// sessions at user-visible checkpoints instead of raw minute counts.
|
|
183
289
|
user_visible_deliverable: str(step.outcome?.user_visible),
|
|
184
290
|
is_core: false,
|
|
291
|
+
external_prerequisites: [],
|
|
185
292
|
phase_id: "",
|
|
186
293
|
introduce_aspect: null
|
|
187
294
|
};
|
|
@@ -201,9 +308,9 @@ function deriveConceptDepthHint(concept) {
|
|
|
201
308
|
return "ulo";
|
|
202
309
|
}
|
|
203
310
|
function deriveDepthInputs(minutes, depth) {
|
|
204
|
-
const ulo = Math.max(
|
|
205
|
-
const cio = Math.max(ulo +
|
|
206
|
-
const sio = Math.max(cio +
|
|
311
|
+
const ulo = Math.max(DEPTH_MINUTE_FRACTIONS.floor, Math.round(minutes * DEPTH_MINUTE_FRACTIONS.ulo));
|
|
312
|
+
const cio = Math.max(ulo + DEPTH_MINUTE_FRACTIONS.floor, Math.round(minutes * DEPTH_MINUTE_FRACTIONS.cio));
|
|
313
|
+
const sio = Math.max(cio + DEPTH_MINUTE_FRACTIONS.floor, minutes);
|
|
207
314
|
const variants = { ulo, cio, sio };
|
|
208
315
|
const candidates = [];
|
|
209
316
|
if (depth === "sio") {
|
|
@@ -246,7 +353,16 @@ function feedNodeFromConcept(concept) {
|
|
|
246
353
|
user_visible_deliverable: "",
|
|
247
354
|
// concepts are taught, not shipped — checkpoints live on product steps
|
|
248
355
|
...deriveDepthInputs(Math.max(0, Math.round(num(concept.estimated_minutes, 15))), deriveConceptDepthHint(concept)),
|
|
249
|
-
|
|
356
|
+
// Knowledge-Tree concepts are the spine of the course — the reconciler must
|
|
357
|
+
// escalate them, never downgrade them. The emitter hardcoded `false`, so the
|
|
358
|
+
// flag the schema documents never reached the planner for any concept.
|
|
359
|
+
is_core: concept.is_core === true || str(concept.standardization) === "standardized",
|
|
360
|
+
external_prerequisites: [],
|
|
361
|
+
depth_context: {
|
|
362
|
+
ulo: str(concept.ulo),
|
|
363
|
+
cio: str(concept.cio),
|
|
364
|
+
sio: strs(concept.sio)
|
|
365
|
+
},
|
|
250
366
|
phase_id: "",
|
|
251
367
|
introduce_aspect: null
|
|
252
368
|
};
|
|
@@ -257,7 +373,10 @@ function addFeatureGroup(f, fid, nodes, edges, groupings) {
|
|
|
257
373
|
let prev = "";
|
|
258
374
|
for (let i = 0; i < steps.length; i++) {
|
|
259
375
|
const stepNode = feedNodeFromStep(steps[i], fid);
|
|
260
|
-
if (!stepNode.id)
|
|
376
|
+
if (!stepNode.id) {
|
|
377
|
+
steplessStepIds.push(fid + "#" + i);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
261
380
|
stepIds.push(stepNode.id);
|
|
262
381
|
nodes.push(stepNode);
|
|
263
382
|
if (prev) edges.push({ from: prev, to: stepNode.id, kind: "task", reason: "feature build order" });
|
|
@@ -280,18 +399,26 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
280
399
|
const features = arr(pg.features);
|
|
281
400
|
const conceptRecs = arr(kg.concepts);
|
|
282
401
|
const conceptById = new Map(conceptRecs.map((c) => [str(c.id), c]));
|
|
402
|
+
const knownConceptIds = new Set(conceptById.keys());
|
|
283
403
|
if (rawPhases.length === 0) {
|
|
404
|
+
const externalByNode2 = /* @__PURE__ */ new Map();
|
|
284
405
|
for (const c of conceptRecs) {
|
|
285
406
|
const node = feedNodeFromConcept(c);
|
|
286
407
|
nodes.push(node);
|
|
287
|
-
|
|
408
|
+
const { internal, external } = splitPrerequisites(c, knownConceptIds);
|
|
409
|
+
if (external.length > 0) {
|
|
410
|
+
node.external_prerequisites = external;
|
|
411
|
+
externalByNode2.set(node.id, external);
|
|
412
|
+
}
|
|
413
|
+
for (const p of internal) edges.push({ from: p, to: node.id, kind: "knowledge", reason: "concept prerequisite" });
|
|
288
414
|
}
|
|
415
|
+
noteExternalPrerequisites(opts, externalByNode2);
|
|
289
416
|
features.forEach((f, fi) => addFeatureGroup(f, str(f.id, "F" + pad2(fi + 1)), nodes, edges, groupings));
|
|
290
417
|
linkHybridKnowledgeEdges(hybrid, nodes, edges);
|
|
291
418
|
newKeywordsInOrder(nodes);
|
|
292
419
|
return validateCurriculumFeed({
|
|
293
420
|
schema_version: 2,
|
|
294
|
-
source:
|
|
421
|
+
source: buildSource(opts, "hybrid_graph"),
|
|
295
422
|
learning_nodes: nodes,
|
|
296
423
|
dependency_edges: edges,
|
|
297
424
|
suggested_groupings: groupings,
|
|
@@ -308,6 +435,8 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
308
435
|
const emitCtx = [];
|
|
309
436
|
const emittedConcepts = /* @__PURE__ */ new Set();
|
|
310
437
|
const previewsEmitted = /* @__PURE__ */ new Set();
|
|
438
|
+
const previewNodeIdByConcept = /* @__PURE__ */ new Map();
|
|
439
|
+
const externalByNode = /* @__PURE__ */ new Map();
|
|
311
440
|
const linkList = arr(hybrid.links);
|
|
312
441
|
for (const rp of rawPhases) {
|
|
313
442
|
const pid = str(rp.id);
|
|
@@ -347,7 +476,7 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
347
476
|
prevNode.name = str(c.name, cid) + " (preview)";
|
|
348
477
|
prevNode.phase_id = pid;
|
|
349
478
|
prevNode.introduce_aspect = "preview";
|
|
350
|
-
prevNode.estimated_minutes = Math.max(
|
|
479
|
+
prevNode.estimated_minutes = Math.max(DEPTH_MINUTE_FRACTIONS.floor, Math.round(prevNode.estimated_minutes * DEPTH_MINUTE_FRACTIONS.preview));
|
|
351
480
|
prevNode.depth_hint = "ulo";
|
|
352
481
|
prevNode.bloom_hint = "Understand";
|
|
353
482
|
prevNode.concept_codes = [str(c.code) || cid];
|
|
@@ -357,6 +486,7 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
357
486
|
prevNode.depth_scaffold_candidates = [];
|
|
358
487
|
nodes.push(prevNode);
|
|
359
488
|
conceptOrder.push(prevNode.id);
|
|
489
|
+
previewNodeIdByConcept.set(cid, prevNode.id);
|
|
360
490
|
continue;
|
|
361
491
|
}
|
|
362
492
|
if (!emittedConcepts.has(cid)) {
|
|
@@ -365,13 +495,22 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
365
495
|
node.introduce_aspect = "new";
|
|
366
496
|
nodes.push(node);
|
|
367
497
|
for (const p of strs(c.prerequisites)) {
|
|
368
|
-
if (!
|
|
369
|
-
|
|
498
|
+
if (!conceptById.has(p)) {
|
|
499
|
+
if (!externalByNode.has(cid)) externalByNode.set(cid, []);
|
|
500
|
+
externalByNode.get(cid).push(p);
|
|
501
|
+
node.external_prerequisites.push(p);
|
|
502
|
+
continue;
|
|
370
503
|
}
|
|
371
|
-
|
|
504
|
+
const previewOfPrereq = previewNodeIdByConcept.get(p);
|
|
505
|
+
if (!emittedConcepts.has(p) && !previewOfPrereq) {
|
|
506
|
+
throw new Error("Concept " + cid + " (phase " + pid + ") has prerequisite " + p + " that is neither introduced nor previewed in an EARLIER phase - fail-closed");
|
|
507
|
+
}
|
|
508
|
+
edges.push({ from: emittedConcepts.has(p) ? p : previewOfPrereq, to: cid, kind: "knowledge", reason: "concept prerequisite" });
|
|
372
509
|
}
|
|
373
510
|
emittedConcepts.add(cid);
|
|
374
511
|
conceptOrder.push(cid);
|
|
512
|
+
} else if (str(intro.aspect, "new") === "new") {
|
|
513
|
+
throw new Error("Phase " + pid + " introduces " + cid + " as new but it was already introduced in an earlier phase - fail-closed");
|
|
375
514
|
} else {
|
|
376
515
|
const revisitId = cid + "__ADV" + pad2(order);
|
|
377
516
|
const node = feedNodeFromConcept(c);
|
|
@@ -379,8 +518,16 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
379
518
|
node.name = str(c.name, cid) + " (advanced)";
|
|
380
519
|
node.phase_id = pid;
|
|
381
520
|
node.introduce_aspect = "advanced";
|
|
382
|
-
node.
|
|
383
|
-
|
|
521
|
+
node.estimated_minutes = Math.max(
|
|
522
|
+
DEPTH_MINUTE_FRACTIONS.advancedFloor,
|
|
523
|
+
Math.round(node.estimated_minutes * DEPTH_MINUTE_FRACTIONS.advancedRevisit)
|
|
524
|
+
);
|
|
525
|
+
node.depth_scaffold_candidates = [{
|
|
526
|
+
from_depth: "sio",
|
|
527
|
+
to_depth: "cio",
|
|
528
|
+
minutes_saved: Math.round(node.estimated_minutes * DEPTH_MINUTE_FRACTIONS.advancedRecapSave),
|
|
529
|
+
reason: "advanced revisit may drop to CIO recap without breaking first-teach chain"
|
|
530
|
+
}];
|
|
384
531
|
node.concept_codes = [str(c.code) || cid];
|
|
385
532
|
edges.push({ from: cid, to: revisitId, kind: "knowledge", reason: "advanced revisit of " + cid + " at phase " + pid });
|
|
386
533
|
nodes.push(node);
|
|
@@ -390,6 +537,7 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
390
537
|
const featureIds = strs(rp.feature_ids).filter((fid) => featureOrder.has(fid)).sort((a, b) => featureOrder.get(a) - featureOrder.get(b));
|
|
391
538
|
emitCtx.push({ id: pid, order, name: str(rp.name, pid), productCompletion: str(rp.product_completion), conceptOrder, featureIds });
|
|
392
539
|
}
|
|
540
|
+
noteExternalPrerequisites(opts, externalByNode);
|
|
393
541
|
for (const l of linkList) {
|
|
394
542
|
const cid = str(l.concept_id);
|
|
395
543
|
if (cid && conceptById.has(cid) && !emittedConcepts.has(cid)) {
|
|
@@ -401,6 +549,16 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
401
549
|
if (missingFeatures.length > 0) {
|
|
402
550
|
throw new Error("Features not covered by any phase: " + missingFeatures.join(", ") + " - fail-closed");
|
|
403
551
|
}
|
|
552
|
+
const orphans = Array.from(conceptById.keys()).filter((cid) => !emittedConcepts.has(cid));
|
|
553
|
+
if (orphans.length > 0) {
|
|
554
|
+
pushDiagnostic(
|
|
555
|
+
opts,
|
|
556
|
+
"orphan_concept",
|
|
557
|
+
"error",
|
|
558
|
+
"Knowledge graph has " + orphans.length + " concept(s) no phase introduces and no link consumes \u2014 they will not be taught: " + orphans.join(", "),
|
|
559
|
+
orphans
|
|
560
|
+
);
|
|
561
|
+
}
|
|
404
562
|
const phaseNodeIds = [];
|
|
405
563
|
for (const ctx of emitCtx) {
|
|
406
564
|
const ids = [...ctx.conceptOrder];
|
|
@@ -408,13 +566,26 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
408
566
|
const f = features.find((x) => str(x.id) === fid);
|
|
409
567
|
const stepIds = addFeatureGroup(f, fid, nodes, edges, groupings);
|
|
410
568
|
for (const sid of stepIds) {
|
|
411
|
-
const node = nodes.find((
|
|
569
|
+
const node = nodes.find((x) => x.id === sid);
|
|
412
570
|
node.phase_id = ctx.id;
|
|
413
571
|
}
|
|
414
572
|
ids.push(...stepIds);
|
|
415
573
|
}
|
|
574
|
+
if (ids.length === 0) {
|
|
575
|
+
throw new Error("Phase " + ctx.id + " contains no teaching nodes (no concepts introduced and no feature steps) - drop it or give it work - fail-closed");
|
|
576
|
+
}
|
|
416
577
|
phaseNodeIds.push(ids);
|
|
417
578
|
}
|
|
579
|
+
const idlessInHybrid = takeSteplessStepIds();
|
|
580
|
+
if (idlessInHybrid.length > 0) {
|
|
581
|
+
pushDiagnostic(
|
|
582
|
+
opts,
|
|
583
|
+
"step_without_id",
|
|
584
|
+
"warning",
|
|
585
|
+
"Step(s) carrying no id cannot become planning nodes and were dropped (feature#index): " + idlessInHybrid.join(", "),
|
|
586
|
+
idlessInHybrid
|
|
587
|
+
);
|
|
588
|
+
}
|
|
418
589
|
const placed = new Set(phaseNodeIds.flat());
|
|
419
590
|
const orderedNodes = [];
|
|
420
591
|
for (const ids of phaseNodeIds) {
|
|
@@ -445,7 +616,13 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
445
616
|
const anchorPhaseIdx = anchorNode ? emitCtx.findIndex((c2) => c2.id === anchorNode.phase_id) : -1;
|
|
446
617
|
const stepPhaseIdx = stepNode ? emitCtx.findIndex((c2) => c2.id === stepNode.phase_id) : -1;
|
|
447
618
|
if (emitCtx.length > 0 && anchorPhaseIdx > stepPhaseIdx) {
|
|
448
|
-
|
|
619
|
+
pushDiagnostic(
|
|
620
|
+
opts,
|
|
621
|
+
"hybrid_link_drift",
|
|
622
|
+
"warning",
|
|
623
|
+
"hybrid link drift: concept " + cid + " is fully taught in a later phase than consumer " + firstStep + " (a preview covers the dependency)",
|
|
624
|
+
[cid, firstStep]
|
|
625
|
+
);
|
|
449
626
|
continue;
|
|
450
627
|
}
|
|
451
628
|
pushEdge(cid, firstStep, str(l.relationship, "hybrid link") + " [" + depth + "]");
|
|
@@ -459,12 +636,12 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
459
636
|
const feedOrder = new Map(nodes.map((n, i) => [n.id, i]));
|
|
460
637
|
const introducer = /* @__PURE__ */ new Map();
|
|
461
638
|
for (const n of nodes) for (const k of n.keywords?.new ?? []) {
|
|
462
|
-
const key = k
|
|
639
|
+
const key = keywordKey(k);
|
|
463
640
|
if (!introducer.has(key)) introducer.set(key, n.id);
|
|
464
641
|
}
|
|
465
642
|
for (const n of nodes) {
|
|
466
643
|
for (const k of n.keywords?.prerequisite ?? []) {
|
|
467
|
-
const src = introducer.get(k
|
|
644
|
+
const src = introducer.get(keywordKey(k));
|
|
468
645
|
if (!src || src === n.id) continue;
|
|
469
646
|
const srcNode = nodes.find((x) => x.id === src);
|
|
470
647
|
const phaseOf = (id) => {
|
|
@@ -475,9 +652,12 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
475
652
|
const fromPhase = phaseOf(src);
|
|
476
653
|
const toPhase = phaseOf(n.id);
|
|
477
654
|
if (conceptOvertaken || emitCtx.length > 0 && fromPhase > toPhase) {
|
|
478
|
-
|
|
655
|
+
pushDiagnostic(
|
|
479
656
|
opts,
|
|
480
|
-
|
|
657
|
+
conceptOvertaken ? "concept_overtaken_by_step" : "keyword_order_drift",
|
|
658
|
+
"warning",
|
|
659
|
+
"keyword-order drift: introducer " + src + " sits in a later phase than consumer " + n.id + ' (keyword "' + k + '") \u2014 host should move the introducer earlier',
|
|
660
|
+
[src, n.id]
|
|
481
661
|
);
|
|
482
662
|
continue;
|
|
483
663
|
}
|
|
@@ -493,7 +673,7 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
493
673
|
}));
|
|
494
674
|
return validateCurriculumFeed({
|
|
495
675
|
schema_version: 2,
|
|
496
|
-
source:
|
|
676
|
+
source: buildSource(opts, "hybrid_graph"),
|
|
497
677
|
learning_nodes: nodes,
|
|
498
678
|
dependency_edges: edges,
|
|
499
679
|
suggested_groupings: groupings,
|
|
@@ -521,11 +701,17 @@ function emitFromKnowledge(kg, opts) {
|
|
|
521
701
|
const edges = [];
|
|
522
702
|
const groupings = [];
|
|
523
703
|
const conceptIds = new Set(arr(kg.concepts).map((c) => str(c.id)));
|
|
704
|
+
const externalByNode = /* @__PURE__ */ new Map();
|
|
524
705
|
for (const c of arr(kg.concepts)) {
|
|
525
706
|
const node = feedNodeFromConcept(c);
|
|
526
707
|
nodes.push(node);
|
|
527
|
-
|
|
528
|
-
|
|
708
|
+
const { internal, external } = splitPrerequisites(c, conceptIds);
|
|
709
|
+
if (external.length > 0) {
|
|
710
|
+
node.external_prerequisites = external;
|
|
711
|
+
externalByNode.set(node.id, external);
|
|
712
|
+
}
|
|
713
|
+
for (const p of internal) {
|
|
714
|
+
if (p !== node.id) {
|
|
529
715
|
edges.push({
|
|
530
716
|
from: p,
|
|
531
717
|
to: node.id,
|
|
@@ -535,6 +721,7 @@ function emitFromKnowledge(kg, opts) {
|
|
|
535
721
|
}
|
|
536
722
|
}
|
|
537
723
|
}
|
|
724
|
+
noteExternalPrerequisites(opts, externalByNode);
|
|
538
725
|
if (arr(kg.learning_path).length > 0) {
|
|
539
726
|
const orderMap = /* @__PURE__ */ new Map();
|
|
540
727
|
arr(kg.learning_path).forEach((s, idx) => {
|
|
@@ -559,7 +746,7 @@ function emitFromKnowledge(kg, opts) {
|
|
|
559
746
|
newKeywordsInOrder(nodes);
|
|
560
747
|
return validateCurriculumFeed({
|
|
561
748
|
schema_version: 2,
|
|
562
|
-
source:
|
|
749
|
+
source: buildSource(opts, "knowledge_graph"),
|
|
563
750
|
learning_nodes: nodes,
|
|
564
751
|
dependency_edges: edges,
|
|
565
752
|
suggested_groupings: groupings,
|
|
@@ -570,17 +757,38 @@ function emitFromProject(pg, opts) {
|
|
|
570
757
|
const nodes = [];
|
|
571
758
|
const edges = [];
|
|
572
759
|
const groupings = [];
|
|
573
|
-
arr(pg.features)
|
|
574
|
-
|
|
760
|
+
const allFeatures = arr(pg.features);
|
|
761
|
+
allFeatures.forEach((f, fi) => addFeatureGroup(f, str(f.id, "F" + pad2(fi + 1)), nodes, edges, groupings));
|
|
762
|
+
const stepless = allFeatures.filter((f) => arr(f.steps).length === 0).map((f) => str(f.id)).filter(Boolean);
|
|
763
|
+
if (stepless.length > 0) {
|
|
764
|
+
pushDiagnostic(
|
|
765
|
+
opts,
|
|
766
|
+
"feature_without_steps",
|
|
767
|
+
"warning",
|
|
768
|
+
"Feature(s) declared but carrying no steps \u2014 they will be absent from the feed: " + stepless.join(", "),
|
|
769
|
+
stepless
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
const idless = takeSteplessStepIds();
|
|
773
|
+
if (idless.length > 0) {
|
|
774
|
+
pushDiagnostic(
|
|
775
|
+
opts,
|
|
776
|
+
"step_without_id",
|
|
777
|
+
"warning",
|
|
778
|
+
"Step(s) carrying no id cannot become planning nodes and were dropped (feature#index): " + idless.join(", "),
|
|
779
|
+
idless
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
if (nodes.length === 0 && allFeatures.length > 0) {
|
|
575
783
|
const hasTasks = arr(pg.implementation ?? {}).length > 0 || arr(pg.implementation?.tasks).length > 0;
|
|
576
784
|
throw new Error(
|
|
577
|
-
"Project graph has " +
|
|
785
|
+
"Project graph has " + allFeatures.length + " features but NO features[].steps - the curriculum feed cannot emit planning nodes." + (hasTasks ? " This looks like a legacy knowledge-tree graph (implementation.tasks). Regenerate it with the domain-kit v3 pipeline (runProjectGraphPipeline / hybridGraphPipeline), then re-run." : " Every feature must carry at least one step with id/name/keywords/effort for curriculum planning.")
|
|
578
786
|
);
|
|
579
787
|
}
|
|
580
788
|
newKeywordsInOrder(nodes);
|
|
581
789
|
return validateCurriculumFeed({
|
|
582
790
|
schema_version: 2,
|
|
583
|
-
source:
|
|
791
|
+
source: buildSource(opts, "project_graph"),
|
|
584
792
|
learning_nodes: nodes,
|
|
585
793
|
dependency_edges: edges,
|
|
586
794
|
suggested_groupings: groupings,
|
|
@@ -599,6 +807,8 @@ function emitCurriculumFeed(graph, opts = {}) {
|
|
|
599
807
|
return emitFromProject(g, opts);
|
|
600
808
|
}
|
|
601
809
|
|
|
810
|
+
exports.DEFAULT_STEP_MINUTES = DEFAULT_STEP_MINUTES;
|
|
811
|
+
exports.DEPTH_MINUTE_FRACTIONS = DEPTH_MINUTE_FRACTIONS;
|
|
602
812
|
exports.emitCurriculumFeed = emitCurriculumFeed;
|
|
603
813
|
//# sourceMappingURL=index.cjs.map
|
|
604
814
|
//# sourceMappingURL=index.cjs.map
|