@thanh01.pmt/domain-kit 0.6.0 → 0.6.2

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 (90) 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-7A2SG466.mjs +325 -0
  6. package/dist/chunk-7A2SG466.mjs.map +1 -0
  7. package/dist/{chunk-DRVOV5ZD.mjs → chunk-DSEJLTUO.mjs} +2 -2
  8. package/dist/chunk-DSEJLTUO.mjs.map +1 -0
  9. package/dist/{chunk-2OGNQUXC.mjs → chunk-GVBDM746.mjs} +152 -303
  10. package/dist/chunk-GVBDM746.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-HRSBIVAW.mjs +3 -0
  14. package/dist/{chunk-ZKLT27T4.mjs.map → chunk-HRSBIVAW.mjs.map} +1 -1
  15. package/dist/chunk-IBVXXVGE.mjs +14 -0
  16. package/dist/chunk-IBVXXVGE.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/{chunk-5OMSYKQP.mjs → chunk-ZHMIJEMA.mjs} +33 -18
  20. package/dist/chunk-ZHMIJEMA.mjs.map +1 -0
  21. package/dist/{conceptEscalator-DPrs4PwC.d.cts → conceptEscalator-BmMJD8C5.d.cts} +1 -1
  22. package/dist/{conceptEscalator-BCsw0ys_.d.ts → conceptEscalator-DMsehscs.d.ts} +1 -1
  23. package/dist/concepts/index.d.cts +4 -3
  24. package/dist/concepts/index.d.ts +4 -3
  25. package/dist/{cpp-CZUPqJt9.d.cts → cpp-vlDl6pCL.d.cts} +4 -4
  26. package/dist/{cpp-CZUPqJt9.d.ts → cpp-vlDl6pCL.d.ts} +4 -4
  27. package/dist/detector/index.cjs +1 -0
  28. package/dist/detector/index.cjs.map +1 -1
  29. package/dist/detector/index.d.cts +2 -2
  30. package/dist/detector/index.d.ts +2 -2
  31. package/dist/detector/index.mjs +1 -1
  32. package/dist/{domainProfileDetector-DHb1T0zB.d.ts → domainProfileDetector-6pjgL-6_.d.ts} +1 -1
  33. package/dist/{domainProfileDetector-bzugdQmS.d.cts → domainProfileDetector-BsADsuRV.d.cts} +1 -1
  34. package/dist/{domainProfileSchema-CwT3Ffsw.d.cts → domainProfileSchema-779FGk14.d.cts} +38 -1
  35. package/dist/{domainProfileSchema-CwT3Ffsw.d.ts → domainProfileSchema-779FGk14.d.ts} +38 -1
  36. package/dist/extractors/index.d.cts +3 -3
  37. package/dist/extractors/index.d.ts +3 -3
  38. package/dist/feed/index.d.cts +1 -1
  39. package/dist/feed/index.d.ts +1 -1
  40. package/dist/graph/index.cjs +236 -57
  41. package/dist/graph/index.cjs.map +1 -1
  42. package/dist/graph/index.d.cts +27 -7
  43. package/dist/graph/index.d.ts +27 -7
  44. package/dist/graph/index.mjs +4 -2
  45. package/dist/{hybridGraphPipeline-CocfYe4d.d.cts → hybridGraphPipeline-B4mamLim.d.cts} +1 -1
  46. package/dist/{hybridGraphPipeline-9dRKJj7r.d.ts → hybridGraphPipeline-D2_1GUva.d.ts} +1 -1
  47. package/dist/index.cjs +278 -68
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.cts +10 -9
  50. package/dist/index.d.ts +10 -9
  51. package/dist/index.mjs +12 -10
  52. package/dist/{keywordExtractor-BxlGRMHC.d.cts → keywordExtractor-1YAFntsn.d.cts} +1 -1
  53. package/dist/{keywordExtractor-DU5XRN8-.d.ts → keywordExtractor-DWw8z_AF.d.ts} +1 -1
  54. package/dist/parsers/index.cjs.map +1 -1
  55. package/dist/parsers/index.d.cts +1 -1
  56. package/dist/parsers/index.d.ts +1 -1
  57. package/dist/parsers/index.mjs +1 -1
  58. package/dist/pipeline/index.cjs +261 -68
  59. package/dist/pipeline/index.cjs.map +1 -1
  60. package/dist/pipeline/index.d.cts +10 -9
  61. package/dist/pipeline/index.d.ts +10 -9
  62. package/dist/pipeline/index.mjs +8 -6
  63. package/dist/schemas/index.cjs +8 -0
  64. package/dist/schemas/index.cjs.map +1 -1
  65. package/dist/schemas/index.d.cts +2 -2
  66. package/dist/schemas/index.d.ts +2 -2
  67. package/dist/schemas/index.mjs +1 -1
  68. package/dist/utils/llmClient.cjs +344 -0
  69. package/dist/utils/llmClient.cjs.map +1 -0
  70. package/dist/utils/llmClient.d.cts +118 -0
  71. package/dist/utils/llmClient.d.ts +118 -0
  72. package/dist/utils/llmClient.mjs +4 -0
  73. package/dist/utils/llmClient.mjs.map +1 -0
  74. package/dist/utils/llmTransportResult.cjs +16 -0
  75. package/dist/utils/llmTransportResult.cjs.map +1 -0
  76. package/dist/utils/llmTransportResult.d.cts +36 -0
  77. package/dist/utils/llmTransportResult.d.ts +36 -0
  78. package/dist/utils/llmTransportResult.mjs +3 -0
  79. package/dist/utils/llmTransportResult.mjs.map +1 -0
  80. package/package.json +9 -10
  81. package/dist/chunk-2OGNQUXC.mjs.map +0 -1
  82. package/dist/chunk-5OMSYKQP.mjs.map +0 -1
  83. package/dist/chunk-67GXK3ZE.mjs.map +0 -1
  84. package/dist/chunk-DRVOV5ZD.mjs.map +0 -1
  85. package/dist/chunk-ERDTHXQA.mjs.map +0 -1
  86. package/dist/chunk-ZKLT27T4.mjs +0 -3
  87. package/dist/llmClient-CX5uUiQ1.d.cts +0 -60
  88. package/dist/llmClient-CX5uUiQ1.d.ts +0 -60
  89. package/dist/{curriculumFeedSchema-DYayelAA.d.ts → curriculumFeedSchema-DK-TUAJZ.d.cts} +8 -8
  90. package/dist/{curriculumFeedSchema-DYayelAA.d.cts → curriculumFeedSchema-DK-TUAJZ.d.ts} +8 -8
@@ -526,6 +526,17 @@ function extractKeywords(sourceContext, basicAnalysis, repoDir, options) {
526
526
  return uniqueKeywords;
527
527
  }
528
528
 
529
+ // src/utils/llmTransportResult.ts
530
+ function transportResultFrom(provider, model, options) {
531
+ return {
532
+ provider,
533
+ model,
534
+ framingStripped: !!options?.framingStripped,
535
+ upstreamError: options?.upstreamError,
536
+ usage: options?.usage
537
+ };
538
+ }
539
+
529
540
  // src/utils/llmClient.ts
530
541
  function summarizeProvenance(callLog) {
531
542
  const providers = [];
@@ -538,6 +549,44 @@ function summarizeProvenance(callLog) {
538
549
  }
539
550
  return { providers, models, calls: callLog.length, failed_calls: failed, degraded: failed > 0 };
540
551
  }
552
+ async function responseJsonCompat(response) {
553
+ const text = await response.text();
554
+ try {
555
+ return JSON.parse(text);
556
+ } catch {
557
+ const start = text.indexOf("{");
558
+ if (start < 0) {
559
+ throw new LlmClientError(`LLM API returned a non-JSON body: ${text.slice(0, 200)}`);
560
+ }
561
+ let depth = 0;
562
+ let inString = false;
563
+ let escape = false;
564
+ for (let i = start; i < text.length; i++) {
565
+ const ch = text[i];
566
+ if (escape) {
567
+ escape = false;
568
+ continue;
569
+ }
570
+ if (ch === "\\") {
571
+ escape = true;
572
+ continue;
573
+ }
574
+ if (ch === '"') {
575
+ inString = !inString;
576
+ continue;
577
+ }
578
+ if (inString) continue;
579
+ if (ch === "{") depth++;
580
+ if (ch === "}") {
581
+ depth--;
582
+ if (depth === 0) {
583
+ return JSON.parse(text.slice(start, i + 1));
584
+ }
585
+ }
586
+ }
587
+ throw new LlmClientError(`LLM API returned an unterminated JSON body: ${text.slice(0, 200)}`);
588
+ }
589
+ }
541
590
  var LlmClientError = class extends Error {
542
591
  constructor(message, status, cause) {
543
592
  super(message);
@@ -546,28 +595,22 @@ var LlmClientError = class extends Error {
546
595
  this.name = "LlmClientError";
547
596
  }
548
597
  };
549
- function resolveProviderChain(config) {
550
- const chain = [];
551
- const push = (label, baseUrl, apiKey, model) => {
552
- if (apiKey && baseUrl && model && !chain.some((c) => c.baseUrl === baseUrl && c.model === model)) {
553
- chain.push({ label, baseUrl, apiKey, model });
554
- }
555
- };
556
- if (config?.apiKey) {
557
- push("config", config.baseUrl || "https://api.openai.com/v1", config.apiKey, config.model || "gpt-4o-mini");
558
- }
559
- 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 || "");
560
- 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 || "");
561
- push("nvidia", "https://integrate.api.nvidia.com/v1", process.env.NVIDIA_API_KEY || "", process.env.NVIDIA_MODEL || "nvidia/nemotron-3-ultra-550b-a55b:free");
562
- push("openrouter", "https://openrouter.ai/api/v1", process.env.OPENROUTER_API_KEY || "", process.env.OPENROUTER_MODEL || "nvidia/nemotron-3-ultra-550b-a55b:free");
563
- if (process.env.OPENROUTER_MODEL) {
564
- const or = chain.find((c) => c.label === "openrouter");
565
- if (or) {
566
- const rest = chain.filter((c) => c !== or);
567
- return [or, ...rest];
568
- }
569
- }
570
- return chain;
598
+ function build9routerProvider(model) {
599
+ const apiKey = process.env.NINEROUTER_API_KEY;
600
+ if (!apiKey) return null;
601
+ const baseUrl = (process.env.NINEROUTER_BASE_URL || "https://ai-router.orchable.app/v1").replace(/^["']|["']$/g, "").trim();
602
+ const label = "9router";
603
+ const modelName = (model || process.env.NINEROUTER_MODEL || "laguna-s-2.1").trim();
604
+ return { label, baseUrl, apiKey, model: modelName };
605
+ }
606
+ function resolve9routerChain(configModel) {
607
+ const primary = build9routerProvider(configModel);
608
+ if (!primary) return [];
609
+ const alt = process.env.NINEROUTER_MODEL_ALT && process.env.NINEROUTER_MODEL_ALT.trim();
610
+ if (!alt) return [primary];
611
+ const secondary = build9routerProvider(alt);
612
+ if (!secondary) return [primary];
613
+ return primary.model === secondary.model ? [primary] : [primary, secondary];
571
614
  }
572
615
  var MIN_REQUEST_INTERVAL_MS = 2500;
573
616
  var requestIntervalMs = Number(process.env.LLM_MIN_REQUEST_INTERVAL_MS || MIN_REQUEST_INTERVAL_MS);
@@ -578,11 +621,11 @@ async function pacedDelay() {
578
621
  lastRequestAt = Date.now();
579
622
  }
580
623
  function createLlmClient(config) {
581
- const chain = resolveProviderChain(config);
624
+ const chain = resolve9routerChain(config?.model);
582
625
  const primary = chain[0];
583
626
  if (!primary) {
584
627
  throw new LlmClientError(
585
- "No LLM provider configured. Set NVIDIA_API_KEY (primary) and/or OPENROUTER_API_KEY (fallback), or pass llmConfig."
628
+ "No 9router provider configured. Set NINEROUTER_API_KEY and NINEROUTER_MODEL (or the NINEROUTER_* env defaults expected by the kit)."
586
629
  );
587
630
  }
588
631
  primary.apiKey;
@@ -664,17 +707,22 @@ function createLlmClient(config) {
664
707
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
665
708
  });
666
709
  if (!retry.ok) throw err;
667
- const retryData = await retry.json();
710
+ const retryData = await responseJsonCompat(retry);
668
711
  const retryContent2 = retryData.choices?.[0]?.message?.content || "";
669
712
  if (!retryContent2.trim()) throw err;
670
713
  callLog[recIdx] = { ...callLog[recIdx], ok: true };
714
+ const transport2 = transportResultFrom(provider.label, retryData.model || provider.model, {
715
+ usage: retryData.usage,
716
+ framingStripped: true
717
+ });
671
718
  return {
672
719
  content: retryContent2,
673
720
  model: retryData.model || provider.model,
674
721
  provider: provider.label,
675
722
  attempts: attempts.slice(),
676
723
  usage: retryData.usage,
677
- finishReason: retryData.choices?.[0]?.finish_reason
724
+ finishReason: retryData.choices?.[0]?.finish_reason,
725
+ _transport: transport2
678
726
  };
679
727
  }
680
728
  if (TRANSIENT.has(response.status)) {
@@ -694,21 +742,26 @@ function createLlmClient(config) {
694
742
  })
695
743
  });
696
744
  if (!retry.ok) throw err;
697
- const retryData = await retry.json();
745
+ const retryData = await responseJsonCompat(retry);
698
746
  const retryContent = retryData.choices?.[0]?.message?.content || "";
699
747
  callLog[recIdx] = { ...callLog[recIdx], ok: true };
748
+ const transport2 = transportResultFrom(provider.label, retryData.model || provider.model, {
749
+ usage: retryData.usage,
750
+ framingStripped: true
751
+ });
700
752
  return {
701
753
  content: retryContent,
702
754
  model: retryData.model || provider.model,
703
755
  provider: provider.label,
704
756
  attempts: attempts.slice(),
705
757
  usage: retryData.usage,
706
- finishReason: retryData.choices?.[0]?.finish_reason
758
+ finishReason: retryData.choices?.[0]?.finish_reason,
759
+ _transport: transport2
707
760
  };
708
761
  }
709
762
  throw err;
710
763
  }
711
- const data = await response.json();
764
+ const data = await responseJsonCompat(response);
712
765
  const wrappedError = data.error;
713
766
  if (wrappedError) {
714
767
  throw new LlmClientError("[" + provider.label + "] upstream error " + (wrappedError.code ?? "") + ": " + (wrappedError.message ?? "unknown"));
@@ -717,13 +770,18 @@ function createLlmClient(config) {
717
770
  if (!content.trim()) {
718
771
  throw new LlmClientError("[" + provider.label + "] empty completion returned");
719
772
  }
773
+ const transport = transportResultFrom(provider.label, data.model || provider.model, {
774
+ usage: data.usage,
775
+ framingStripped: true
776
+ });
720
777
  return {
721
778
  content,
722
779
  model: data.model || provider.model,
723
780
  provider: provider.label,
724
781
  attempts: attempts.slice(),
725
782
  usage: data.usage,
726
- finishReason: data.choices?.[0]?.finish_reason
783
+ finishReason: data.choices?.[0]?.finish_reason,
784
+ _transport: transport
727
785
  };
728
786
  } catch (err) {
729
787
  const msg = err instanceof Error ? err.message : String(err);
@@ -897,7 +955,7 @@ async function extractProjectOverview(options) {
897
955
  (kw) => `- ${kw.keyword} (from ${kw.source})`
898
956
  );
899
957
  if (kwItems.length > 0) {
900
- keywordsAnchor = "\nAST Domain Keywords & Patterns (from static analysis \u2014 use as context anchor):\n" + kwItems.join("\n") + "\n";
958
+ keywordsAnchor = "\nDomain Keywords & Patterns (from regex-based static scan \u2014 use as context anchor):\n" + kwItems.join("\n") + "\n";
901
959
  }
902
960
  }
903
961
  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.";
@@ -1946,7 +2004,7 @@ async function runProjectGraphPipeline(options) {
1946
2004
  const filePaths = selectedPaths;
1947
2005
  const primaryLang = detectPrimaryLanguage(filePaths);
1948
2006
  log("STEP_1", `Primary language: ${primaryLang}, ${filePaths.length} files loaded (${fileContentsMap.size} with content)`);
1949
- log("STEP_1", "Parsing source files with AST-level analysis...");
2007
+ log("STEP_1", "Parsing source files with regex-based scan (no AST library)...");
1950
2008
  const allParseResults = [];
1951
2009
  for (const [relPath, content] of fileContentsMap) {
1952
2010
  const fullPath = relPath;
@@ -1970,7 +2028,7 @@ async function runProjectGraphPipeline(options) {
1970
2028
  } else {
1971
2029
  mergedContext = mergeCppResults(allParseResults.filter((r) => "error_handling" in r && !("docstrings" in r) && !("property_wrappers" in r)));
1972
2030
  }
1973
- log("STEP_2", "Extracting keywords from AST analysis...");
2031
+ log("STEP_2", "Extracting keywords from regex-based source scan...");
1974
2032
  const astKeywords = extractKeywords(mergedContext);
1975
2033
  log("STEP_2", `Extracted ${astKeywords.length} keywords`);
1976
2034
  log("STEP_3", "Building SDK API index...");
@@ -2710,7 +2768,110 @@ function techSignalHits(text) {
2710
2768
  }
2711
2769
  return hits;
2712
2770
  }
2713
- function auditDepthLayers(concepts) {
2771
+ var COMPLETABLE_VERBS = [
2772
+ "build",
2773
+ "create",
2774
+ "implement",
2775
+ "write",
2776
+ "configure",
2777
+ "add",
2778
+ "use",
2779
+ "apply",
2780
+ "display",
2781
+ "store",
2782
+ "load",
2783
+ "save",
2784
+ "handle",
2785
+ "parse",
2786
+ "validate",
2787
+ "test",
2788
+ "register",
2789
+ "compute",
2790
+ "generate",
2791
+ "print",
2792
+ "format",
2793
+ "convert",
2794
+ "extend",
2795
+ "define",
2796
+ "execute",
2797
+ "run",
2798
+ "read",
2799
+ "fetch",
2800
+ "wrap",
2801
+ "log",
2802
+ "call",
2803
+ "raise",
2804
+ "install",
2805
+ "import",
2806
+ "export",
2807
+ "declare",
2808
+ "embed",
2809
+ "model",
2810
+ "represent",
2811
+ "override",
2812
+ "customize",
2813
+ "enforce",
2814
+ "mock",
2815
+ "assert",
2816
+ "query",
2817
+ "index",
2818
+ "cache",
2819
+ "stream",
2820
+ "split",
2821
+ "join",
2822
+ "strip",
2823
+ "encode",
2824
+ "decode",
2825
+ "traverse",
2826
+ "measure",
2827
+ "evaluate",
2828
+ "benchmark",
2829
+ "diagnose",
2830
+ "demonstrate",
2831
+ "verify",
2832
+ "organize",
2833
+ "structure",
2834
+ "choose",
2835
+ "select",
2836
+ "compare",
2837
+ "distinguish",
2838
+ "attach",
2839
+ "annotate",
2840
+ "expose",
2841
+ "accept",
2842
+ "manage",
2843
+ "group",
2844
+ "sort",
2845
+ "filter",
2846
+ "map",
2847
+ "reduce",
2848
+ "catch",
2849
+ "throw",
2850
+ "migrate",
2851
+ "inspect",
2852
+ "trace",
2853
+ "parameterize",
2854
+ "construct",
2855
+ "design",
2856
+ "capture",
2857
+ // Completable implementation verbs present in production SIO output but
2858
+ // missing from the original registry (2026-10-07 smoke test, swift kg):
2859
+ "update",
2860
+ "inject",
2861
+ "compose",
2862
+ "pass",
2863
+ "modify",
2864
+ "connect",
2865
+ "remove",
2866
+ "transform",
2867
+ "trigger",
2868
+ "refresh",
2869
+ "persist",
2870
+ "observe",
2871
+ "arrange",
2872
+ "derive"
2873
+ ];
2874
+ function auditDepthLayers(concepts, transport) {
2714
2875
  return concepts.map((c) => {
2715
2876
  const ulo = (c.ulo ?? "").trim();
2716
2877
  const cio = (c.cio ?? "").trim();
@@ -2731,7 +2892,7 @@ function auditDepthLayers(concepts) {
2731
2892
  });
2732
2893
  const conceptKeywords = (c.keywords ?? []).map((k) => k.toLowerCase()).filter(Boolean);
2733
2894
  const sioOffConceptItems = conceptKeywords.length > 0 ? sioItems.filter((s) => !boundToConcept(s)) : [];
2734
- 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;
2895
+ const COMPLETABLE = new RegExp("\\b(?:" + COMPLETABLE_VERBS.join("|") + ")\\b", "i");
2735
2896
  const sioNotCompletable = sioItems.filter((s) => !COMPLETABLE.test(s) && !/`[^`]+`/.test(s));
2736
2897
  const density = (text) => {
2737
2898
  if (!text) return 0;
@@ -2771,7 +2932,8 @@ function auditDepthLayers(concepts) {
2771
2932
  clauseOk,
2772
2933
  codesOk,
2773
2934
  codeIssues,
2774
- passed
2935
+ passed,
2936
+ transport
2775
2937
  };
2776
2938
  });
2777
2939
  }
@@ -2781,14 +2943,14 @@ function excerpt(text, max) {
2781
2943
  const t = (text || "").trim();
2782
2944
  return t.length > max ? t.slice(0, max) + "..." : t;
2783
2945
  }
2784
- async function chatJson(client, system, user) {
2946
+ async function chatJson(client, system, user, transport) {
2785
2947
  const r = await client.chat(
2786
2948
  [{ role: "system", content: system }, { role: "user", content: user }],
2787
2949
  { temperature: 0.1, maxTokens: parseInt(process.env.KG_MAX_TOKENS || "65536", 10) }
2788
2950
  );
2789
2951
  return parseJsonFromLlm(r.content);
2790
2952
  }
2791
- var LO_BATCH_SIZE = 6;
2953
+ var LO_BATCH_SIZE = 10;
2792
2954
  function chunkConcepts(items, size) {
2793
2955
  const out = [];
2794
2956
  for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
@@ -2827,7 +2989,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
2827
2989
  for (const batch of chunkConcepts(concepts, LO_BATCH_SIZE)) {
2828
2990
  const batchList = batch.map((c) => "- " + c.id + " (" + c.name + ")" + (c.description ? ": " + excerpt(c.description, 160) : "")).join("\n");
2829
2991
  const batchUser = "Concepts:\n" + batchList + '\n\nReturn JSON:\n{"ulos":[{"concept_id":"C1","ulo":"..."}]}';
2830
- const uloRes = await chatJson(client, uloSystem, batchUser);
2992
+ const uloRes = await chatJson(client, uloSystem, batchUser, transportResultFrom("9router", client.model, { framingStripped: true }));
2831
2993
  for (const u of uloRes.ulos ?? []) {
2832
2994
  if (u.concept_id && u.ulo) uloByConcept[u.concept_id] = standardStatement(u.ulo);
2833
2995
  }
@@ -2866,7 +3028,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
2866
3028
  new Set(batch.map((c) => c.id));
2867
3029
  const batchUloContext = batch.map((c) => "- " + c.id + " (" + c.name + "): " + (uloByConcept[c.id] || "(missing)")).join("\n");
2868
3030
  const batchCioUser = "Approved ULOs:\n" + batchUloContext + '\n\nReturn JSON:\n{"cios":[{"concept_id":"C1","cio":"..."}]}';
2869
- const cioRes = await chatJson(client, cioSystem, batchCioUser);
3031
+ const cioRes = await chatJson(client, cioSystem, batchCioUser, transportResultFrom("9router", client.model, { framingStripped: true }));
2870
3032
  for (const x of cioRes.cios ?? []) {
2871
3033
  if (x.concept_id && x.cio) cioByConcept[x.concept_id] = standardStatement(x.cio);
2872
3034
  }
@@ -2891,15 +3053,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
2891
3053
  " context where it is used. Application/Analysis/Creation skills are assessed",
2892
3054
  " INDIRECTLY through completing these tasks \u2014 there is no fixed Bloom ceiling.",
2893
3055
  " MACHINE CHECK: every item MUST contain at least one ACTION VERB from this whitelist",
2894
- " (build, create, implement, write, configure, add, use, apply, display, store, load,",
2895
- " save, handle, parse, validate, test, register, compute, generate, print, format,",
2896
- " convert, extend, define, execute, run, read, fetch, wrap, log, call, raise, install,",
2897
- " import, export, declare, embed, model, represent, override, customize, enforce, mock,",
2898
- " assert, query, index, cache, stream, split, join, strip, encode, decode, traverse,",
2899
- " measure, evaluate, benchmark, diagnose, demonstrate, verify, organize, structure,",
2900
- " choose, select, compare, distinguish, attach, annotate, expose, accept, manage, group,",
2901
- " sort, filter, map, reduce, catch, throw, migrate, inspect, trace, parameterize,",
2902
- " construct, design, capture) \u2014 OR wrap the concrete construct in backticks.",
3056
+ " (" + COMPLETABLE_VERBS.join(", ") + ") \u2014 OR wrap the concrete construct in backticks.",
2903
3057
  " Items with abstract verbs (understand, learn, know, grasp, appreciate, explore) and",
2904
3058
  " no backtick construct FAIL the audit. Prefer starting the task phrase with the verb.",
2905
3059
  "3. 2-3 items per concept, each referencing a DIFFERENT concrete construct.",
@@ -2927,7 +3081,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
2927
3081
  "Return JSON:",
2928
3082
  '{"sios":[{"concept_id":"C1","sio":["...","..."]}]}'
2929
3083
  ].join("\n");
2930
- const sioRes = await chatJson(client, sioSystem, batchSioUser);
3084
+ const sioRes = await chatJson(client, sioSystem, batchSioUser, transportResultFrom("9router", client.model, { framingStripped: true }));
2931
3085
  for (const x of sioRes.sios ?? []) {
2932
3086
  if (x.concept_id && Array.isArray(x.sio)) sioByConcept[x.concept_id] = x.sio.map((item) => standardStatement(String(item)));
2933
3087
  }
@@ -2980,7 +3134,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
2980
3134
  "- CIO: HOW mechanism, technology-agnostic, more concrete than ULO, Marr 2-Language neutral.",
2981
3135
  "- SIO: keyword-bound completable implementation tasks naming real constructs of " + techStack + ".",
2982
3136
  "- 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).",
2983
- "- 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.",
3137
+ "- 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.",
2984
3138
  "",
2985
3139
  JSON_ONLY
2986
3140
  ].join("\n");
@@ -3028,7 +3182,7 @@ async function authorDepthLayersHierarchically(client, concepts, techStack, warn
3028
3182
  "Depth-layer contract audit failed after repair for: " + detail + " \u2014 regenerate or fix the prompt contract (fail-closed, no silent fallback)"
3029
3183
  );
3030
3184
  }
3031
- return { uloByConcept, cioByConcept, sioByConcept, audits, warnings };
3185
+ return { uloByConcept, cioByConcept, sioByConcept, audits, warnings, lastTransport: audits[0]?.transport };
3032
3186
  }
3033
3187
  function cell(v) {
3034
3188
  return (v || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -3171,7 +3325,7 @@ async function standardizeConcepts(opts) {
3171
3325
  " Adopt the tree's terminology. Only propose new when the tree genuinely has NO concept teaching that knowledge (broader AND narrower both fail).",
3172
3326
  ' 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.',
3173
3327
  " If between 1 and 3 tree concepts are CLOSE but not exact, list their codes in possible_tree_matches for human review.",
3174
- `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.`,
3328
+ `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.`,
3175
3329
  "Rules:",
3176
3330
  "- Judge by MEANING (definition + keywords), not by string similarity.",
3177
3331
  '- 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.',
@@ -3211,10 +3365,25 @@ async function standardizeConcepts(opts) {
3211
3365
  const tc = catalog.concepts.find((x) => x.code === d.tree_code);
3212
3366
  if (!tc) throw new Error("Concept " + c.id + " standardized to unknown tree code " + d.tree_code + " \u2014 fail-closed");
3213
3367
  const treeKw = new Set(tc.keywords.map((k) => k.toLowerCase()));
3214
- const adopted = (d.adopted_keywords || []).map((k) => String(k)).filter((k) => {
3215
- if (treeKw.has(k.toLowerCase())) return true;
3216
- throw new Error("Concept " + c.id + ' adopted keyword "' + k + '" not in tree concept ' + tc.code + " \u2014 fail-closed");
3217
- });
3368
+ const rawAdopted = (d.adopted_keywords || []).map((k) => String(k)).filter(Boolean);
3369
+ let adopted;
3370
+ if (tc.keywords.length === 0) {
3371
+ adopted = rawAdopted.filter((k) => (c.keywords ?? []).some((ck) => ck.toLowerCase() === k.toLowerCase()));
3372
+ if (adopted.length === 0) adopted = (c.keywords ?? []).slice(0, 8);
3373
+ warnings.push("Concept " + c.id + ": tree concept " + tc.code + " has no keyword registry \u2014 adopted " + adopted.length + " keyword(s) from the analysis concept");
3374
+ } else {
3375
+ const norm = (k) => k.toLowerCase().replace(/ies$/, "y").replace(/s$/, "");
3376
+ const treeKwNorm = new Set([...treeKw].map(norm));
3377
+ const dropped = [];
3378
+ adopted = rawAdopted.filter((k) => {
3379
+ if (treeKw.has(k.toLowerCase()) || treeKwNorm.has(norm(k))) return true;
3380
+ dropped.push(k);
3381
+ return false;
3382
+ });
3383
+ if (dropped.length > 0) {
3384
+ warnings.push("Concept " + c.id + ": dropped " + dropped.length + " adopted keyword(s) not in tree concept " + tc.code + " registry: " + dropped.join(", "));
3385
+ }
3386
+ }
3218
3387
  if (tc.keywords.length > 0 && adopted.length === 0) {
3219
3388
  throw new Error("Concept " + c.id + " standardized to " + tc.code + " without adopted_keywords \u2014 fail-closed");
3220
3389
  }
@@ -3309,7 +3478,7 @@ async function generateKnowledgeGraph(options) {
3309
3478
  log("STEP_1", `Parsed syllabus: ${parsedSyllabus.units.length} units, ${parsedSyllabus.allTopics.length} topics`);
3310
3479
  }
3311
3480
  const client = createLlmClient(llmConfig);
3312
- const maxSyllabusChars = parseInt(process.env.KG_MAX_SYLLABUS_CHARS || "16000", 10);
3481
+ const maxSyllabusChars = parseInt(process.env.KG_MAX_SYLLABUS_CHARS || "32000", 10);
3313
3482
  let truncatedSyllabus = "";
3314
3483
  if (syllabusText) {
3315
3484
  if (syllabusText.length > maxSyllabusChars) {
@@ -3499,23 +3668,32 @@ Return JSON:
3499
3668
  }
3500
3669
  }
3501
3670
  const idByTreeCode = new Map(concepts.filter((c) => c.tree_code).map((c) => [c.tree_code, c.id]));
3671
+ const unmappedTreePrereqs = /* @__PURE__ */ new Set();
3502
3672
  for (const c of concepts) {
3503
3673
  for (const tp of c.tree_prerequisites || []) {
3504
3674
  const target = idByTreeCode.get(tp);
3505
3675
  if (target && target !== c.id && !(c.prerequisites || []).includes(target)) {
3506
3676
  c.prerequisites = [...c.prerequisites || [], target];
3507
3677
  warnings.push("Prereq seeded from tree: " + c.id + " <- " + tp + " (" + target + ")");
3678
+ } else if (!target) {
3679
+ unmappedTreePrereqs.add(tp);
3508
3680
  }
3509
3681
  }
3510
3682
  }
3683
+ if (unmappedTreePrereqs.size > 0) {
3684
+ const shown = [...unmappedTreePrereqs].slice(0, 10).join(", ");
3685
+ warnings.push("Tree prereqs outside THIS analysis (no edge seeded, " + unmappedTreePrereqs.size + " codes): " + shown + (unmappedTreePrereqs.size > 10 ? ", ..." : ""));
3686
+ }
3511
3687
  log("STEP_2_5", "Standardized: " + std.decisions.filter((d) => d.decision === "standardized").length + ", proposed new: " + std.decisions.filter((d) => d.decision === "proposed_new").length);
3688
+ } else {
3689
+ warnings.push("No knowledgeTreePath provided \u2014 concepts are NOT standardized against the knowledge tree (all proposed_new)");
3512
3690
  }
3513
3691
  log("STEP_3", "Checking for prerequisite cycles...");
3514
3692
  const cycles = detectPrerequisiteCycles(concepts);
3515
3693
  if (cycles.length > 0) {
3516
3694
  log("STEP_3", `Found ${cycles.length} cycle(s) in prerequisite chain \u2014 breaking cycles`);
3517
3695
  for (const cycle of cycles) {
3518
- warnings.push(`Cycle detected: ${cycle.join(" \u2192 ")} \u2014 breaking at weakest edge`);
3696
+ warnings.push(`Cycle detected: ${cycle.join(" \u2192 ")} \u2014 removing the closing back-edge (last DFS edge; pedagogically arbitrary)`);
3519
3697
  }
3520
3698
  const { fixed, removed } = breakPrerequisiteCycles(concepts, cycles);
3521
3699
  for (let i = 0; i < concepts.length; i++) {
@@ -4432,6 +4610,15 @@ function emitCurriculumFeed(graph, opts = {}) {
4432
4610
  return emitFromProject(g, opts);
4433
4611
  }
4434
4612
 
4613
+ // src/pipeline/emitFeed.ts
4614
+ function emitNormalizedFeed(req) {
4615
+ return emitCurriculumFeed(req.graph, {
4616
+ warnings: req.warnings,
4617
+ hallucinationCount: req.hallucinationCount,
4618
+ provenance: req.provenance ? summarizeProvenance(req.provenance) : void 0
4619
+ });
4620
+ }
4621
+
4435
4622
  // src/pipeline/knowledgeGraphPipeline.ts
4436
4623
  async function runKnowledgeGraphPipeline(options) {
4437
4624
  const warnings = [];
@@ -4452,9 +4639,10 @@ async function runKnowledgeGraphPipeline(options) {
4452
4639
  parsedSyllabus
4453
4640
  });
4454
4641
  warnings.push(...result.warnings);
4455
- const curriculumFeed = emitCurriculumFeed(result.knowledgeGraph, {
4642
+ const curriculumFeed = emitNormalizedFeed({
4643
+ graph: result.knowledgeGraph,
4456
4644
  warnings,
4457
- provenance: summarizeProvenance(result.provenance)
4645
+ provenance: result.provenance
4458
4646
  });
4459
4647
  return {
4460
4648
  knowledgeGraph: result.knowledgeGraph,
@@ -4770,9 +4958,10 @@ async function runHybridGraphPipeline(options) {
4770
4958
  const warnings = [];
4771
4959
  const result = await generateHybridGraph(options);
4772
4960
  warnings.push(...result.warnings);
4773
- const curriculumFeed = emitCurriculumFeed(result.hybridGraph, {
4961
+ const curriculumFeed = emitNormalizedFeed({
4962
+ graph: result.hybridGraph,
4774
4963
  warnings,
4775
- provenance: summarizeProvenance(result.provenance)
4964
+ provenance: result.provenance
4776
4965
  });
4777
4966
  return {
4778
4967
  hybridGraph: result.hybridGraph,
@@ -5651,6 +5840,7 @@ function detectDomainProfile(options) {
5651
5840
  learningMode: modeResult.confidence
5652
5841
  },
5653
5842
  evidence: [...inputResult.evidence, ...domainResult.evidence, ...modeResult.evidence],
5843
+ ambiguous: domainResult.ambiguous,
5654
5844
  detectedTechStack: domainResult.detectedTechStack,
5655
5845
  detectedSubject: options.parsedSyllabus?.subject.title || domainResult.detectedSubject,
5656
5846
  detectedDomain: domainResult.domainCategory,
@@ -5718,7 +5908,7 @@ async function runDomainPipeline(options) {
5718
5908
  const projectResult2 = await runProjectGraphPipeline({
5719
5909
  repoDir: options.repoDir,
5720
5910
  goal: options.goal || "Build complete project",
5721
- techStack: options.techStack || "typescript",
5911
+ techStack: options.techStack || domainProfile.detectedTechStack?.[0] || "typescript",
5722
5912
  llmConfig: options.llmConfig,
5723
5913
  embeddings: options.embeddings,
5724
5914
  onProgress: options.onProgress
@@ -5728,8 +5918,9 @@ async function runDomainPipeline(options) {
5728
5918
  description: options.description,
5729
5919
  syllabusText: options.syllabusText,
5730
5920
  parsedSyllabus: options.parsedSyllabus,
5921
+ gradeBand: options.gradeBand,
5731
5922
  domain: domainProfile.domainCategory,
5732
- techStack: options.techStack,
5923
+ techStack: options.techStack || domainProfile.detectedTechStack?.[0],
5733
5924
  llmConfig: options.llmConfig,
5734
5925
  onProgress: options.onProgress,
5735
5926
  knowledgeTreePath: options.knowledgeTreePath,
@@ -5759,8 +5950,9 @@ async function runDomainPipeline(options) {
5759
5950
  description: options.description,
5760
5951
  syllabusText: options.syllabusText,
5761
5952
  parsedSyllabus: options.parsedSyllabus,
5953
+ gradeBand: options.gradeBand,
5762
5954
  domain: domainProfile.domainCategory,
5763
- techStack: options.techStack,
5955
+ techStack: options.techStack || domainProfile.detectedTechStack?.[0],
5764
5956
  llmConfig: options.llmConfig,
5765
5957
  onProgress: options.onProgress,
5766
5958
  knowledgeTreePath: options.knowledgeTreePath,
@@ -5789,10 +5981,11 @@ async function runDomainPipeline(options) {
5789
5981
  embeddings: options.embeddings,
5790
5982
  onProgress: options.onProgress
5791
5983
  });
5792
- const curriculumFeed = emitCurriculumFeed(projectResult.projectGraph, {
5984
+ const curriculumFeed = emitNormalizedFeed({
5985
+ graph: projectResult.projectGraph,
5793
5986
  warnings,
5794
5987
  hallucinationCount: projectResult.hallucinations.length,
5795
- provenance: summarizeProvenance(projectResult.provenance)
5988
+ provenance: projectResult.provenance
5796
5989
  });
5797
5990
  return {
5798
5991
  domainProfile,