@thanh01.pmt/domain-kit 0.2.1 → 0.4.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 +42 -11
- package/dist/{chunk-CRN6D4HG.mjs → chunk-HILSUZOF.mjs} +32 -8
- package/dist/chunk-HILSUZOF.mjs.map +1 -0
- package/dist/{chunk-23TQPTHG.mjs → chunk-LZQLLAYP.mjs} +5 -5
- package/dist/{chunk-23TQPTHG.mjs.map → chunk-LZQLLAYP.mjs.map} +1 -1
- package/dist/{chunk-HH4UX65C.mjs → chunk-PKUEOSZ3.mjs} +42 -4
- package/dist/chunk-PKUEOSZ3.mjs.map +1 -0
- package/dist/{chunk-HUDFN4IX.mjs → chunk-QJF4QCBJ.mjs} +99 -4
- package/dist/chunk-QJF4QCBJ.mjs.map +1 -0
- package/dist/{chunk-WHBAGJWN.mjs → chunk-VCPIWJ7R.mjs} +4 -4
- package/dist/chunk-VCPIWJ7R.mjs.map +1 -0
- package/dist/curriculumFeedEmitter-6IK6PFDH.mjs +4 -0
- package/dist/{curriculumFeedEmitter-E5DAJYUZ.mjs.map → curriculumFeedEmitter-6IK6PFDH.mjs.map} +1 -1
- package/dist/{curriculumFeedSchema-TEeQg2bY.d.ts → curriculumFeedSchema-B66H3gEV.d.cts} +148 -8
- package/dist/{curriculumFeedSchema-TEeQg2bY.d.cts → curriculumFeedSchema-B66H3gEV.d.ts} +148 -8
- package/dist/feed/index.cjs +136 -3
- package/dist/feed/index.cjs.map +1 -1
- package/dist/feed/index.d.cts +1 -1
- package/dist/feed/index.d.ts +1 -1
- package/dist/feed/index.mjs +2 -2
- package/dist/graph/index.cjs +30 -5
- package/dist/graph/index.cjs.map +1 -1
- package/dist/graph/index.d.cts +2 -2
- package/dist/graph/index.d.ts +2 -2
- package/dist/graph/index.mjs +1 -1
- package/dist/{hybridGraphPipeline-DVZYLCAQ.d.ts → hybridGraphPipeline-DXQDbBEj.d.ts} +14 -2
- package/dist/{hybridGraphPipeline-DmWRKYXf.d.cts → hybridGraphPipeline-dT-bio-3.d.cts} +14 -2
- package/dist/{hybridGraphSchema-BCgXicgA.d.ts → hybridGraphSchema-nrUeSwpU.d.cts} +15 -15
- package/dist/{hybridGraphSchema-BCgXicgA.d.cts → hybridGraphSchema-nrUeSwpU.d.ts} +15 -15
- package/dist/index.cjs +168 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +5 -5
- package/dist/pipeline/index.cjs +165 -8
- package/dist/pipeline/index.cjs.map +1 -1
- package/dist/pipeline/index.d.cts +3 -3
- package/dist/pipeline/index.d.ts +3 -3
- package/dist/pipeline/index.mjs +4 -4
- package/dist/schemas/index.cjs +42 -4
- 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 +10 -9
- package/LICENSE +0 -21
- package/dist/chunk-CRN6D4HG.mjs.map +0 -1
- package/dist/chunk-HH4UX65C.mjs.map +0 -1
- package/dist/chunk-HUDFN4IX.mjs.map +0 -1
- package/dist/chunk-WHBAGJWN.mjs.map +0 -1
- package/dist/curriculumFeedEmitter-E5DAJYUZ.mjs +0 -4
|
@@ -105,8 +105,39 @@ declare const FeedNodeSchema: z.ZodObject<{
|
|
|
105
105
|
}>, "many">>;
|
|
106
106
|
feature_id: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
107
107
|
concept_ref: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
108
|
+
user_visible_deliverable: z.ZodDefault<z.ZodString>;
|
|
109
|
+
depth_variants: z.ZodOptional<z.ZodObject<{
|
|
110
|
+
ulo: z.ZodNumber;
|
|
111
|
+
cio: z.ZodNumber;
|
|
112
|
+
sio: z.ZodNumber;
|
|
113
|
+
}, "strip", z.ZodTypeAny, {
|
|
114
|
+
ulo: number;
|
|
115
|
+
cio: number;
|
|
116
|
+
sio: number;
|
|
117
|
+
}, {
|
|
118
|
+
ulo: number;
|
|
119
|
+
cio: number;
|
|
120
|
+
sio: number;
|
|
121
|
+
}>>;
|
|
122
|
+
depth_scaffold_candidates: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
123
|
+
from_depth: z.ZodEnum<["ulo", "cio", "sio"]>;
|
|
124
|
+
to_depth: z.ZodEnum<["ulo", "cio", "sio"]>;
|
|
125
|
+
minutes_saved: z.ZodNumber;
|
|
126
|
+
reason: z.ZodDefault<z.ZodString>;
|
|
127
|
+
}, "strip", z.ZodTypeAny, {
|
|
128
|
+
reason: string;
|
|
129
|
+
minutes_saved: number;
|
|
130
|
+
from_depth: "ulo" | "cio" | "sio";
|
|
131
|
+
to_depth: "ulo" | "cio" | "sio";
|
|
132
|
+
}, {
|
|
133
|
+
minutes_saved: number;
|
|
134
|
+
from_depth: "ulo" | "cio" | "sio";
|
|
135
|
+
to_depth: "ulo" | "cio" | "sio";
|
|
136
|
+
reason?: string | undefined;
|
|
137
|
+
}>, "many">>;
|
|
138
|
+
is_core: z.ZodDefault<z.ZodBoolean>;
|
|
108
139
|
phase_id: z.ZodDefault<z.ZodString>;
|
|
109
|
-
introduce_aspect: z.ZodDefault<z.ZodNullable<z.ZodEnum<["new", "advanced"]>>>;
|
|
140
|
+
introduce_aspect: z.ZodDefault<z.ZodNullable<z.ZodEnum<["new", "advanced", "preview"]>>>;
|
|
110
141
|
}, "strip", z.ZodTypeAny, {
|
|
111
142
|
estimated_minutes: number;
|
|
112
143
|
id: string;
|
|
@@ -137,7 +168,20 @@ declare const FeedNodeSchema: z.ZodObject<{
|
|
|
137
168
|
file: string;
|
|
138
169
|
}[];
|
|
139
170
|
concept_ref: string | null;
|
|
140
|
-
|
|
171
|
+
user_visible_deliverable: string;
|
|
172
|
+
is_core: boolean;
|
|
173
|
+
introduce_aspect: "new" | "advanced" | "preview" | null;
|
|
174
|
+
depth_variants?: {
|
|
175
|
+
ulo: number;
|
|
176
|
+
cio: number;
|
|
177
|
+
sio: number;
|
|
178
|
+
} | undefined;
|
|
179
|
+
depth_scaffold_candidates?: {
|
|
180
|
+
reason: string;
|
|
181
|
+
minutes_saved: number;
|
|
182
|
+
from_depth: "ulo" | "cio" | "sio";
|
|
183
|
+
to_depth: "ulo" | "cio" | "sio";
|
|
184
|
+
}[] | undefined;
|
|
141
185
|
}, {
|
|
142
186
|
estimated_minutes: number;
|
|
143
187
|
id: string;
|
|
@@ -168,7 +212,20 @@ declare const FeedNodeSchema: z.ZodObject<{
|
|
|
168
212
|
evidence?: string | undefined;
|
|
169
213
|
}[] | undefined;
|
|
170
214
|
concept_ref?: string | null | undefined;
|
|
171
|
-
|
|
215
|
+
user_visible_deliverable?: string | undefined;
|
|
216
|
+
depth_variants?: {
|
|
217
|
+
ulo: number;
|
|
218
|
+
cio: number;
|
|
219
|
+
sio: number;
|
|
220
|
+
} | undefined;
|
|
221
|
+
depth_scaffold_candidates?: {
|
|
222
|
+
minutes_saved: number;
|
|
223
|
+
from_depth: "ulo" | "cio" | "sio";
|
|
224
|
+
to_depth: "ulo" | "cio" | "sio";
|
|
225
|
+
reason?: string | undefined;
|
|
226
|
+
}[] | undefined;
|
|
227
|
+
is_core?: boolean | undefined;
|
|
228
|
+
introduce_aspect?: "new" | "advanced" | "preview" | null | undefined;
|
|
172
229
|
}>;
|
|
173
230
|
declare const FeedEdgeSchema: z.ZodObject<{
|
|
174
231
|
from: z.ZodString;
|
|
@@ -306,8 +363,39 @@ declare const CurriculumFeedSchema: z.ZodObject<{
|
|
|
306
363
|
}>, "many">>;
|
|
307
364
|
feature_id: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
308
365
|
concept_ref: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
366
|
+
user_visible_deliverable: z.ZodDefault<z.ZodString>;
|
|
367
|
+
depth_variants: z.ZodOptional<z.ZodObject<{
|
|
368
|
+
ulo: z.ZodNumber;
|
|
369
|
+
cio: z.ZodNumber;
|
|
370
|
+
sio: z.ZodNumber;
|
|
371
|
+
}, "strip", z.ZodTypeAny, {
|
|
372
|
+
ulo: number;
|
|
373
|
+
cio: number;
|
|
374
|
+
sio: number;
|
|
375
|
+
}, {
|
|
376
|
+
ulo: number;
|
|
377
|
+
cio: number;
|
|
378
|
+
sio: number;
|
|
379
|
+
}>>;
|
|
380
|
+
depth_scaffold_candidates: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
381
|
+
from_depth: z.ZodEnum<["ulo", "cio", "sio"]>;
|
|
382
|
+
to_depth: z.ZodEnum<["ulo", "cio", "sio"]>;
|
|
383
|
+
minutes_saved: z.ZodNumber;
|
|
384
|
+
reason: z.ZodDefault<z.ZodString>;
|
|
385
|
+
}, "strip", z.ZodTypeAny, {
|
|
386
|
+
reason: string;
|
|
387
|
+
minutes_saved: number;
|
|
388
|
+
from_depth: "ulo" | "cio" | "sio";
|
|
389
|
+
to_depth: "ulo" | "cio" | "sio";
|
|
390
|
+
}, {
|
|
391
|
+
minutes_saved: number;
|
|
392
|
+
from_depth: "ulo" | "cio" | "sio";
|
|
393
|
+
to_depth: "ulo" | "cio" | "sio";
|
|
394
|
+
reason?: string | undefined;
|
|
395
|
+
}>, "many">>;
|
|
396
|
+
is_core: z.ZodDefault<z.ZodBoolean>;
|
|
309
397
|
phase_id: z.ZodDefault<z.ZodString>;
|
|
310
|
-
introduce_aspect: z.ZodDefault<z.ZodNullable<z.ZodEnum<["new", "advanced"]>>>;
|
|
398
|
+
introduce_aspect: z.ZodDefault<z.ZodNullable<z.ZodEnum<["new", "advanced", "preview"]>>>;
|
|
311
399
|
}, "strip", z.ZodTypeAny, {
|
|
312
400
|
estimated_minutes: number;
|
|
313
401
|
id: string;
|
|
@@ -338,7 +426,20 @@ declare const CurriculumFeedSchema: z.ZodObject<{
|
|
|
338
426
|
file: string;
|
|
339
427
|
}[];
|
|
340
428
|
concept_ref: string | null;
|
|
341
|
-
|
|
429
|
+
user_visible_deliverable: string;
|
|
430
|
+
is_core: boolean;
|
|
431
|
+
introduce_aspect: "new" | "advanced" | "preview" | null;
|
|
432
|
+
depth_variants?: {
|
|
433
|
+
ulo: number;
|
|
434
|
+
cio: number;
|
|
435
|
+
sio: number;
|
|
436
|
+
} | undefined;
|
|
437
|
+
depth_scaffold_candidates?: {
|
|
438
|
+
reason: string;
|
|
439
|
+
minutes_saved: number;
|
|
440
|
+
from_depth: "ulo" | "cio" | "sio";
|
|
441
|
+
to_depth: "ulo" | "cio" | "sio";
|
|
442
|
+
}[] | undefined;
|
|
342
443
|
}, {
|
|
343
444
|
estimated_minutes: number;
|
|
344
445
|
id: string;
|
|
@@ -369,7 +470,20 @@ declare const CurriculumFeedSchema: z.ZodObject<{
|
|
|
369
470
|
evidence?: string | undefined;
|
|
370
471
|
}[] | undefined;
|
|
371
472
|
concept_ref?: string | null | undefined;
|
|
372
|
-
|
|
473
|
+
user_visible_deliverable?: string | undefined;
|
|
474
|
+
depth_variants?: {
|
|
475
|
+
ulo: number;
|
|
476
|
+
cio: number;
|
|
477
|
+
sio: number;
|
|
478
|
+
} | undefined;
|
|
479
|
+
depth_scaffold_candidates?: {
|
|
480
|
+
minutes_saved: number;
|
|
481
|
+
from_depth: "ulo" | "cio" | "sio";
|
|
482
|
+
to_depth: "ulo" | "cio" | "sio";
|
|
483
|
+
reason?: string | undefined;
|
|
484
|
+
}[] | undefined;
|
|
485
|
+
is_core?: boolean | undefined;
|
|
486
|
+
introduce_aspect?: "new" | "advanced" | "preview" | null | undefined;
|
|
373
487
|
}>, "many">;
|
|
374
488
|
dependency_edges: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
375
489
|
from: z.ZodString;
|
|
@@ -466,7 +580,20 @@ declare const CurriculumFeedSchema: z.ZodObject<{
|
|
|
466
580
|
file: string;
|
|
467
581
|
}[];
|
|
468
582
|
concept_ref: string | null;
|
|
469
|
-
|
|
583
|
+
user_visible_deliverable: string;
|
|
584
|
+
is_core: boolean;
|
|
585
|
+
introduce_aspect: "new" | "advanced" | "preview" | null;
|
|
586
|
+
depth_variants?: {
|
|
587
|
+
ulo: number;
|
|
588
|
+
cio: number;
|
|
589
|
+
sio: number;
|
|
590
|
+
} | undefined;
|
|
591
|
+
depth_scaffold_candidates?: {
|
|
592
|
+
reason: string;
|
|
593
|
+
minutes_saved: number;
|
|
594
|
+
from_depth: "ulo" | "cio" | "sio";
|
|
595
|
+
to_depth: "ulo" | "cio" | "sio";
|
|
596
|
+
}[] | undefined;
|
|
470
597
|
}[];
|
|
471
598
|
dependency_edges: {
|
|
472
599
|
from: string;
|
|
@@ -517,7 +644,20 @@ declare const CurriculumFeedSchema: z.ZodObject<{
|
|
|
517
644
|
evidence?: string | undefined;
|
|
518
645
|
}[] | undefined;
|
|
519
646
|
concept_ref?: string | null | undefined;
|
|
520
|
-
|
|
647
|
+
user_visible_deliverable?: string | undefined;
|
|
648
|
+
depth_variants?: {
|
|
649
|
+
ulo: number;
|
|
650
|
+
cio: number;
|
|
651
|
+
sio: number;
|
|
652
|
+
} | undefined;
|
|
653
|
+
depth_scaffold_candidates?: {
|
|
654
|
+
minutes_saved: number;
|
|
655
|
+
from_depth: "ulo" | "cio" | "sio";
|
|
656
|
+
to_depth: "ulo" | "cio" | "sio";
|
|
657
|
+
reason?: string | undefined;
|
|
658
|
+
}[] | undefined;
|
|
659
|
+
is_core?: boolean | undefined;
|
|
660
|
+
introduce_aspect?: "new" | "advanced" | "preview" | null | undefined;
|
|
521
661
|
}[];
|
|
522
662
|
phases?: {
|
|
523
663
|
id: string;
|
package/dist/feed/index.cjs
CHANGED
|
@@ -31,11 +31,33 @@ var FeedNodeSchema = zod.z.object({
|
|
|
31
31
|
references: zod.z.array(zod.z.object({ file: zod.z.string().min(1), evidence: zod.z.string().default("") })).default([]),
|
|
32
32
|
feature_id: zod.z.string().nullable().default(null),
|
|
33
33
|
concept_ref: zod.z.string().nullable().default(null),
|
|
34
|
+
// P52/T2.1: outcome.user_visible projected from the ProjectGraph step — the
|
|
35
|
+
// tangible checkpoint the session-cutting planner aims for (empty when the
|
|
36
|
+
// step declares no user-visible result). Concepts carry '' by default.
|
|
37
|
+
user_visible_deliverable: zod.z.string().default(""),
|
|
38
|
+
// P52/T3.1 — depth-reconcile inputs (optional/defaulted so old feeds
|
|
39
|
+
// validate unchanged):
|
|
40
|
+
// • depth_variants: minutes to teach this node at each depth level.
|
|
41
|
+
// ULO (know-it-exists) ≤ CIO (mechanism) ≤ SIO (implementation). All 3
|
|
42
|
+
// required so a downgrade never turns an anchor teach into a zero.
|
|
43
|
+
// • depth_scaffold_candidates: parallel to scaffold_candidates but
|
|
44
|
+
// acts on DEPTH (e.g. SIO→CIO) instead of lesson time.
|
|
45
|
+
// • is_core: concept is a Master-Tree core concept — reconciler must
|
|
46
|
+
// never downgrade it; escalate instead.
|
|
47
|
+
depth_variants: zod.z.object({ ulo: zod.z.number().int().nonnegative(), cio: zod.z.number().int().nonnegative(), sio: zod.z.number().int().nonnegative() }).optional(),
|
|
48
|
+
depth_scaffold_candidates: zod.z.array(zod.z.object({
|
|
49
|
+
from_depth: zod.z.enum(["ulo", "cio", "sio"]),
|
|
50
|
+
to_depth: zod.z.enum(["ulo", "cio", "sio"]),
|
|
51
|
+
minutes_saved: zod.z.number().int().nonnegative(),
|
|
52
|
+
reason: zod.z.string().default("")
|
|
53
|
+
})).optional(),
|
|
54
|
+
is_core: zod.z.boolean().default(false),
|
|
34
55
|
// Progressive-completion bookkeeping (feed v2, additive):
|
|
35
56
|
// phase_id = development phase that introduces/uses this node;
|
|
36
|
-
// introduce_aspect = how the phase teaches a concept node (first intro
|
|
57
|
+
// introduce_aspect = how the phase teaches a concept node (first intro, advanced
|
|
58
|
+
// revisit, or preview — a light ULO awareness pass that is NOT the introduction).
|
|
37
59
|
phase_id: zod.z.string().default(""),
|
|
38
|
-
introduce_aspect: zod.z.enum(["new", "advanced"]).nullable().default(null)
|
|
60
|
+
introduce_aspect: zod.z.enum(["new", "advanced", "preview"]).nullable().default(null)
|
|
39
61
|
});
|
|
40
62
|
var FeedEdgeSchema = zod.z.object({
|
|
41
63
|
from: zod.z.string().min(1),
|
|
@@ -94,6 +116,22 @@ function validateCurriculumFeed(feed) {
|
|
|
94
116
|
}
|
|
95
117
|
const phaseIds = parsed.phases.map((p) => p.id);
|
|
96
118
|
if (new Set(phaseIds).size !== phaseIds.length) throw new Error("curriculum feed contains duplicate phase ids");
|
|
119
|
+
if (parsed.phases.length > 0) {
|
|
120
|
+
const phaseIndexOf = /* @__PURE__ */ new Map();
|
|
121
|
+
parsed.phases.forEach((p, i) => {
|
|
122
|
+
for (const id of p.node_ids) phaseIndexOf.set(id, i);
|
|
123
|
+
});
|
|
124
|
+
for (const e of parsed.dependency_edges) {
|
|
125
|
+
const fromIdx = phaseIndexOf.get(e.from);
|
|
126
|
+
const toIdx = phaseIndexOf.get(e.to);
|
|
127
|
+
if (fromIdx === void 0 || toIdx === void 0) continue;
|
|
128
|
+
if (fromIdx > toIdx) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"curriculum feed teaching-order violation: prerequisite " + e.from + " (phase " + (fromIdx + 1) + ") is scheduled AFTER its consumer " + e.to + " (phase " + (toIdx + 1) + ")"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
97
135
|
return parsed;
|
|
98
136
|
}
|
|
99
137
|
|
|
@@ -108,6 +146,9 @@ function stepMinutes(step) {
|
|
|
108
146
|
const effort = step.effort ?? {};
|
|
109
147
|
return Math.max(0, Math.round(num(effort.estimated_minutes, num(step.estimated_minutes, 45))));
|
|
110
148
|
}
|
|
149
|
+
function pushWarning(opts, message) {
|
|
150
|
+
(opts.warnings ?? (opts.warnings = [])).push(message);
|
|
151
|
+
}
|
|
111
152
|
function newKeywordsInOrder(nodes) {
|
|
112
153
|
const seen = /* @__PURE__ */ new Set();
|
|
113
154
|
for (const n of nodes) {
|
|
@@ -137,6 +178,10 @@ function feedNodeFromStep(step, featureId) {
|
|
|
137
178
|
references: arr(step.files).map((f) => ({ file: str(f), evidence: "step file" })).filter((f) => f.file),
|
|
138
179
|
feature_id: featureId,
|
|
139
180
|
concept_ref: null,
|
|
181
|
+
// P52/T2.1: carry the step's tangible outcome so the planner can cut
|
|
182
|
+
// sessions at user-visible checkpoints instead of raw minute counts.
|
|
183
|
+
user_visible_deliverable: str(step.outcome?.user_visible),
|
|
184
|
+
is_core: false,
|
|
140
185
|
phase_id: "",
|
|
141
186
|
introduce_aspect: null
|
|
142
187
|
};
|
|
@@ -155,6 +200,20 @@ function deriveConceptDepthHint(concept) {
|
|
|
155
200
|
if (str(concept.cio) || prereqs.length >= 1) return "cio";
|
|
156
201
|
return "ulo";
|
|
157
202
|
}
|
|
203
|
+
function deriveDepthInputs(minutes, depth) {
|
|
204
|
+
const ulo = Math.max(5, Math.round(minutes * 0.35));
|
|
205
|
+
const cio = Math.max(ulo + 5, Math.round(minutes * 0.65));
|
|
206
|
+
const sio = Math.max(cio + 5, minutes);
|
|
207
|
+
const variants = { ulo, cio, sio };
|
|
208
|
+
const candidates = [];
|
|
209
|
+
if (depth === "sio") {
|
|
210
|
+
candidates.push({ from_depth: "sio", to_depth: "cio", minutes_saved: sio - cio, reason: "SIO implementation practice deferred \u2014 CIO mechanism suffices for downstream consumers" });
|
|
211
|
+
}
|
|
212
|
+
if (depth === "cio" || depth === "sio") {
|
|
213
|
+
candidates.push({ from_depth: depth, to_depth: "ulo", minutes_saved: (depth === "sio" ? sio : cio) - ulo, reason: "Mechanism deferred \u2014 ULO awareness suffices (know-it-exists); never below the ULO floor" });
|
|
214
|
+
}
|
|
215
|
+
return { depth_variants: variants, depth_scaffold_candidates: candidates };
|
|
216
|
+
}
|
|
158
217
|
function feedNodeFromConcept(concept) {
|
|
159
218
|
const id = str(concept.id);
|
|
160
219
|
const pts = arr(concept.problem_types).map((p) => str(p.bloom_level, "Understand")).filter((b) => b in BLOOM_RANK).sort((a, b) => BLOOM_RANK[a] - BLOOM_RANK[b]);
|
|
@@ -184,6 +243,10 @@ function feedNodeFromConcept(concept) {
|
|
|
184
243
|
references,
|
|
185
244
|
feature_id: null,
|
|
186
245
|
concept_ref: id,
|
|
246
|
+
user_visible_deliverable: "",
|
|
247
|
+
// concepts are taught, not shipped — checkpoints live on product steps
|
|
248
|
+
...deriveDepthInputs(Math.max(0, Math.round(num(concept.estimated_minutes, 15))), deriveConceptDepthHint(concept)),
|
|
249
|
+
is_core: false,
|
|
187
250
|
phase_id: "",
|
|
188
251
|
introduce_aspect: null
|
|
189
252
|
};
|
|
@@ -244,6 +307,7 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
244
307
|
}
|
|
245
308
|
const emitCtx = [];
|
|
246
309
|
const emittedConcepts = /* @__PURE__ */ new Set();
|
|
310
|
+
const previewsEmitted = /* @__PURE__ */ new Set();
|
|
247
311
|
const linkList = arr(hybrid.links);
|
|
248
312
|
for (const rp of rawPhases) {
|
|
249
313
|
const pid = str(rp.id);
|
|
@@ -252,7 +316,8 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
252
316
|
const intros = arr(rp.introduces);
|
|
253
317
|
const news = intros.filter((i) => str(i.aspect, "new") === "new");
|
|
254
318
|
const advanced = intros.filter((i) => str(i.aspect) === "advanced");
|
|
255
|
-
|
|
319
|
+
const previews = intros.filter((i) => str(i.aspect) === "preview");
|
|
320
|
+
for (const intro of [...news, ...advanced, ...previews]) {
|
|
256
321
|
const cid = str(intro.concept_id);
|
|
257
322
|
const c = conceptById.get(cid);
|
|
258
323
|
if (!c) {
|
|
@@ -261,6 +326,39 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
261
326
|
if (str(intro.aspect, "new") === "advanced" && !emittedConcepts.has(cid)) {
|
|
262
327
|
throw new Error("Phase " + pid + " advanced-revisits concept " + cid + " that was never emitted - fail-closed");
|
|
263
328
|
}
|
|
329
|
+
if (str(intro.aspect, "new") === "preview") {
|
|
330
|
+
if (emittedConcepts.has(cid)) {
|
|
331
|
+
throw new Error("Phase " + pid + " previews concept " + cid + " that was already introduced - preview must precede the anchor teach - fail-closed");
|
|
332
|
+
}
|
|
333
|
+
if (previewsEmitted.has(cid)) {
|
|
334
|
+
throw new Error("Phase " + pid + " duplicate preview of concept " + cid + " - fail-closed");
|
|
335
|
+
}
|
|
336
|
+
const pidIdx = rawPhases.findIndex((rp2) => str(rp2.id) === pid);
|
|
337
|
+
const anchorIdx = rawPhases.findIndex((rp2) => arr(rp2.introduces).some((i2) => str(i2.concept_id) === cid && str(i2.aspect, "new") !== "preview"));
|
|
338
|
+
if (anchorIdx < 0) {
|
|
339
|
+
throw new Error("Phase " + pid + " previews concept " + cid + " that is never fully introduced in any phase - fail-closed");
|
|
340
|
+
}
|
|
341
|
+
if (pidIdx >= anchorIdx) {
|
|
342
|
+
throw new Error("Phase " + pid + " previews concept " + cid + " whose anchor introduction is not in a LATER phase - fail-closed");
|
|
343
|
+
}
|
|
344
|
+
previewsEmitted.add(cid);
|
|
345
|
+
const prevNode = feedNodeFromConcept(c);
|
|
346
|
+
prevNode.id = cid + "__PREV" + pad2(order);
|
|
347
|
+
prevNode.name = str(c.name, cid) + " (preview)";
|
|
348
|
+
prevNode.phase_id = pid;
|
|
349
|
+
prevNode.introduce_aspect = "preview";
|
|
350
|
+
prevNode.estimated_minutes = Math.max(5, Math.round(prevNode.estimated_minutes * 0.25));
|
|
351
|
+
prevNode.depth_hint = "ulo";
|
|
352
|
+
prevNode.bloom_hint = "Understand";
|
|
353
|
+
prevNode.concept_codes = [str(c.code) || cid];
|
|
354
|
+
prevNode.keywords = { all: strs(c.keywords), new: [], prerequisite: [] };
|
|
355
|
+
prevNode.scaffolding = { scaffold_candidates: [], core_value_minutes: 0 };
|
|
356
|
+
prevNode.depth_variants = { ulo: prevNode.estimated_minutes, cio: prevNode.estimated_minutes, sio: prevNode.estimated_minutes };
|
|
357
|
+
prevNode.depth_scaffold_candidates = [];
|
|
358
|
+
nodes.push(prevNode);
|
|
359
|
+
conceptOrder.push(prevNode.id);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
264
362
|
if (!emittedConcepts.has(cid)) {
|
|
265
363
|
const node = feedNodeFromConcept(c);
|
|
266
364
|
node.phase_id = pid;
|
|
@@ -281,6 +379,7 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
281
379
|
node.name = str(c.name, cid) + " (advanced)";
|
|
282
380
|
node.phase_id = pid;
|
|
283
381
|
node.introduce_aspect = "advanced";
|
|
382
|
+
node.depth_scaffold_candidates = [{ from_depth: "sio", to_depth: "cio", minutes_saved: Math.round(node.estimated_minutes * 0.3), reason: "advanced revisit may drop to CIO recap without breaking first-teach chain" }];
|
|
284
383
|
node.estimated_minutes = Math.max(15, Math.round(node.estimated_minutes * 0.5));
|
|
285
384
|
node.concept_codes = [str(c.code) || cid];
|
|
286
385
|
edges.push({ from: cid, to: revisitId, kind: "knowledge", reason: "advanced revisit of " + cid + " at phase " + pid });
|
|
@@ -316,6 +415,17 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
316
415
|
}
|
|
317
416
|
phaseNodeIds.push(ids);
|
|
318
417
|
}
|
|
418
|
+
const placed = new Set(phaseNodeIds.flat());
|
|
419
|
+
const orderedNodes = [];
|
|
420
|
+
for (const ids of phaseNodeIds) {
|
|
421
|
+
for (const id of ids) {
|
|
422
|
+
const n = nodes.find((x) => x.id === id);
|
|
423
|
+
if (n) orderedNodes.push(n);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
for (const n of nodes) if (!placed.has(n.id)) orderedNodes.push(n);
|
|
427
|
+
nodes.length = 0;
|
|
428
|
+
nodes.push(...orderedNodes);
|
|
319
429
|
const edgeSeen = new Set(edges.map((e) => e.from + ">" + e.to));
|
|
320
430
|
const pushEdge = (from, to, reason) => {
|
|
321
431
|
const key = from + ">" + to;
|
|
@@ -330,6 +440,14 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
330
440
|
const firstStep = (stepIdsByFeature.get(fid) || [])[0];
|
|
331
441
|
if (!firstStep) continue;
|
|
332
442
|
const depth = str(l.depth, "cio");
|
|
443
|
+
const anchorNode = nodes.find((n2) => n2.kind === "concept" && n2.concept_ref === cid && n2.introduce_aspect === "new");
|
|
444
|
+
const stepNode = nodes.find((n2) => n2.id === firstStep);
|
|
445
|
+
const anchorPhaseIdx = anchorNode ? emitCtx.findIndex((c2) => c2.id === anchorNode.phase_id) : -1;
|
|
446
|
+
const stepPhaseIdx = stepNode ? emitCtx.findIndex((c2) => c2.id === stepNode.phase_id) : -1;
|
|
447
|
+
if (emitCtx.length > 0 && anchorPhaseIdx > stepPhaseIdx) {
|
|
448
|
+
pushWarning(opts, "hybrid link drift: concept " + cid + " is fully taught in a later phase than consumer " + firstStep + " (a preview covers the dependency)");
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
333
451
|
pushEdge(cid, firstStep, str(l.relationship, "hybrid link") + " [" + depth + "]");
|
|
334
452
|
for (const n of nodes) {
|
|
335
453
|
if (n.kind === "concept" && n.concept_ref === cid && n.introduce_aspect === "advanced" && n.phase_id === str(l.phase_id)) {
|
|
@@ -348,6 +466,21 @@ function emitFromHybrid(hybrid, opts) {
|
|
|
348
466
|
for (const k of n.keywords?.prerequisite ?? []) {
|
|
349
467
|
const src = introducer.get(k.toLowerCase());
|
|
350
468
|
if (!src || src === n.id) continue;
|
|
469
|
+
const srcNode = nodes.find((x) => x.id === src);
|
|
470
|
+
const phaseOf = (id) => {
|
|
471
|
+
const n2 = nodes.find((x) => x.id === id);
|
|
472
|
+
return n2 ? emitCtx.findIndex((c) => c.id === n2.phase_id) : -1;
|
|
473
|
+
};
|
|
474
|
+
const conceptOvertaken = n.kind === "concept" && srcNode?.kind === "product_step";
|
|
475
|
+
const fromPhase = phaseOf(src);
|
|
476
|
+
const toPhase = phaseOf(n.id);
|
|
477
|
+
if (conceptOvertaken || emitCtx.length > 0 && fromPhase > toPhase) {
|
|
478
|
+
pushWarning(
|
|
479
|
+
opts,
|
|
480
|
+
"keyword-order drift: introducer " + src + " sits in a later phase than consumer " + n.id + ' (keyword "' + k + '") \u2014 host should move the introducer earlier'
|
|
481
|
+
);
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
351
484
|
if (feedOrder.get(src) < feedOrder.get(n.id)) pushEdge(src, n.id, "keyword prerequisite [" + k + "]");
|
|
352
485
|
}
|
|
353
486
|
}
|