@thanh01.pmt/domain-kit 0.6.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -10
  3. package/dist/assembly/index.d.cts +3 -2
  4. package/dist/assembly/index.d.ts +3 -2
  5. package/dist/{chunk-2OGNQUXC.mjs → chunk-4UCXYRM3.mjs} +152 -303
  6. package/dist/chunk-4UCXYRM3.mjs.map +1 -0
  7. package/dist/chunk-7A2SG466.mjs +325 -0
  8. package/dist/chunk-7A2SG466.mjs.map +1 -0
  9. package/dist/{chunk-DRVOV5ZD.mjs → chunk-DSEJLTUO.mjs} +2 -2
  10. package/dist/chunk-DSEJLTUO.mjs.map +1 -0
  11. package/dist/{chunk-ERDTHXQA.mjs → chunk-H4W3YJLK.mjs} +10 -3
  12. package/dist/chunk-H4W3YJLK.mjs.map +1 -0
  13. package/dist/chunk-IBVXXVGE.mjs +14 -0
  14. package/dist/chunk-IBVXXVGE.mjs.map +1 -0
  15. package/dist/{chunk-5OMSYKQP.mjs → chunk-KHCZYLKM.mjs} +33 -18
  16. package/dist/chunk-KHCZYLKM.mjs.map +1 -0
  17. package/dist/{chunk-67GXK3ZE.mjs → chunk-WD3P5V3B.mjs} +3 -2
  18. package/dist/chunk-WD3P5V3B.mjs.map +1 -0
  19. package/dist/{conceptEscalator-DPrs4PwC.d.cts → conceptEscalator-BmMJD8C5.d.cts} +1 -1
  20. package/dist/{conceptEscalator-BCsw0ys_.d.ts → conceptEscalator-DMsehscs.d.ts} +1 -1
  21. package/dist/concepts/index.d.cts +4 -3
  22. package/dist/concepts/index.d.ts +4 -3
  23. package/dist/{cpp-CZUPqJt9.d.cts → cpp-vlDl6pCL.d.cts} +4 -4
  24. package/dist/{cpp-CZUPqJt9.d.ts → cpp-vlDl6pCL.d.ts} +4 -4
  25. package/dist/detector/index.cjs +1 -0
  26. package/dist/detector/index.cjs.map +1 -1
  27. package/dist/detector/index.d.cts +2 -2
  28. package/dist/detector/index.d.ts +2 -2
  29. package/dist/detector/index.mjs +1 -1
  30. package/dist/{domainProfileDetector-DHb1T0zB.d.ts → domainProfileDetector-6pjgL-6_.d.ts} +1 -1
  31. package/dist/{domainProfileDetector-bzugdQmS.d.cts → domainProfileDetector-BsADsuRV.d.cts} +1 -1
  32. package/dist/{domainProfileSchema-CwT3Ffsw.d.cts → domainProfileSchema-779FGk14.d.cts} +38 -1
  33. package/dist/{domainProfileSchema-CwT3Ffsw.d.ts → domainProfileSchema-779FGk14.d.ts} +38 -1
  34. package/dist/extractors/index.d.cts +3 -3
  35. package/dist/extractors/index.d.ts +3 -3
  36. package/dist/feed/index.d.cts +1 -1
  37. package/dist/feed/index.d.ts +1 -1
  38. package/dist/graph/index.cjs +235 -57
  39. package/dist/graph/index.cjs.map +1 -1
  40. package/dist/graph/index.d.cts +9 -6
  41. package/dist/graph/index.d.ts +9 -6
  42. package/dist/graph/index.mjs +3 -1
  43. package/dist/{hybridGraphPipeline-CocfYe4d.d.cts → hybridGraphPipeline-B4mamLim.d.cts} +1 -1
  44. package/dist/{hybridGraphPipeline-9dRKJj7r.d.ts → hybridGraphPipeline-D2_1GUva.d.ts} +1 -1
  45. package/dist/index.cjs +277 -68
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.cts +9 -8
  48. package/dist/index.d.ts +9 -8
  49. package/dist/index.mjs +11 -9
  50. package/dist/{keywordExtractor-BxlGRMHC.d.cts → keywordExtractor-1YAFntsn.d.cts} +1 -1
  51. package/dist/{keywordExtractor-DU5XRN8-.d.ts → keywordExtractor-DWw8z_AF.d.ts} +1 -1
  52. package/dist/parsers/index.cjs.map +1 -1
  53. package/dist/parsers/index.d.cts +1 -1
  54. package/dist/parsers/index.d.ts +1 -1
  55. package/dist/parsers/index.mjs +1 -1
  56. package/dist/pipeline/index.cjs +261 -68
  57. package/dist/pipeline/index.cjs.map +1 -1
  58. package/dist/pipeline/index.d.cts +10 -9
  59. package/dist/pipeline/index.d.ts +10 -9
  60. package/dist/pipeline/index.mjs +8 -6
  61. package/dist/schemas/index.cjs +8 -0
  62. package/dist/schemas/index.cjs.map +1 -1
  63. package/dist/schemas/index.d.cts +2 -2
  64. package/dist/schemas/index.d.ts +2 -2
  65. package/dist/schemas/index.mjs +1 -1
  66. package/dist/utils/llmClient.cjs +344 -0
  67. package/dist/utils/llmClient.cjs.map +1 -0
  68. package/dist/utils/llmClient.d.cts +118 -0
  69. package/dist/utils/llmClient.d.ts +118 -0
  70. package/dist/utils/llmClient.mjs +4 -0
  71. package/dist/utils/llmClient.mjs.map +1 -0
  72. package/dist/utils/llmTransportResult.cjs +16 -0
  73. package/dist/utils/llmTransportResult.cjs.map +1 -0
  74. package/dist/utils/llmTransportResult.d.cts +36 -0
  75. package/dist/utils/llmTransportResult.d.ts +36 -0
  76. package/dist/utils/llmTransportResult.mjs +3 -0
  77. package/dist/utils/llmTransportResult.mjs.map +1 -0
  78. package/package.json +9 -10
  79. package/dist/chunk-2OGNQUXC.mjs.map +0 -1
  80. package/dist/chunk-5OMSYKQP.mjs.map +0 -1
  81. package/dist/chunk-67GXK3ZE.mjs.map +0 -1
  82. package/dist/chunk-DRVOV5ZD.mjs.map +0 -1
  83. package/dist/chunk-ERDTHXQA.mjs.map +0 -1
  84. package/dist/llmClient-CX5uUiQ1.d.cts +0 -60
  85. package/dist/llmClient-CX5uUiQ1.d.ts +0 -60
  86. package/dist/{curriculumFeedSchema-DYayelAA.d.ts → curriculumFeedSchema-DK-TUAJZ.d.cts} +8 -8
  87. package/dist/{curriculumFeedSchema-DYayelAA.d.cts → curriculumFeedSchema-DK-TUAJZ.d.ts} +8 -8
@@ -4,7 +4,56 @@ var fs = require('fs');
4
4
  var os = require('os');
5
5
  var path = require('path');
6
6
 
7
+ // src/utils/llmTransportResult.ts
8
+ function transportResultFrom(provider, model, options) {
9
+ return {
10
+ provider,
11
+ model,
12
+ framingStripped: !!options?.framingStripped,
13
+ upstreamError: options?.upstreamError,
14
+ usage: options?.usage
15
+ };
16
+ }
17
+
7
18
  // src/utils/llmClient.ts
19
+ async function responseJsonCompat(response) {
20
+ const text = await response.text();
21
+ try {
22
+ return JSON.parse(text);
23
+ } catch {
24
+ const start = text.indexOf("{");
25
+ if (start < 0) {
26
+ throw new LlmClientError(`LLM API returned a non-JSON body: ${text.slice(0, 200)}`);
27
+ }
28
+ let depth = 0;
29
+ let inString = false;
30
+ let escape = false;
31
+ for (let i = start; i < text.length; i++) {
32
+ const ch = text[i];
33
+ if (escape) {
34
+ escape = false;
35
+ continue;
36
+ }
37
+ if (ch === "\\") {
38
+ escape = true;
39
+ continue;
40
+ }
41
+ if (ch === '"') {
42
+ inString = !inString;
43
+ continue;
44
+ }
45
+ if (inString) continue;
46
+ if (ch === "{") depth++;
47
+ if (ch === "}") {
48
+ depth--;
49
+ if (depth === 0) {
50
+ return JSON.parse(text.slice(start, i + 1));
51
+ }
52
+ }
53
+ }
54
+ throw new LlmClientError(`LLM API returned an unterminated JSON body: ${text.slice(0, 200)}`);
55
+ }
56
+ }
8
57
  var LlmClientError = class extends Error {
9
58
  constructor(message, status, cause) {
10
59
  super(message);
@@ -13,28 +62,22 @@ var LlmClientError = class extends Error {
13
62
  this.name = "LlmClientError";
14
63
  }
15
64
  };
16
- function resolveProviderChain(config) {
17
- const chain = [];
18
- const push = (label, baseUrl, apiKey, model) => {
19
- if (apiKey && baseUrl && model && !chain.some((c) => c.baseUrl === baseUrl && c.model === model)) {
20
- chain.push({ label, baseUrl, apiKey, model });
21
- }
22
- };
23
- if (config?.apiKey) {
24
- push("config", config.baseUrl || "https://api.openai.com/v1", config.apiKey, config.model || "gpt-4o-mini");
25
- }
26
- push("legacy", process.env.LLM_BASE_URL || process.env.OPENAI_BASE_URL || "", process.env.LLM_API_KEY || process.env.OPENAI_API_KEY || "", process.env.LLM_MODEL || process.env.LLM_TIER_FAST || "");
27
- push("dashscope", process.env.DASHSCOPE_BASE_URL || process.env.ALIBABA_BASE_URL || "", process.env.DASHSCOPE_API_KEY || process.env.ALIBABA_API_KEY || "", process.env.DASHSCOPE_MODEL || process.env.DEFAULT_AI_MODEL || "");
28
- push("nvidia", "https://integrate.api.nvidia.com/v1", process.env.NVIDIA_API_KEY || "", process.env.NVIDIA_MODEL || "nvidia/nemotron-3-ultra-550b-a55b:free");
29
- push("openrouter", "https://openrouter.ai/api/v1", process.env.OPENROUTER_API_KEY || "", process.env.OPENROUTER_MODEL || "nvidia/nemotron-3-ultra-550b-a55b:free");
30
- if (process.env.OPENROUTER_MODEL) {
31
- const or = chain.find((c) => c.label === "openrouter");
32
- if (or) {
33
- const rest = chain.filter((c) => c !== or);
34
- return [or, ...rest];
35
- }
36
- }
37
- return chain;
65
+ function build9routerProvider(model) {
66
+ const apiKey = process.env.NINEROUTER_API_KEY;
67
+ if (!apiKey) return null;
68
+ const baseUrl = (process.env.NINEROUTER_BASE_URL || "https://ai-router.orchable.app/v1").replace(/^["']|["']$/g, "").trim();
69
+ const label = "9router";
70
+ const modelName = (model || process.env.NINEROUTER_MODEL || "laguna-s-2.1").trim();
71
+ return { label, baseUrl, apiKey, model: modelName };
72
+ }
73
+ function resolve9routerChain(configModel) {
74
+ const primary = build9routerProvider(configModel);
75
+ if (!primary) return [];
76
+ const alt = process.env.NINEROUTER_MODEL_ALT && process.env.NINEROUTER_MODEL_ALT.trim();
77
+ if (!alt) return [primary];
78
+ const secondary = build9routerProvider(alt);
79
+ if (!secondary) return [primary];
80
+ return primary.model === secondary.model ? [primary] : [primary, secondary];
38
81
  }
39
82
  var MIN_REQUEST_INTERVAL_MS = 2500;
40
83
  var requestIntervalMs = Number(process.env.LLM_MIN_REQUEST_INTERVAL_MS || MIN_REQUEST_INTERVAL_MS);
@@ -45,11 +88,11 @@ async function pacedDelay() {
45
88
  lastRequestAt = Date.now();
46
89
  }
47
90
  function createLlmClient(config) {
48
- const chain = resolveProviderChain(config);
91
+ const chain = resolve9routerChain(config?.model);
49
92
  const primary = chain[0];
50
93
  if (!primary) {
51
94
  throw new LlmClientError(
52
- "No LLM provider configured. Set NVIDIA_API_KEY (primary) and/or OPENROUTER_API_KEY (fallback), or pass llmConfig."
95
+ "No 9router provider configured. Set NINEROUTER_API_KEY and NINEROUTER_MODEL (or the NINEROUTER_* env defaults expected by the kit)."
53
96
  );
54
97
  }
55
98
  primary.apiKey;
@@ -131,17 +174,22 @@ function createLlmClient(config) {
131
174
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
132
175
  });
133
176
  if (!retry.ok) throw err;
134
- const retryData = await retry.json();
177
+ const retryData = await responseJsonCompat(retry);
135
178
  const retryContent2 = retryData.choices?.[0]?.message?.content || "";
136
179
  if (!retryContent2.trim()) throw err;
137
180
  callLog[recIdx] = { ...callLog[recIdx], ok: true };
181
+ const transport2 = transportResultFrom(provider.label, retryData.model || provider.model, {
182
+ usage: retryData.usage,
183
+ framingStripped: true
184
+ });
138
185
  return {
139
186
  content: retryContent2,
140
187
  model: retryData.model || provider.model,
141
188
  provider: provider.label,
142
189
  attempts: attempts.slice(),
143
190
  usage: retryData.usage,
144
- finishReason: retryData.choices?.[0]?.finish_reason
191
+ finishReason: retryData.choices?.[0]?.finish_reason,
192
+ _transport: transport2
145
193
  };
146
194
  }
147
195
  if (TRANSIENT.has(response.status)) {
@@ -161,21 +209,26 @@ function createLlmClient(config) {
161
209
  })
162
210
  });
163
211
  if (!retry.ok) throw err;
164
- const retryData = await retry.json();
212
+ const retryData = await responseJsonCompat(retry);
165
213
  const retryContent = retryData.choices?.[0]?.message?.content || "";
166
214
  callLog[recIdx] = { ...callLog[recIdx], ok: true };
215
+ const transport2 = transportResultFrom(provider.label, retryData.model || provider.model, {
216
+ usage: retryData.usage,
217
+ framingStripped: true
218
+ });
167
219
  return {
168
220
  content: retryContent,
169
221
  model: retryData.model || provider.model,
170
222
  provider: provider.label,
171
223
  attempts: attempts.slice(),
172
224
  usage: retryData.usage,
173
- finishReason: retryData.choices?.[0]?.finish_reason
225
+ finishReason: retryData.choices?.[0]?.finish_reason,
226
+ _transport: transport2
174
227
  };
175
228
  }
176
229
  throw err;
177
230
  }
178
- const data = await response.json();
231
+ const data = await responseJsonCompat(response);
179
232
  const wrappedError = data.error;
180
233
  if (wrappedError) {
181
234
  throw new LlmClientError("[" + provider.label + "] upstream error " + (wrappedError.code ?? "") + ": " + (wrappedError.message ?? "unknown"));
@@ -184,13 +237,18 @@ function createLlmClient(config) {
184
237
  if (!content.trim()) {
185
238
  throw new LlmClientError("[" + provider.label + "] empty completion returned");
186
239
  }
240
+ const transport = transportResultFrom(provider.label, data.model || provider.model, {
241
+ usage: data.usage,
242
+ framingStripped: true
243
+ });
187
244
  return {
188
245
  content,
189
246
  model: data.model || provider.model,
190
247
  provider: provider.label,
191
248
  attempts: attempts.slice(),
192
249
  usage: data.usage,
193
- finishReason: data.choices?.[0]?.finish_reason
250
+ finishReason: data.choices?.[0]?.finish_reason,
251
+ _transport: transport
194
252
  };
195
253
  } catch (err) {
196
254
  const msg = err instanceof Error ? err.message : String(err);
@@ -364,7 +422,7 @@ async function extractProjectOverview(options) {
364
422
  (kw) => `- ${kw.keyword} (from ${kw.source})`
365
423
  );
366
424
  if (kwItems.length > 0) {
367
- keywordsAnchor = "\nAST Domain Keywords & Patterns (from static analysis \u2014 use as context anchor):\n" + kwItems.join("\n") + "\n";
425
+ keywordsAnchor = "\nDomain Keywords & Patterns (from regex-based static scan \u2014 use as context anchor):\n" + kwItems.join("\n") + "\n";
368
426
  }
369
427
  }
370
428
  const systemPrompt = "You are a software architect. The app the user will build IS EXACTLY the provided files (not an SDK/lib). Analyze them and return a PROJECT OVERVIEW \u2014 meta only, DO NOT list steps (steps are handled in a later step). NEVER invent files/APIs. NEVER reference internal SDK/lib unless it appears in the given files.\nEND-USER VIEW (M1/Design Thinking): product.purpose must describe the USER PROBLEM (what the user does with the app, which need it solves). NEVER describe repo/artifacts.\nDEVELOPMENT STAGES: Include 4-6 development stages in product.development_stages. Each stage must have: stage (name), need (list of feature needs), learn (concepts learned), product_state (what works), temporary_approach, and validation.\nMISSING GAPS: Identify 0-5 missing_gaps (features/functionality that should exist but don't).\nTECH DEBT: Identify 0-5 tech_debt issues (code quality problems).\nALL text must be in ENGLISH.";
@@ -1380,7 +1438,110 @@ function techSignalHits(text) {
1380
1438
  }
1381
1439
  return hits;
1382
1440
  }
1383
- function auditDepthLayers(concepts) {
1441
+ var COMPLETABLE_VERBS = [
1442
+ "build",
1443
+ "create",
1444
+ "implement",
1445
+ "write",
1446
+ "configure",
1447
+ "add",
1448
+ "use",
1449
+ "apply",
1450
+ "display",
1451
+ "store",
1452
+ "load",
1453
+ "save",
1454
+ "handle",
1455
+ "parse",
1456
+ "validate",
1457
+ "test",
1458
+ "register",
1459
+ "compute",
1460
+ "generate",
1461
+ "print",
1462
+ "format",
1463
+ "convert",
1464
+ "extend",
1465
+ "define",
1466
+ "execute",
1467
+ "run",
1468
+ "read",
1469
+ "fetch",
1470
+ "wrap",
1471
+ "log",
1472
+ "call",
1473
+ "raise",
1474
+ "install",
1475
+ "import",
1476
+ "export",
1477
+ "declare",
1478
+ "embed",
1479
+ "model",
1480
+ "represent",
1481
+ "override",
1482
+ "customize",
1483
+ "enforce",
1484
+ "mock",
1485
+ "assert",
1486
+ "query",
1487
+ "index",
1488
+ "cache",
1489
+ "stream",
1490
+ "split",
1491
+ "join",
1492
+ "strip",
1493
+ "encode",
1494
+ "decode",
1495
+ "traverse",
1496
+ "measure",
1497
+ "evaluate",
1498
+ "benchmark",
1499
+ "diagnose",
1500
+ "demonstrate",
1501
+ "verify",
1502
+ "organize",
1503
+ "structure",
1504
+ "choose",
1505
+ "select",
1506
+ "compare",
1507
+ "distinguish",
1508
+ "attach",
1509
+ "annotate",
1510
+ "expose",
1511
+ "accept",
1512
+ "manage",
1513
+ "group",
1514
+ "sort",
1515
+ "filter",
1516
+ "map",
1517
+ "reduce",
1518
+ "catch",
1519
+ "throw",
1520
+ "migrate",
1521
+ "inspect",
1522
+ "trace",
1523
+ "parameterize",
1524
+ "construct",
1525
+ "design",
1526
+ "capture",
1527
+ // Completable implementation verbs present in production SIO output but
1528
+ // missing from the original registry (2026-10-07 smoke test, swift kg):
1529
+ "update",
1530
+ "inject",
1531
+ "compose",
1532
+ "pass",
1533
+ "modify",
1534
+ "connect",
1535
+ "remove",
1536
+ "transform",
1537
+ "trigger",
1538
+ "refresh",
1539
+ "persist",
1540
+ "observe",
1541
+ "arrange",
1542
+ "derive"
1543
+ ];
1544
+ function auditDepthLayers(concepts, transport) {
1384
1545
  return concepts.map((c) => {
1385
1546
  const ulo = (c.ulo ?? "").trim();
1386
1547
  const cio = (c.cio ?? "").trim();
@@ -1401,7 +1562,7 @@ function auditDepthLayers(concepts) {
1401
1562
  });
1402
1563
  const conceptKeywords = (c.keywords ?? []).map((k) => k.toLowerCase()).filter(Boolean);
1403
1564
  const sioOffConceptItems = conceptKeywords.length > 0 ? sioItems.filter((s) => !boundToConcept(s)) : [];
1404
- const COMPLETABLE = /\b(build|create|implement|write|configure|add|use|apply|display|store|load|save|handle|parse|validate|test|register|compute|generate|print|format|convert|extend|define|execute|run|read|fetch|wrap|log|call|raise|install|import|export|declare|embed|model|represent|override|customize|enforce|mock|assert|query|index|cache|stream|split|join|strip|encode|decode|traverse|measure|evaluate|benchmark|diagnose|demonstrate|verify|organize|structure|choose|select|compare|distinguish|attach|annotate|expose|accept|manage|group|sort|filter|map|reduce|catch|throw|migrate|inspect|trace|parameterize|construct|design|capture)\b/i;
1565
+ const COMPLETABLE = new RegExp("\\b(?:" + COMPLETABLE_VERBS.join("|") + ")\\b", "i");
1405
1566
  const sioNotCompletable = sioItems.filter((s) => !COMPLETABLE.test(s) && !/`[^`]+`/.test(s));
1406
1567
  const density = (text) => {
1407
1568
  if (!text) return 0;
@@ -1441,7 +1602,8 @@ function auditDepthLayers(concepts) {
1441
1602
  clauseOk,
1442
1603
  codesOk,
1443
1604
  codeIssues,
1444
- passed
1605
+ passed,
1606
+ transport
1445
1607
  };
1446
1608
  });
1447
1609
  }
@@ -1451,14 +1613,14 @@ function excerpt(text, max) {
1451
1613
  const t = (text || "").trim();
1452
1614
  return t.length > max ? t.slice(0, max) + "..." : t;
1453
1615
  }
1454
- async function chatJson(client, system, user) {
1616
+ async function chatJson(client, system, user, transport) {
1455
1617
  const r = await client.chat(
1456
1618
  [{ role: "system", content: system }, { role: "user", content: user }],
1457
1619
  { temperature: 0.1, maxTokens: parseInt(process.env.KG_MAX_TOKENS || "65536", 10) }
1458
1620
  );
1459
1621
  return parseJsonFromLlm(r.content);
1460
1622
  }
1461
- var LO_BATCH_SIZE = 6;
1623
+ var LO_BATCH_SIZE = 10;
1462
1624
  function chunkConcepts(items, size) {
1463
1625
  const out = [];
1464
1626
  for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
@@ -1497,7 +1659,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
1497
1659
  for (const batch of chunkConcepts(concepts, LO_BATCH_SIZE)) {
1498
1660
  const batchList = batch.map((c) => "- " + c.id + " (" + c.name + ")" + (c.description ? ": " + excerpt(c.description, 160) : "")).join("\n");
1499
1661
  const batchUser = "Concepts:\n" + batchList + '\n\nReturn JSON:\n{"ulos":[{"concept_id":"C1","ulo":"..."}]}';
1500
- const uloRes = await chatJson(client, uloSystem, batchUser);
1662
+ const uloRes = await chatJson(client, uloSystem, batchUser, transportResultFrom("9router", client.model, { framingStripped: true }));
1501
1663
  for (const u of uloRes.ulos ?? []) {
1502
1664
  if (u.concept_id && u.ulo) uloByConcept[u.concept_id] = standardStatement(u.ulo);
1503
1665
  }
@@ -1536,7 +1698,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
1536
1698
  new Set(batch.map((c) => c.id));
1537
1699
  const batchUloContext = batch.map((c) => "- " + c.id + " (" + c.name + "): " + (uloByConcept[c.id] || "(missing)")).join("\n");
1538
1700
  const batchCioUser = "Approved ULOs:\n" + batchUloContext + '\n\nReturn JSON:\n{"cios":[{"concept_id":"C1","cio":"..."}]}';
1539
- const cioRes = await chatJson(client, cioSystem, batchCioUser);
1701
+ const cioRes = await chatJson(client, cioSystem, batchCioUser, transportResultFrom("9router", client.model, { framingStripped: true }));
1540
1702
  for (const x of cioRes.cios ?? []) {
1541
1703
  if (x.concept_id && x.cio) cioByConcept[x.concept_id] = standardStatement(x.cio);
1542
1704
  }
@@ -1561,15 +1723,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
1561
1723
  " context where it is used. Application/Analysis/Creation skills are assessed",
1562
1724
  " INDIRECTLY through completing these tasks \u2014 there is no fixed Bloom ceiling.",
1563
1725
  " MACHINE CHECK: every item MUST contain at least one ACTION VERB from this whitelist",
1564
- " (build, create, implement, write, configure, add, use, apply, display, store, load,",
1565
- " save, handle, parse, validate, test, register, compute, generate, print, format,",
1566
- " convert, extend, define, execute, run, read, fetch, wrap, log, call, raise, install,",
1567
- " import, export, declare, embed, model, represent, override, customize, enforce, mock,",
1568
- " assert, query, index, cache, stream, split, join, strip, encode, decode, traverse,",
1569
- " measure, evaluate, benchmark, diagnose, demonstrate, verify, organize, structure,",
1570
- " choose, select, compare, distinguish, attach, annotate, expose, accept, manage, group,",
1571
- " sort, filter, map, reduce, catch, throw, migrate, inspect, trace, parameterize,",
1572
- " construct, design, capture) \u2014 OR wrap the concrete construct in backticks.",
1726
+ " (" + COMPLETABLE_VERBS.join(", ") + ") \u2014 OR wrap the concrete construct in backticks.",
1573
1727
  " Items with abstract verbs (understand, learn, know, grasp, appreciate, explore) and",
1574
1728
  " no backtick construct FAIL the audit. Prefer starting the task phrase with the verb.",
1575
1729
  "3. 2-3 items per concept, each referencing a DIFFERENT concrete construct.",
@@ -1597,7 +1751,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
1597
1751
  "Return JSON:",
1598
1752
  '{"sios":[{"concept_id":"C1","sio":["...","..."]}]}'
1599
1753
  ].join("\n");
1600
- const sioRes = await chatJson(client, sioSystem, batchSioUser);
1754
+ const sioRes = await chatJson(client, sioSystem, batchSioUser, transportResultFrom("9router", client.model, { framingStripped: true }));
1601
1755
  for (const x of sioRes.sios ?? []) {
1602
1756
  if (x.concept_id && Array.isArray(x.sio)) sioByConcept[x.concept_id] = x.sio.map((item) => standardStatement(String(item)));
1603
1757
  }
@@ -1650,7 +1804,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
1650
1804
  "- CIO: HOW mechanism, technology-agnostic, more concrete than ULO, Marr 2-Language neutral.",
1651
1805
  "- SIO: keyword-bound completable implementation tasks naming real constructs of " + techStack + ".",
1652
1806
  "- BINDING RULE: every SIO item must contain at least one of the concept's listed keywords VERBATIM (copy the exact keyword text into the sentence).",
1653
- "- COMPLETABILITY RULE: every SIO item must contain an actionable verb (build, create, implement, write, configure, parse, validate, handle, store, load, run, read, fetch, display, generate, format, convert, test, debug, trace, measure, design, structure, select, filter, sort, map, reduce, catch, throw, migrate, deploy, query, index, cache, stream, inspect) or wrap a concrete construct in backticks \u2014 abstract verbs (understand, learn, know, grasp, explore, appreciate) always fail.",
1807
+ "- COMPLETABILITY RULE: every SIO item must contain an actionable verb (" + ["build", "create", "update", "inject", "compose", "pass", "modify", "use"].join(", ") + " ...full registry in the SIO authoring step) or wrap a concrete construct in backticks \u2014 abstract verbs (understand, learn, know, grasp, explore, appreciate) always fail.",
1654
1808
  "",
1655
1809
  JSON_ONLY
1656
1810
  ].join("\n");
@@ -1698,7 +1852,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
1698
1852
  "Depth-layer contract audit failed after repair for: " + detail + " \u2014 regenerate or fix the prompt contract (fail-closed, no silent fallback)"
1699
1853
  );
1700
1854
  }
1701
- return { uloByConcept, cioByConcept, sioByConcept, audits, warnings };
1855
+ return { uloByConcept, cioByConcept, sioByConcept, audits, warnings, lastTransport: audits[0]?.transport };
1702
1856
  }
1703
1857
  function cell(v) {
1704
1858
  return (v || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -1841,7 +1995,7 @@ async function standardizeConcepts(opts) {
1841
1995
  " Adopt the tree's terminology. Only propose new when the tree genuinely has NO concept teaching that knowledge (broader AND narrower both fail).",
1842
1996
  ' 2. "proposed_new" - no tree concept covers it at the same granularity. Propose an UPPER_SNAKE_CASE code (descriptive, stable, max 5 words) and a one-sentence definition.',
1843
1997
  " If between 1 and 3 tree concepts are CLOSE but not exact, list their codes in possible_tree_matches for human review.",
1844
- `For a "standardized" decision ALSO return "adopted_keywords": the subset of that tree concept's keywords (from the shortlist) that make sense for the course TECH STACK. Drop keywords that name tooling from other ecosystems (e.g. JS test runners for a Python course) \u2014 keep conceptual keywords even when spelled generically. Maximum 8, ordered most-relevant first.`,
1998
+ `For a "standardized" decision ALSO return "adopted_keywords": copy VERBATIM from that tree concept's keyword list in the shortlist (exact tokens, do not re-spell, singularize or translate). Pick the subset that makes sense for the course TECH STACK; if none fit, return an EMPTY array \u2014 never invent a keyword. Maximum 8, ordered most-relevant first.`,
1845
1999
  "Rules:",
1846
2000
  "- Judge by MEANING (definition + keywords), not by string similarity.",
1847
2001
  '- COVERAGE RULE: a "standardized" match must cover ALL primary dimensions of the input concept \u2014 every distinct aspect named in its title or description. A tree concept covering only ONE of several aspects (e.g. "interpreting error messages" for a concept about error handling AND user experience) is a PARTIAL match: decide "proposed_new" and list that tree code in possible_tree_matches.',
@@ -1881,10 +2035,25 @@ async function standardizeConcepts(opts) {
1881
2035
  const tc = catalog.concepts.find((x) => x.code === d.tree_code);
1882
2036
  if (!tc) throw new Error("Concept " + c.id + " standardized to unknown tree code " + d.tree_code + " \u2014 fail-closed");
1883
2037
  const treeKw = new Set(tc.keywords.map((k) => k.toLowerCase()));
1884
- const adopted = (d.adopted_keywords || []).map((k) => String(k)).filter((k) => {
1885
- if (treeKw.has(k.toLowerCase())) return true;
1886
- throw new Error("Concept " + c.id + ' adopted keyword "' + k + '" not in tree concept ' + tc.code + " \u2014 fail-closed");
1887
- });
2038
+ const rawAdopted = (d.adopted_keywords || []).map((k) => String(k)).filter(Boolean);
2039
+ let adopted;
2040
+ if (tc.keywords.length === 0) {
2041
+ adopted = rawAdopted.filter((k) => (c.keywords ?? []).some((ck) => ck.toLowerCase() === k.toLowerCase()));
2042
+ if (adopted.length === 0) adopted = (c.keywords ?? []).slice(0, 8);
2043
+ warnings.push("Concept " + c.id + ": tree concept " + tc.code + " has no keyword registry \u2014 adopted " + adopted.length + " keyword(s) from the analysis concept");
2044
+ } else {
2045
+ const norm = (k) => k.toLowerCase().replace(/ies$/, "y").replace(/s$/, "");
2046
+ const treeKwNorm = new Set([...treeKw].map(norm));
2047
+ const dropped = [];
2048
+ adopted = rawAdopted.filter((k) => {
2049
+ if (treeKw.has(k.toLowerCase()) || treeKwNorm.has(norm(k))) return true;
2050
+ dropped.push(k);
2051
+ return false;
2052
+ });
2053
+ if (dropped.length > 0) {
2054
+ warnings.push("Concept " + c.id + ": dropped " + dropped.length + " adopted keyword(s) not in tree concept " + tc.code + " registry: " + dropped.join(", "));
2055
+ }
2056
+ }
1888
2057
  if (tc.keywords.length > 0 && adopted.length === 0) {
1889
2058
  throw new Error("Concept " + c.id + " standardized to " + tc.code + " without adopted_keywords \u2014 fail-closed");
1890
2059
  }
@@ -1979,7 +2148,7 @@ async function generateKnowledgeGraph(options) {
1979
2148
  log("STEP_1", `Parsed syllabus: ${parsedSyllabus.units.length} units, ${parsedSyllabus.allTopics.length} topics`);
1980
2149
  }
1981
2150
  const client = createLlmClient(llmConfig);
1982
- const maxSyllabusChars = parseInt(process.env.KG_MAX_SYLLABUS_CHARS || "16000", 10);
2151
+ const maxSyllabusChars = parseInt(process.env.KG_MAX_SYLLABUS_CHARS || "32000", 10);
1983
2152
  let truncatedSyllabus = "";
1984
2153
  if (syllabusText) {
1985
2154
  if (syllabusText.length > maxSyllabusChars) {
@@ -2169,23 +2338,32 @@ Return JSON:
2169
2338
  }
2170
2339
  }
2171
2340
  const idByTreeCode = new Map(concepts.filter((c) => c.tree_code).map((c) => [c.tree_code, c.id]));
2341
+ const unmappedTreePrereqs = /* @__PURE__ */ new Set();
2172
2342
  for (const c of concepts) {
2173
2343
  for (const tp of c.tree_prerequisites || []) {
2174
2344
  const target = idByTreeCode.get(tp);
2175
2345
  if (target && target !== c.id && !(c.prerequisites || []).includes(target)) {
2176
2346
  c.prerequisites = [...c.prerequisites || [], target];
2177
2347
  warnings.push("Prereq seeded from tree: " + c.id + " <- " + tp + " (" + target + ")");
2348
+ } else if (!target) {
2349
+ unmappedTreePrereqs.add(tp);
2178
2350
  }
2179
2351
  }
2180
2352
  }
2353
+ if (unmappedTreePrereqs.size > 0) {
2354
+ const shown = [...unmappedTreePrereqs].slice(0, 10).join(", ");
2355
+ warnings.push("Tree prereqs outside THIS analysis (no edge seeded, " + unmappedTreePrereqs.size + " codes): " + shown + (unmappedTreePrereqs.size > 10 ? ", ..." : ""));
2356
+ }
2181
2357
  log("STEP_2_5", "Standardized: " + std.decisions.filter((d) => d.decision === "standardized").length + ", proposed new: " + std.decisions.filter((d) => d.decision === "proposed_new").length);
2358
+ } else {
2359
+ warnings.push("No knowledgeTreePath provided \u2014 concepts are NOT standardized against the knowledge tree (all proposed_new)");
2182
2360
  }
2183
2361
  log("STEP_3", "Checking for prerequisite cycles...");
2184
2362
  const cycles = detectPrerequisiteCycles(concepts);
2185
2363
  if (cycles.length > 0) {
2186
2364
  log("STEP_3", `Found ${cycles.length} cycle(s) in prerequisite chain \u2014 breaking cycles`);
2187
2365
  for (const cycle of cycles) {
2188
- warnings.push(`Cycle detected: ${cycle.join(" \u2192 ")} \u2014 breaking at weakest edge`);
2366
+ warnings.push(`Cycle detected: ${cycle.join(" \u2192 ")} \u2014 removing the closing back-edge (last DFS edge; pedagogically arbitrary)`);
2189
2367
  }
2190
2368
  const { fixed, removed } = breakPrerequisiteCycles(concepts, cycles);
2191
2369
  for (let i = 0; i < concepts.length; i++) {