@thanh01.pmt/domain-kit 0.5.0 → 0.6.1
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 +3 -2
- package/dist/assembly/index.d.ts +3 -2
- package/dist/{chunk-TYXSJUQU.mjs → chunk-4UCXYRM3.mjs} +208 -310
- package/dist/chunk-4UCXYRM3.mjs.map +1 -0
- package/dist/chunk-7A2SG466.mjs +325 -0
- package/dist/chunk-7A2SG466.mjs.map +1 -0
- package/dist/{chunk-E4GCCWJO.mjs → chunk-CGNXGVHV.mjs} +37 -4
- package/dist/chunk-CGNXGVHV.mjs.map +1 -0
- package/dist/{chunk-DRVOV5ZD.mjs → chunk-DSEJLTUO.mjs} +2 -2
- package/dist/chunk-DSEJLTUO.mjs.map +1 -0
- package/dist/{chunk-VCPIWJ7R.mjs → chunk-H4W3YJLK.mjs} +29 -4
- package/dist/chunk-H4W3YJLK.mjs.map +1 -0
- package/dist/chunk-IBVXXVGE.mjs +14 -0
- package/dist/chunk-IBVXXVGE.mjs.map +1 -0
- package/dist/{chunk-G7HLFMD2.mjs → chunk-KHCZYLKM.mjs} +68 -33
- package/dist/chunk-KHCZYLKM.mjs.map +1 -0
- package/dist/chunk-OYSZQVPY.mjs +10 -0
- package/dist/chunk-OYSZQVPY.mjs.map +1 -0
- package/dist/{chunk-BL5NBKPH.mjs → chunk-WD3P5V3B.mjs} +19 -8
- package/dist/chunk-WD3P5V3B.mjs.map +1 -0
- package/dist/{chunk-TWKIVUTQ.mjs → chunk-WKTH3JPA.mjs} +197 -36
- package/dist/chunk-WKTH3JPA.mjs.map +1 -0
- package/dist/{conceptEscalator-ltRLtPhf.d.cts → conceptEscalator-BmMJD8C5.d.cts} +10 -1
- package/dist/{conceptEscalator-YfMKIKCZ.d.ts → conceptEscalator-DMsehscs.d.ts} +10 -1
- package/dist/concepts/index.d.cts +4 -3
- package/dist/concepts/index.d.ts +4 -3
- package/dist/{cpp-CZUPqJt9.d.cts → cpp-vlDl6pCL.d.cts} +4 -4
- package/dist/{cpp-CZUPqJt9.d.ts → cpp-vlDl6pCL.d.ts} +4 -4
- package/dist/{curriculumFeedSchema-DUFwFyr2.d.ts → curriculumFeedSchema-DK-TUAJZ.d.cts} +164 -9
- package/dist/{curriculumFeedSchema-DUFwFyr2.d.cts → curriculumFeedSchema-DK-TUAJZ.d.ts} +164 -9
- package/dist/detector/index.cjs +22 -6
- package/dist/detector/index.cjs.map +1 -1
- package/dist/detector/index.d.cts +2 -2
- package/dist/detector/index.d.ts +2 -2
- package/dist/detector/index.mjs +2 -1
- package/dist/{domainProfileDetector-DHb1T0zB.d.ts → domainProfileDetector-6pjgL-6_.d.ts} +1 -1
- package/dist/{domainProfileDetector-bzugdQmS.d.cts → domainProfileDetector-BsADsuRV.d.cts} +1 -1
- package/dist/{domainProfileSchema-CwT3Ffsw.d.ts → domainProfileSchema-779FGk14.d.cts} +38 -1
- package/dist/{domainProfileSchema-CwT3Ffsw.d.cts → domainProfileSchema-779FGk14.d.ts} +38 -1
- package/dist/extractors/index.d.cts +3 -3
- package/dist/extractors/index.d.ts +3 -3
- package/dist/feed/index.cjs +229 -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 +308 -86
- package/dist/graph/index.cjs.map +1 -1
- package/dist/graph/index.d.cts +20 -8
- package/dist/graph/index.d.ts +20 -8
- package/dist/graph/index.mjs +4 -1
- package/dist/{hybridGraphPipeline-dT-bio-3.d.cts → hybridGraphPipeline-B4mamLim.d.cts} +6 -2
- package/dist/{hybridGraphPipeline-DXQDbBEj.d.ts → hybridGraphPipeline-D2_1GUva.d.ts} +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 +3948 -3464
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -10
- package/dist/index.d.ts +11 -10
- package/dist/index.mjs +14 -11
- package/dist/{keywordExtractor-BxlGRMHC.d.cts → keywordExtractor-1YAFntsn.d.cts} +1 -1
- package/dist/{keywordExtractor-DU5XRN8-.d.ts → keywordExtractor-DWw8z_AF.d.ts} +1 -1
- package/dist/parsers/index.cjs.map +1 -1
- package/dist/parsers/index.d.cts +1 -1
- package/dist/parsers/index.d.ts +1 -1
- package/dist/parsers/index.mjs +1 -1
- package/dist/pipeline/index.cjs +2436 -1994
- package/dist/pipeline/index.cjs.map +1 -1
- package/dist/pipeline/index.d.cts +19 -10
- package/dist/pipeline/index.d.ts +19 -10
- package/dist/pipeline/index.mjs +11 -8
- package/dist/schemas/index.cjs +63 -2
- package/dist/schemas/index.cjs.map +1 -1
- package/dist/schemas/index.d.cts +3 -3
- package/dist/schemas/index.d.ts +3 -3
- package/dist/schemas/index.mjs +2 -2
- package/dist/utils/llmClient.cjs +344 -0
- package/dist/utils/llmClient.cjs.map +1 -0
- package/dist/utils/llmClient.d.cts +118 -0
- package/dist/utils/llmClient.d.ts +118 -0
- package/dist/utils/llmClient.mjs +4 -0
- package/dist/utils/llmClient.mjs.map +1 -0
- package/dist/utils/llmTransportResult.cjs +16 -0
- package/dist/utils/llmTransportResult.cjs.map +1 -0
- package/dist/utils/llmTransportResult.d.cts +36 -0
- package/dist/utils/llmTransportResult.d.ts +36 -0
- package/dist/utils/llmTransportResult.mjs +3 -0
- package/dist/utils/llmTransportResult.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-BL5NBKPH.mjs.map +0 -1
- package/dist/chunk-DRVOV5ZD.mjs.map +0 -1
- package/dist/chunk-E4GCCWJO.mjs.map +0 -1
- package/dist/chunk-G7HLFMD2.mjs.map +0 -1
- package/dist/chunk-TWKIVUTQ.mjs.map +0 -1
- package/dist/chunk-TYXSJUQU.mjs.map +0 -1
- package/dist/chunk-VCPIWJ7R.mjs.map +0 -1
- package/dist/curriculumFeedEmitter-T7NUBLAM.mjs +0 -4
- package/dist/curriculumFeedEmitter-T7NUBLAM.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,14 @@ 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([]),
|
|
55
63
|
// Depth layer text (optional, additive — old feeds validate unchanged).
|
|
56
64
|
// Carries the ULO/CIO/SIO authored text from the KnowledgeGraph concept
|
|
57
65
|
// so downstream consumers (KX generator) can inject depth-appropriate
|
|
@@ -87,12 +95,28 @@ var FeedPhaseSchema = zod.z.object({
|
|
|
87
95
|
product_completion: zod.z.string().min(1),
|
|
88
96
|
node_ids: zod.z.array(zod.z.string()).min(1)
|
|
89
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
|
+
});
|
|
90
111
|
var CurriculumFeedSchema = zod.z.object({
|
|
91
112
|
schema_version: zod.z.literal(2),
|
|
92
113
|
source: zod.z.object({
|
|
93
114
|
graph_type: zod.z.enum(["project_graph", "knowledge_graph", "hybrid_graph"]),
|
|
115
|
+
/** DERIVED from diagnostics — do not write to it directly. */
|
|
94
116
|
warnings: zod.z.array(zod.z.string()).default([]),
|
|
95
|
-
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()
|
|
96
120
|
}),
|
|
97
121
|
learning_nodes: zod.z.array(FeedNodeSchema).min(1, "curriculum feed must contain at least one learning node"),
|
|
98
122
|
dependency_edges: zod.z.array(FeedEdgeSchema).default([]),
|
|
@@ -125,6 +149,15 @@ function validateCurriculumFeed(feed) {
|
|
|
125
149
|
}
|
|
126
150
|
const phaseIds = parsed.phases.map((p) => p.id);
|
|
127
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
|
+
}
|
|
128
161
|
if (parsed.phases.length > 0) {
|
|
129
162
|
const phaseIndexOf = /* @__PURE__ */ new Map();
|
|
130
163
|
parsed.phases.forEach((p, i) => {
|
|
@@ -145,18 +178,82 @@ function validateCurriculumFeed(feed) {
|
|
|
145
178
|
}
|
|
146
179
|
|
|
147
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;
|
|
148
197
|
var BLOOM_RANK = { Remember: 0, Understand: 1, Apply: 2, Analyze: 3, Evaluate: 4, Create: 5 };
|
|
149
198
|
var pad2 = (n) => String(n).padStart(2, "0");
|
|
199
|
+
var steplessStepIds = [];
|
|
200
|
+
var takeSteplessStepIds = () => steplessStepIds.splice(0, steplessStepIds.length);
|
|
150
201
|
var arr = (v) => Array.isArray(v) ? v : [];
|
|
151
202
|
var str = (v, d = "") => typeof v === "string" ? v : d;
|
|
152
203
|
var num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
|
|
153
204
|
var strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
154
205
|
function stepMinutes(step) {
|
|
155
206
|
const effort = step.effort ?? {};
|
|
156
|
-
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))));
|
|
208
|
+
}
|
|
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
|
+
};
|
|
157
254
|
}
|
|
158
|
-
function
|
|
159
|
-
|
|
255
|
+
function keywordKey(k) {
|
|
256
|
+
return k.normalize("NFC").toLowerCase();
|
|
160
257
|
}
|
|
161
258
|
function newKeywordsInOrder(nodes) {
|
|
162
259
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -165,9 +262,9 @@ function newKeywordsInOrder(nodes) {
|
|
|
165
262
|
n.keywords.new = [];
|
|
166
263
|
n.keywords.prerequisite = [];
|
|
167
264
|
for (const k of all) {
|
|
168
|
-
if (seen.has(k)) n.keywords.prerequisite.push(k);
|
|
265
|
+
if (seen.has(keywordKey(k))) n.keywords.prerequisite.push(k);
|
|
169
266
|
else n.keywords.new.push(k);
|
|
170
|
-
seen.add(k);
|
|
267
|
+
seen.add(keywordKey(k));
|
|
171
268
|
}
|
|
172
269
|
}
|
|
173
270
|
}
|
|
@@ -191,6 +288,7 @@ function feedNodeFromStep(step, featureId) {
|
|
|
191
288
|
// sessions at user-visible checkpoints instead of raw minute counts.
|
|
192
289
|
user_visible_deliverable: str(step.outcome?.user_visible),
|
|
193
290
|
is_core: false,
|
|
291
|
+
external_prerequisites: [],
|
|
194
292
|
phase_id: "",
|
|
195
293
|
introduce_aspect: null
|
|
196
294
|
};
|
|
@@ -210,9 +308,9 @@ function deriveConceptDepthHint(concept) {
|
|
|
210
308
|
return "ulo";
|
|
211
309
|
}
|
|
212
310
|
function deriveDepthInputs(minutes, depth) {
|
|
213
|
-
const ulo = Math.max(
|
|
214
|
-
const cio = Math.max(ulo +
|
|
215
|
-
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);
|
|
216
314
|
const variants = { ulo, cio, sio };
|
|
217
315
|
const candidates = [];
|
|
218
316
|
if (depth === "sio") {
|
|
@@ -255,7 +353,11 @@ function feedNodeFromConcept(concept) {
|
|
|
255
353
|
user_visible_deliverable: "",
|
|
256
354
|
// concepts are taught, not shipped — checkpoints live on product steps
|
|
257
355
|
...deriveDepthInputs(Math.max(0, Math.round(num(concept.estimated_minutes, 15))), deriveConceptDepthHint(concept)),
|
|
258
|
-
|
|
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: [],
|
|
259
361
|
depth_context: {
|
|
260
362
|
ulo: str(concept.ulo),
|
|
261
363
|
cio: str(concept.cio),
|
|
@@ -271,7 +373,10 @@ function addFeatureGroup(f, fid, nodes, edges, groupings) {
|
|
|
271
373
|
let prev = "";
|
|
272
374
|
for (let i = 0; i < steps.length; i++) {
|
|
273
375
|
const stepNode = feedNodeFromStep(steps[i], fid);
|
|
274
|
-
if (!stepNode.id)
|
|
376
|
+
if (!stepNode.id) {
|
|
377
|
+
steplessStepIds.push(fid + "#" + i);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
275
380
|
stepIds.push(stepNode.id);
|
|
276
381
|
nodes.push(stepNode);
|
|
277
382
|
if (prev) edges.push({ from: prev, to: stepNode.id, kind: "task", reason: "feature build order" });
|
|
@@ -294,18 +399,26 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
294
399
|
const features = arr(pg.features);
|
|
295
400
|
const conceptRecs = arr(kg.concepts);
|
|
296
401
|
const conceptById = new Map(conceptRecs.map((c) => [str(c.id), c]));
|
|
402
|
+
const knownConceptIds = new Set(conceptById.keys());
|
|
297
403
|
if (rawPhases.length === 0) {
|
|
404
|
+
const externalByNode2 = /* @__PURE__ */ new Map();
|
|
298
405
|
for (const c of conceptRecs) {
|
|
299
406
|
const node = feedNodeFromConcept(c);
|
|
300
407
|
nodes.push(node);
|
|
301
|
-
|
|
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" });
|
|
302
414
|
}
|
|
415
|
+
noteExternalPrerequisites(opts, externalByNode2);
|
|
303
416
|
features.forEach((f, fi) => addFeatureGroup(f, str(f.id, "F" + pad2(fi + 1)), nodes, edges, groupings));
|
|
304
417
|
linkHybridKnowledgeEdges(hybrid, nodes, edges);
|
|
305
418
|
newKeywordsInOrder(nodes);
|
|
306
419
|
return validateCurriculumFeed({
|
|
307
420
|
schema_version: 2,
|
|
308
|
-
source:
|
|
421
|
+
source: buildSource(opts, "hybrid_graph"),
|
|
309
422
|
learning_nodes: nodes,
|
|
310
423
|
dependency_edges: edges,
|
|
311
424
|
suggested_groupings: groupings,
|
|
@@ -322,6 +435,8 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
322
435
|
const emitCtx = [];
|
|
323
436
|
const emittedConcepts = /* @__PURE__ */ new Set();
|
|
324
437
|
const previewsEmitted = /* @__PURE__ */ new Set();
|
|
438
|
+
const previewNodeIdByConcept = /* @__PURE__ */ new Map();
|
|
439
|
+
const externalByNode = /* @__PURE__ */ new Map();
|
|
325
440
|
const linkList = arr(hybrid.links);
|
|
326
441
|
for (const rp of rawPhases) {
|
|
327
442
|
const pid = str(rp.id);
|
|
@@ -361,7 +476,7 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
361
476
|
prevNode.name = str(c.name, cid) + " (preview)";
|
|
362
477
|
prevNode.phase_id = pid;
|
|
363
478
|
prevNode.introduce_aspect = "preview";
|
|
364
|
-
prevNode.estimated_minutes = Math.max(
|
|
479
|
+
prevNode.estimated_minutes = Math.max(DEPTH_MINUTE_FRACTIONS.floor, Math.round(prevNode.estimated_minutes * DEPTH_MINUTE_FRACTIONS.preview));
|
|
365
480
|
prevNode.depth_hint = "ulo";
|
|
366
481
|
prevNode.bloom_hint = "Understand";
|
|
367
482
|
prevNode.concept_codes = [str(c.code) || cid];
|
|
@@ -371,6 +486,7 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
371
486
|
prevNode.depth_scaffold_candidates = [];
|
|
372
487
|
nodes.push(prevNode);
|
|
373
488
|
conceptOrder.push(prevNode.id);
|
|
489
|
+
previewNodeIdByConcept.set(cid, prevNode.id);
|
|
374
490
|
continue;
|
|
375
491
|
}
|
|
376
492
|
if (!emittedConcepts.has(cid)) {
|
|
@@ -379,13 +495,22 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
379
495
|
node.introduce_aspect = "new";
|
|
380
496
|
nodes.push(node);
|
|
381
497
|
for (const p of strs(c.prerequisites)) {
|
|
382
|
-
if (!
|
|
383
|
-
|
|
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;
|
|
384
503
|
}
|
|
385
|
-
|
|
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" });
|
|
386
509
|
}
|
|
387
510
|
emittedConcepts.add(cid);
|
|
388
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");
|
|
389
514
|
} else {
|
|
390
515
|
const revisitId = cid + "__ADV" + pad2(order);
|
|
391
516
|
const node = feedNodeFromConcept(c);
|
|
@@ -393,8 +518,16 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
393
518
|
node.name = str(c.name, cid) + " (advanced)";
|
|
394
519
|
node.phase_id = pid;
|
|
395
520
|
node.introduce_aspect = "advanced";
|
|
396
|
-
node.
|
|
397
|
-
|
|
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
|
+
}];
|
|
398
531
|
node.concept_codes = [str(c.code) || cid];
|
|
399
532
|
edges.push({ from: cid, to: revisitId, kind: "knowledge", reason: "advanced revisit of " + cid + " at phase " + pid });
|
|
400
533
|
nodes.push(node);
|
|
@@ -404,6 +537,7 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
404
537
|
const featureIds = strs(rp.feature_ids).filter((fid) => featureOrder.has(fid)).sort((a, b) => featureOrder.get(a) - featureOrder.get(b));
|
|
405
538
|
emitCtx.push({ id: pid, order, name: str(rp.name, pid), productCompletion: str(rp.product_completion), conceptOrder, featureIds });
|
|
406
539
|
}
|
|
540
|
+
noteExternalPrerequisites(opts, externalByNode);
|
|
407
541
|
for (const l of linkList) {
|
|
408
542
|
const cid = str(l.concept_id);
|
|
409
543
|
if (cid && conceptById.has(cid) && !emittedConcepts.has(cid)) {
|
|
@@ -415,6 +549,16 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
415
549
|
if (missingFeatures.length > 0) {
|
|
416
550
|
throw new Error("Features not covered by any phase: " + missingFeatures.join(", ") + " - fail-closed");
|
|
417
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
|
+
}
|
|
418
562
|
const phaseNodeIds = [];
|
|
419
563
|
for (const ctx of emitCtx) {
|
|
420
564
|
const ids = [...ctx.conceptOrder];
|
|
@@ -422,13 +566,26 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
422
566
|
const f = features.find((x) => str(x.id) === fid);
|
|
423
567
|
const stepIds = addFeatureGroup(f, fid, nodes, edges, groupings);
|
|
424
568
|
for (const sid of stepIds) {
|
|
425
|
-
const node = nodes.find((
|
|
569
|
+
const node = nodes.find((x) => x.id === sid);
|
|
426
570
|
node.phase_id = ctx.id;
|
|
427
571
|
}
|
|
428
572
|
ids.push(...stepIds);
|
|
429
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
|
+
}
|
|
430
577
|
phaseNodeIds.push(ids);
|
|
431
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
|
+
}
|
|
432
589
|
const placed = new Set(phaseNodeIds.flat());
|
|
433
590
|
const orderedNodes = [];
|
|
434
591
|
for (const ids of phaseNodeIds) {
|
|
@@ -459,7 +616,13 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
459
616
|
const anchorPhaseIdx = anchorNode ? emitCtx.findIndex((c2) => c2.id === anchorNode.phase_id) : -1;
|
|
460
617
|
const stepPhaseIdx = stepNode ? emitCtx.findIndex((c2) => c2.id === stepNode.phase_id) : -1;
|
|
461
618
|
if (emitCtx.length > 0 && anchorPhaseIdx > stepPhaseIdx) {
|
|
462
|
-
|
|
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
|
+
);
|
|
463
626
|
continue;
|
|
464
627
|
}
|
|
465
628
|
pushEdge(cid, firstStep, str(l.relationship, "hybrid link") + " [" + depth + "]");
|
|
@@ -473,12 +636,12 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
473
636
|
const feedOrder = new Map(nodes.map((n, i) => [n.id, i]));
|
|
474
637
|
const introducer = /* @__PURE__ */ new Map();
|
|
475
638
|
for (const n of nodes) for (const k of n.keywords?.new ?? []) {
|
|
476
|
-
const key = k
|
|
639
|
+
const key = keywordKey(k);
|
|
477
640
|
if (!introducer.has(key)) introducer.set(key, n.id);
|
|
478
641
|
}
|
|
479
642
|
for (const n of nodes) {
|
|
480
643
|
for (const k of n.keywords?.prerequisite ?? []) {
|
|
481
|
-
const src = introducer.get(k
|
|
644
|
+
const src = introducer.get(keywordKey(k));
|
|
482
645
|
if (!src || src === n.id) continue;
|
|
483
646
|
const srcNode = nodes.find((x) => x.id === src);
|
|
484
647
|
const phaseOf = (id) => {
|
|
@@ -489,9 +652,12 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
489
652
|
const fromPhase = phaseOf(src);
|
|
490
653
|
const toPhase = phaseOf(n.id);
|
|
491
654
|
if (conceptOvertaken || emitCtx.length > 0 && fromPhase > toPhase) {
|
|
492
|
-
|
|
655
|
+
pushDiagnostic(
|
|
493
656
|
opts,
|
|
494
|
-
|
|
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]
|
|
495
661
|
);
|
|
496
662
|
continue;
|
|
497
663
|
}
|
|
@@ -507,7 +673,7 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
507
673
|
}));
|
|
508
674
|
return validateCurriculumFeed({
|
|
509
675
|
schema_version: 2,
|
|
510
|
-
source:
|
|
676
|
+
source: buildSource(opts, "hybrid_graph"),
|
|
511
677
|
learning_nodes: nodes,
|
|
512
678
|
dependency_edges: edges,
|
|
513
679
|
suggested_groupings: groupings,
|
|
@@ -535,11 +701,17 @@ function emitFromKnowledge(kg, opts) {
|
|
|
535
701
|
const edges = [];
|
|
536
702
|
const groupings = [];
|
|
537
703
|
const conceptIds = new Set(arr(kg.concepts).map((c) => str(c.id)));
|
|
704
|
+
const externalByNode = /* @__PURE__ */ new Map();
|
|
538
705
|
for (const c of arr(kg.concepts)) {
|
|
539
706
|
const node = feedNodeFromConcept(c);
|
|
540
707
|
nodes.push(node);
|
|
541
|
-
|
|
542
|
-
|
|
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) {
|
|
543
715
|
edges.push({
|
|
544
716
|
from: p,
|
|
545
717
|
to: node.id,
|
|
@@ -549,6 +721,7 @@ function emitFromKnowledge(kg, opts) {
|
|
|
549
721
|
}
|
|
550
722
|
}
|
|
551
723
|
}
|
|
724
|
+
noteExternalPrerequisites(opts, externalByNode);
|
|
552
725
|
if (arr(kg.learning_path).length > 0) {
|
|
553
726
|
const orderMap = /* @__PURE__ */ new Map();
|
|
554
727
|
arr(kg.learning_path).forEach((s, idx) => {
|
|
@@ -573,7 +746,7 @@ function emitFromKnowledge(kg, opts) {
|
|
|
573
746
|
newKeywordsInOrder(nodes);
|
|
574
747
|
return validateCurriculumFeed({
|
|
575
748
|
schema_version: 2,
|
|
576
|
-
source:
|
|
749
|
+
source: buildSource(opts, "knowledge_graph"),
|
|
577
750
|
learning_nodes: nodes,
|
|
578
751
|
dependency_edges: edges,
|
|
579
752
|
suggested_groupings: groupings,
|
|
@@ -584,17 +757,38 @@ function emitFromProject(pg, opts) {
|
|
|
584
757
|
const nodes = [];
|
|
585
758
|
const edges = [];
|
|
586
759
|
const groupings = [];
|
|
587
|
-
arr(pg.features)
|
|
588
|
-
|
|
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) {
|
|
589
783
|
const hasTasks = arr(pg.implementation ?? {}).length > 0 || arr(pg.implementation?.tasks).length > 0;
|
|
590
784
|
throw new Error(
|
|
591
|
-
"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.")
|
|
592
786
|
);
|
|
593
787
|
}
|
|
594
788
|
newKeywordsInOrder(nodes);
|
|
595
789
|
return validateCurriculumFeed({
|
|
596
790
|
schema_version: 2,
|
|
597
|
-
source:
|
|
791
|
+
source: buildSource(opts, "project_graph"),
|
|
598
792
|
learning_nodes: nodes,
|
|
599
793
|
dependency_edges: edges,
|
|
600
794
|
suggested_groupings: groupings,
|
|
@@ -613,6 +807,8 @@ function emitCurriculumFeed(graph, opts = {}) {
|
|
|
613
807
|
return emitFromProject(g, opts);
|
|
614
808
|
}
|
|
615
809
|
|
|
810
|
+
exports.DEFAULT_STEP_MINUTES = DEFAULT_STEP_MINUTES;
|
|
811
|
+
exports.DEPTH_MINUTE_FRACTIONS = DEPTH_MINUTE_FRACTIONS;
|
|
616
812
|
exports.emitCurriculumFeed = emitCurriculumFeed;
|
|
617
813
|
//# sourceMappingURL=index.cjs.map
|
|
618
814
|
//# sourceMappingURL=index.cjs.map
|