npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@thanh01.pmt/domain-kit

v0.6.9

Published

Domain graph generation engine — decomposes any domain (software, hardware, math, science...) into structured domain graphs for curriculum planning.

Readme

@thanh01.pmt/domain-kit

Project graph generation engine — decomposes any domain (software, hardware, math, science, language...) into structured domain graphs for curriculum planning.

TypeScript Zod License: MIT


Table of Contents


Overview

domain-kit is a TypeScript package that analyzes a learning domain and produces a structured domain_graph — the single source of truth consumed by curriculum-kit to generate lessons, activities, assessments, and all pedagogical artifacts.

Key capabilities:

  • Auto-detect input type (repo, syllabus, description) and domain category (software, math, science...) with multi-language support (EN, VI, ZH, JA, KO, FR)
  • Context-dependent domain detection — .cpp files default to software; hardware_iot only when Arduino/ESP32/sensor context is present
  • Choose graph type automatically: project_graph (product-driven) or knowledge_graph (concept-driven) or hybrid_graph
  • Source scanning of Swift, TypeScript/JavaScript, Python and C/C++ files (regex-based — the package has no parser dependency)
  • LLM-powered decomposition of projects into features → steps → keywords
  • ULO/CIO/SIO depth classification — each keyword/concept mapped to WHAT (ulo), HOW (cio), or IMPLEMENTATION (sio)
  • Concept resolution with ULO/CIO-aware matching against Master Tree concept codes
  • Prerequisite cycle detection — DFS cycle detection + Kahn's topological sort for knowledge graphs
  • Keyword-per-milestone tracking with new vs prerequisite distinction
  • Time-budget-aware assembly — milestones split against a 90-minute session, a 1.15 overhead factor and a 120-minute cap (fixed by the pipeline today; assembleRoadmap() takes them as parameters)

End-to-End: From Raw Input to CurriculumFeed

domain-kit never writes a lesson, never schedules a session, never picks an activity. Its entire job is to turn something that already exists — a repository, a syllabus, a short description — into one JSON object with a fixed shape (CurriculumFeed v2) that curriculum-kit can schedule without guessing.

Read this section as four layers that always run in this order:

| Layer | Who | Calls the LLM? | When it fails | |---|---|---|---| | 1. Detect | detectDomainProfile() | no | never throws — reports confidence / ambiguous | | 2. Generate | runProjectGraphPipeline / runKnowledgeGraphPipeline / runHybridGraphPipeline | yes | throw (fail-closed) | | 3. Emit | emitCurriculumFeed() | no | throw (fail-closed) | | 4. Validate | validateCurriculumFeed() | no | throw (fail-closed) |

The rule that makes the package trustworthy: layer 2 and 3 never hand back a half-built graph. A missing LLM key, an empty phase plan, a concept with no keywords, a backwards prerequisite edge — all of them stop the run at the layer that produced them, instead of surfacing as a broken lesson three packages later.

Shape is fail-closed; content is reported. Structural violations throw at emission. Things that make a graph pedagogically suspect — a feature with no steps, a concept no phase introduces — are diagnostics (source.diagnostics, each with a code and a severity) rather than throws, so the host decides whether to block. See ADR-0002.

The whole picture

 +------------------------------------------------------------------------------+
 | INPUT                                                                        |
 |   repoDir / repoUrl - syllabusText / parsedSyllabus - description - goal     |
 |   techStack - chapters - fileExtensions - gradeBand                          |
 |   forcedLearningMode (router override) - knowledgeTreePath - proposalsPath   |
 +------------------------------------+-----------------------------------------+
                                      v
 +------------------------------------------------------------------------------+
 | LAYER 1 - detectDomainProfile() - pure, deterministic, no LLM                |
 |   detectInputType() -> detectDomain() -> detectLearningMode()                |
 |   => DomainProfile { inputType, domainCategory, learningMode, graphType,     |
 |                     confidence, evidence, ambiguous? }                       |
 +------------------------------------+-----------------------------------------+
                                      | effectiveMode = forcedLearningMode
                                      |   ?? domainProfile.learningMode
                                      | (graphType is only its echo)
        +---------------------+---------------------+
        v                     v                     v
    'product'             'concept'             'hybrid'
        |                     |                     |
        |                     |        repoDir present? --no--> FALLBACK to the
        |                     |             |                  concept branch,
        |                     |             |                  loud if auto (R1)
        v                     v             v
 runProjectGraph   runKnowledgeGraph   runProjectGraphPipeline + runKnowledgeGraphPipeline
 Pipeline          Pipeline            (two full graph runs), then:
 STEP_1 scan/      (parseSyllabus      generateHybridGraph()
   select/regex      first if raw        STEP_1 feature-concept links
 STEP_2 keyword      syllabusText)       STEP_2 decomposePhases
   extract           generateKnowledge     (auditPhasePlan fail-closed)
 STEP_3 SDK API      Graph:              STEP_3 totals
   index              STEP_1 decompose
 STEP_4 LLM           STEP_2 validate & clean
   C0/C1/C2           STEP_2_5 standardize vs
 STEP_5 verify          mlo-knowlege-tree.tsv  <== knowledgeTreePath enters
   vs source            here, fail-closed (R5, R6)
 STEP_6 escalate     STEP_3 detect + break cycles (R4)
   keywords          STEP_4 topological sort
 STEP_7 resolve to   STEP_5 ULO/CIO/SIO authoring
   Master Tree       STEP_5b SIO keyword enrichment
   concepts          STEP_5c minute calibration
 STEP_8 assemble     STEP_6 verify + coverage audit
   roadmap (feed is
   emitted by the
   ROUTER, not this
   pipeline - R3)
        |                     |                     |
        +---------------------+---------------------+
                              v
 +------------------------------------------------------------------------------+
 | LAYER 3 - emitCurriculumFeed(graph) - pure projection, no LLM                |
 |   Options built in ONE shared helper (pipeline/emitFeed.ts, R3 fixed):       |
 |   router (project), knowledge orchestrator, hybrid orchestrator.             |
 |   learning_nodes[] - dependency_edges[] - suggested_groupings[] - phases[]   |
 |   (phases[] is filled ONLY by the hybrid emitter - R1)                       |
 +------------------------------------+-----------------------------------------+
                                      v
 +------------------------------------------------------------------------------+
 | LAYER 4 - validateCurriculumFeed(feed) - structural + teaching order         |
 |   Entry dispatches by graph shape; anything that already looks like a        |
 |   feed is pass-through validated (R7)                                        |
 +------------------------------------------------------------------------------+
                                      v
                            CurriculumFeed  (schema_version: 2)
                                      ||
                                      ||  ====== package boundary ======
                                      ||  nothing below this line is
                                      ||  domain-kit's business
                                      v
              curriculum-kit -> units -> sessions -> lessons -> activities

Risk tags (R1-R9) are defined in the Risk map below.

Two facts about that diagram are easy to miss and worth stating up front:

  1. Only the hybrid path fills phases[]. emitFromProject and emitFromKnowledge both return phases: []. "The order in the file is the order in the classroom" is therefore a guarantee that only hybrid consumers can rely on — a project-only or knowledge-only feed has no phase authority at all.

  2. runProjectGraphPipeline stops at STEP_8. Feed emission for the product branch is done by runDomainPipeline() (static import of the emitter - the old dynamic import() was removed as pure inconsistency), not by the project pipeline itself. The knowledge and hybrid branches emit from their own thin orchestrators in src/pipeline/.

  3. Hybrid degrades loudly; explicit requests fail closed. An AUTO-detected hybrid without a repoDir runs the concept branch instead and returns a structured modeDowngrade: { from, to, reason } on the result (R1). A FORCED forcedLearningMode: 'hybrid' without repoDir throws. Both hybrid knowledge legs pass knowledgeTreePath/proposalsPath through, so STEP_2_5 tree standardization runs in every mode (R2). Both fixed 2026-10-07.

Risk map (2026-10-07 - re-verified against code)

Written while re-drawing the diagram above; every row cites the code it was verified against. Full postmortems: deep review 2026-10-05 (app repo, docs/analysis/2026-10-05-graph-curriculum-deep-review.md) and ADR-0002.

| # | Risk | Where | Severity | |---|---|---|---| | R1 | FIXED 2026-10-07. Was: hybrid without repoDir silently degraded to a knowledge-graph feed (only a warnings[] entry). Now a FORCED hybrid throws (fail-closed); an auto-detected downgrade returns a structured modeDowngrade { from, to, reason }. | src/pipeline/domainRouter.ts | High | | R2 | FIXED 2026-10-07. Was: the hybrid branch's knowledge legs dropped knowledgeTreePath/proposalsPath, so STEP_2_5 ran in concept mode but not hybrid mode for the same subject. Now both hybrid legs pass them through. | src/pipeline/domainRouter.ts | High | | R3 | FIXED 2026-10-07. Was: feed emission invoked from three places with drifting options (hallucinationCount only from the router). Now ONE shared helper emitNormalizedFeed() in src/pipeline/emitFeed.ts builds options for all three call sites (lives in pipeline/ so feed/ keeps its no-llmClient invariant). | src/pipeline/emitFeed.ts | Medium | | R4 | Cycle breaking removes the DFS back-edge positionally (mechanism verified 2026-10-07: NO "weakest edge"/link-counting weighting — that was the old misleading log text, now fixed) and only warns. Which edge is dropped decides teaching order. | src/graph/knowledgeGraphVerifier.ts:69 | Medium | | R5 | FIXED 2026-10-07. Was: STEP_2_5 only ran when knowledgeTreePath was provided and the absence of tree anchoring was silent. Now the pipeline pushes an explicit warning (all concepts then stay proposed_new). | src/graph/knowledgeGraphPipeline.ts | Medium | | R6 | FIXED 2026-10-07. Was: tree prereq codes outside THIS analysis were dropped silently. Now one aggregated warning lists unmapped tree prereq codes (deduped, display capped at 10). | src/graph/knowledgeGraphPipeline.ts | Medium | | R7 | emitCurriculumFeed dispatches by shape (key presence) and pass-through-validates anything that "looks like a feed" (CF-8 in the deep review). | src/feed/curriculumFeedEmitter.ts:833-839 | Low | | R8 | Non-fatal pedagogy issues funnel into a single lossy warnings[] string channel; some drops only hit console.warn (e.g. a concept falling out of learning_path, KG-3). Structured diagnostics per ADR-0002 should replace string warnings. | deep review KG-3 | Medium | | R9 | FIXED 2026-10-07. Was: ASCII-only regex assumptions broke on Vietnamese text (KG-1) - empty derived keywords threw, registries filled with fragments. Now a shared stripDiacritics() (NFD + combining-mark strip + đ→d, incl. the precomposed-base-letter case NFD cannot decompose) backs every tokenizer; regression tests in src/__tests__/vietnamese-tokenization.test.ts. | src/utils/diacritics.ts | Critical |

Layer 1 - Detect

detectDomainProfile() runs three detectors in sequence and never calls an LLM:

  • Input type — repository | syllabus | description | files | mixed
  • Domain category — multi-signal scoring over 340+ keywords in 10 categories, with Vietnamese / English / Chinese / Japanese / French keyword lists and context-dependent extension mapping (.cpp is ambiguous: software by default, hardware_iot only with Arduino/ESP32/sensor context).
  • Learning mode — product | concept | hybrid, the signal that actually picks the branch in layer 2.

graphType on the profile is a convenience mirror of learningMode (product→project_graph, concept→knowledge_graph, hybrid→hybrid_graph); when the two would disagree, forcedLearningMode overrides the mode and graphType is recomputed at the branch.

Layer 2 - Generate (one branch by learning mode)

Branch A — project_graph (product-driven)

 STEP_1     scan the file TREE, names only (no content)
            └─▶ LLM picks the relevant files  ─▶ read content for those files only
            └─▶ parse .swift / .ts .tsx / .js .jsx / .py / .ino .cpp .c .h .hpp
            └─▶ merge results by PRIMARY language
 STEP_2     extractKeywords()  → weighted keywords (app / esp32 tags)
 STEP_3     SDK API index, derived from STEP_2 keywords
 STEP_4_C0  scaffold   → feature F0 "FOUNDATION & SETUP" (LLM failure ⇒ F0 is skipped
                         silently and the pipeline continues without it)
 STEP_4_C1  overview   → product{ goals, users, journeys, development_stages } + features
 STEP_4_C2  steps      → per-feature steps, one batched LLM call, falling back to
                         per-feature; a feature that still fails is logged and left
                         step-less (no throw)
 STEP_5     verifyProjectGraph() → strip files / APIs / keywords not found in code;
                                F0 keywords are exempt (pedagogical terms)
 STEP_6     escalateAndMapConcepts() → keyword → technology-neutral concept +
                                       depth (ulo | cio | sio) + evidence files.
                                       Depth comes from the LLM; on LLM failure a
                                       regex fallback reads the usage context
                                       (import → sio, code body → sio, comment → ulo)
 STEP_7     resolveConcepts() → concept → Master-Tree code, scored ULO/CIO-aware:
                                   sio keyword → concept.keywords
                                   cio keyword → concept.description + concept.cio
                                   ulo keyword → concept.ulo + concept.description
 STEP_8     assembleRoadmap() → milestones grouped into phases, each milestone tagged
                                with all / new / prerequisite keywords + a time budget
 ───────────────────────── package boundary ───────────────────────────────
            emitCurriculumFeed(projectGraph) → CurriculumFeed (phases: [])

Branch B — knowledge_graph (concept-driven)

 STEP_1    parseSyllabus(syllabusText) → structured units/topics
           LLM decomposes the subject → concepts + prerequisites + problem_types
           + categories + techniques. The syllabus is truncated at
           KG_MAX_SYLLABUS_CHARS (default 32000) with a warning.

 STEP_2    validate & clean
           · description shorter than 50 chars → warning
           · KEYWORD REGISTRY IS FAIL-CLOSED: a concept with no keywords gets them
             derived from its name/description; if nothing is derivable → throw
           · a prerequisite pointing at a non-existent concept → edge removed + warning

 STEP_2_5  only when knowledgeTreePath is supplied: standardize every concept against
           mlo-knowlege-tree.tsv → `standardized` (adopts the tree code + the keywords
           the standardizer chose for this tech stack) or `proposed_new`.
           A concept with no decision → throw. Tree prerequisite codes then seed
           internal edges where both ends are in this analysis.

 STEP_3    detectPrerequisiteCycles() → breakPrerequisiteCycles() at the weakest edge
 STEP_4    topologicalSort() (Kahn) → learning_path is REBUILT from the sorted order
 STEP_5    hierarchical ULO/CIO/SIO authoring, then deterministic ULO/CIO/SIO codes
           (a tree-standardized code from STEP_2_5 always wins over a generated one)
 STEP_5b   mine the SIO text for technology identifiers — backticked tokens,
           @Annotations, a.b() calls, .modifiers, camelCase — and merge them into the
           keyword registry (capped at 8 per concept)
 STEP_5c   minute calibration:
             score = 0.2·sio + 0.15·problem_types + 0.15·prerequisites
                   + 0.2·(has Analyze/Evaluate/Create)
             minutes = 15 + 45·score          ⇒ 15 … 60 minutes
           The calibrated value overrides the LLM estimate only when the LLM returned
           its default 30, or when the two diverge by more than 50%.

 STEP_6    verifyKnowledgeGraph() → referential integrity + REVERSE COVERAGE audit:
           every syllabus topic must be covered by at least one concept.
           Findings are reported as warnings, not thrown.
 ───────────────────────── package boundary ───────────────────────────────
           emitCurriculumFeed(knowledgeGraph) → CurriculumFeed (phases: [])

Branch C — hybrid_graph (both, and how they interlock)

Needs a project_graph and a knowledge_graph — it is the only branch that runs two generators before it starts.

 input: project_graph + knowledge_graph

 STEP_1   LLM emits feature ↔ concept links
          { feature_id, concept_id, relationship: requires|demonstrates|applies|extends,
            depth: ulo|cio|sio, estimated_prereq_minutes }
          A link naming an unknown feature or concept is DROPPED with a warning.

 STEP_2   decomposePhases() — the LLM turns the build into PROGRESSIVE COMPLETION
          PHASES: each phase states what the product can demonstrably do once it is
          done, owns every feature built in it, and declares which concepts it teaches
          at its start (aspect = new | advanced | preview).
          auditPhasePlan() then checks the contract deterministically:
            · every feature in EXACTLY one phase, none skipped
            · every phase has a non-empty product_completion
            · a concept is introduced no later than the phase that consumes it
            · no concept before its own prerequisites — checked per PHASE, not per array
              position: teaching a concept and its prerequisite in the SAME phase is valid
              whichever order the LLM emitted them, and flagging it would burn all three
              repair attempts and then throw away a good plan
            · a preview is satisfied by an earlier anchor or an earlier preview, and every
              preview has an anchor teach somewhere
            · preview must precede its anchor and precede nothing already introduced
          Violations are fed back verbatim; at most 3 attempts, then throw. A phase
          model that cannot be trusted is worse than no output.

 STEP_3   totals
          total_project_minutes  = Σ step.effort.estimated_minutes (fallback 15)
          total_concept_minutes  = Σ concept.estimated_minutes   (fallback 30)
          linked_prereq_minutes  = total_project_minutes + Σ max-per-concept prereq
                                   minutes, counting only concepts that HAVE a feature
                                   link. It is NOT the course total.
 ───────────────────────── package boundary ───────────────────────────────
          emitCurriculumFeed(hybridGraph) → CurriculumFeed — the only path that
          fills phases[], and therefore the only path that defines teaching order.

Layer 3 - Emit (one shape for every graph)

 emitCurriculumFeed(graph, opts?)
   ├─ graph already has learning_nodes[]   → validate and pass through unchanged
   ├─ has project_graph | knowledge_graph | links[] | phases[]
   │                                        → emitFromHybrid()
   ├─ has concepts[] or type==='knowledge_graph'
   │                                        → emitFromKnowledge()
   └─ otherwise                             → emitFromProject()

emitFromHybrid — phase-driven emission. For each phase, in order, it emits new concepts first, then advanced revisits, then previews, and only then the phase's features and their steps (project-graph order):

| Aspect | Node id | Minutes | Carries | |---|---|---|---| | new | <cid> | the concept's own estimate | keywords.new — the legitimate vocabulary introducer | | advanced | <cid>__ADV<phase> | max(15, 50% of anchor) | a sio → cio downgrade candidate saving 30% of the revisit's own minutes | | preview | <cid>__PREV<phase> | max(5, 25% of anchor) | keywords.all — may introduce the vocabulary early, with no prerequisite edges and no scaffolding |

A preview in an EARLIER phase satisfies a downstream concept's prerequisite — that is what it is for. A preview in the same phase does not, because within a phase the order is new → advanced → preview, so it would arrive after the dependent concept.

Knowledge edges

Edges are materialized as concept → first step of the consumer feature (kind: 'knowledge'); feature build order and depends_on become kind: 'task'. When a prerequisite is satisfied only by a preview, the edge points at the preview node — the vocabulary genuinely comes from there.

Keyword identity is case- and Unicode-insensitive: SwiftUI / swiftui and an NFD/NFC Vietnamese pair are the same keyword. The ledger and the edge materializer share one comparison key, so they cannot disagree about what counts as a first appearance.

Edges are materialized as concept → first step of the consumer feature (kind: 'knowledge'); feature build order and depends_on become kind: 'task'.

Where the graph contradicts itself, the emitter reports instead of inventing:

  • a link whose anchor teach sits in a LATER phase than its consumer → hybrid_link_drift, no edge;
  • a product step that would become the introducer of a concept's vocabulary → concept_overtaken_by_step, no edge;
  • a keyword introducer that sits in a later phase than its consumer → keyword_order_drift, no edge.

That last class of edge is what once produced the 2026-09-17 "Teaching-order violation" incident: the contradiction used to escape emission and explode inside the planner.

Coverage holes that used to be silent

| Diagnostic | Severity | Meaning | |---|---|---| | orphan_concept | error | the knowledge graph holds a concept that no phase introduces and no link consumes — it will not be taught | | feature_without_steps | warning | the project graph declares a feature whose steps never materialised (STEP_4_C2 failed for it) — it will not appear in the feed | | step_without_id | warning | a step with no id cannot become a node and was dropped (reported as feature#index) | | external_prerequisite | info | a concept depends on knowledge outside this analysis — recorded on the node as external_prerequisites, not an error | | upstream_warning | warning | a warning string handed in by the graph pipeline, kept for backward compatibility |

Both of the first two used to vanish with an empty warnings: []. The host can gate on code/severity instead of substring-matching free text.

Entry requirements

A concept can depend on knowledge the analysis does not cover — a course on vector calculus legitimately requires linear algebra. Those ids are not nodes here, so they cannot be edges; dropping them silently would throw away exactly what someone deciding whether to enrol needs most.

They land on the node instead:

{ "id": "C7", "kind": "concept", "external_prerequisites": ["C_LINALG"] }

and surface once as an external_prerequisite info diagnostic. Info, not warning — a course with entry requirements is normal, and a host that gates on severity should not block on it. The three emission paths used to disagree here: the knowledge path dropped them silently while both hybrid paths hard-failed (one built a dangling edge, the other threw an ordering error blaming a concept that was not in the analysis), so any graph referencing prior knowledge was un-emittable.

Provenance

source.provenance records which providers actually served the run:

provenance: {
  providers: ['openrouter', 'nvidia'],  // failover is visible, not silent
  models: ['preset-x', 'nemotron-3-ultra'],
  calls: 7,
  failed_calls: 1,                       // openrouter fell through
  degraded: true,
}

Without it, a graph produced half by a paid preset and half by a free tier is indistinguishable from a clean one — and not reproducible or auditable. The client is created once per pipeline run and threaded through every step, so all calls land in one log; it is per-instance, so concurrent runs never inherit each other's history.

Layer 4 - Validate (fail-closed, at the source)

validateCurriculumFeed() runs on every emission path and rejects:

  • duplicate node ids;
  • an edge pointing at an unknown node, or a self-loop;
  • a grouping or phase referencing an unknown node;
  • duplicate phase ids;
  • duplicate or non-ascending phase order — order is the teaching sequence once phases exist, so two phases claiming one slot makes it undefined;
  • an empty feed (learning_nodes has a minimum of 1);
  • the teaching-order invariant — when phases[] is non-empty, a prerequisite must live in the same phase or an earlier one than its consumer.

emitFromProject adds one more: a graph with features[] but no features[].steps is rejected by name as a legacy knowledge-tree graph, with the regeneration command in the message.

The three graph types at a glance

| | project_graph | knowledge_graph | hybrid_graph | |---|---|---|---| | Answers | "what is being built?" | "what must be understood?" | "both, and how they interlock" | | LLM sees | file tree + selected file contents | syllabus / description | both graphs, rendered as text | | Atomic node | features[].steps[] | concepts[] | both | | Ordering authority | array order of features/steps | learning_path (topological) | phases[] → feed.phases | | Time source | step.effort.estimated_minutes | concept.estimated_minutes (calibrated 15–60) | sum of both | | Prerequisite evidence | none — build order only | concept.prerequisites[], proven acyclic | both, cross-checked against concept prerequisites | | feed.phases | always [] | always [] | filled ⇒ teaching order | | Entry point | runProjectGraphPipeline() | runKnowledgeGraphPipeline() | runHybridGraphPipeline() |

Depth and minute conventions

Exported from code as DEPTH_MINUTE_FRACTIONS and calibrateConceptMinutes() — read those rather than copying numbers out of this table.

| Convention | Value | Source | |---|---|---| | preview node minutes | max(5, 25%) of the anchor | DEPTH_MINUTE_FRACTIONS | | ULO / CIO / SIO minutes for a concept node | max(5, 35%) / max(ulo+5, 65%) / max(cio+5, 100%) | DEPTH_MINUTE_FRACTIONS | | advanced revisit minutes | max(15, 50%) of the anchor | DEPTH_MINUTE_FRACTIONS | | advanced revisit downgrade candidate | sio → cio, saves 30% of the revisit's own minutes | DEPTH_MINUTE_FRACTIONS | | step with no estimated_minutes | 15 minutes (same constant the hybrid totals use) | DEFAULT_STEP_MINUTES | | concept minutes after calibration | 15–60 minutes | calibrateConceptMinutes() | | roadmap session / overhead / milestone split cap | 90 min / ×1.15 / 120 min — hardcoded by the pipeline, not configurable through it | STEP_8 in pipeline/projectGraphPipeline.ts | | phase minute balance demanded of the LLM | 120–240 min per phase; rebalance below 60 or above 400 | STEP_2 in graph/hybridGraphPipeline.ts | | file budget per run | 70 files / 500 000 chars | utils/fileUtils.ts |

The three depth levels are a single ladder applied everywhere: ulo = WHAT + WHY (technology-agnostic), cio = HOW the mechanism works (still no concrete API), sio = IMPLEMENTATION (must name a real API, function, module or file). A keyword can only ever move down this ladder, never across it.

The percentages above live in one exported table, DEPTH_MINUTE_FRACTIONS. They were previously hardcoded in three places (25% / 35% / 30%), which meant "what does a ULO pass cost?" had three answers. Read that table rather than hardcoding a fourth.

Unicode: Vietnamese input reaches the detector in NFC or NFD form. Text is normalized at the boundary and also matched with diacritics stripped, so the same sentence classifies identically whichever form it arrives in — before this, one sentence measured as math (confidence 1.0) in NFC and other (confidence 0.2) in NFD.


Installation

npm install @thanh01.pmt/domain-kit

Or with workspace link:

npm link @thanh01.pmt/domain-kit

Peer Dependencies

{
  "zod": "^3.23.8"
}

Environment Variables (for LLM features)

LLM_API_KEY=sk-...           # OpenAI-compatible API key
LLM_BASE_URL=https://api.openai.com/v1  # API base URL
LLM_MODEL=gpt-4o-mini        # Model to use
LLM_MAX_TOKENS=65536          # Max output tokens (default)
LLM_MIN_REQUEST_INTERVAL_MS=2500  # process-wide pacing; 0 disables

# Extra knobs the pipelines read directly:
KG_MAX_SYLLABUS_CHARS=32000   # syllabus truncation before decomposition
KG_MAX_TOKENS=65536           # knowledge-graph generation budget
LLM_REQUEST_TIMEOUT_MS=300000 # per-request cap

Provider chain. createLlmClient() builds an ordered chain and fails over through it on transient errors (408/429/500/502/503/504):

  1. explicit llmConfig.apiKey
  2. LLM_* (or OPENAI_*) — generic OpenAI-compatible
  3. DASHSCOPE_* / ALIBABA_*
  4. NVIDIA_API_KEY → NVIDIA NIM
  5. OPENROUTER_API_KEY → OpenRouter

If OPENROUTER_MODEL is set, OpenRouter is moved to the front. Requests are paced process-wide so free tiers are not rate-limited by a burst; the interval is configurable via LLM_MIN_REQUEST_INTERVAL_MS or llmConfig.minRequestIntervalMs (set 0 when the caller owns the rate limit — several concurrent pipelines in one process, or serverless).

Each client keeps a callLog of every provider attempt it made. One client is created per pipeline run and threaded through every step, so PipelineResult.provenance and CurriculumFeed.source.provenance reflect that whole run — and because the log is per-instance, concurrent runs never inherit each other's history.


Quick Start

1. Detect Domain Profile

import { detectDomainProfile } from '@thanh01.pmt/domain-kit';

const profile = detectDomainProfile({
  repoUrl: 'https://github.com/user/talky',
  goal: 'Build iOS chat app with SwiftUI and Firebase',
  techStack: 'Swift,SwiftUI,Firebase',
});

console.log(profile.graphType);       // 'project_graph'
console.log(profile.domainCategory);  // 'software'
console.log(profile.learningMode);    // 'product'

2. Generate Project Graph (Software)

import { runProjectGraphPipeline } from '@thanh01.pmt/domain-kit';

const result = await runProjectGraphPipeline({
  repoDir: '/path/to/repo',
  goal: 'Build iOS chat app with SwiftUI',
  techStack: 'Swift,SwiftUI,Firebase',
});

console.log(result.projectGraph);     // ProjectGraph schema
console.log(result.roadmap);          // AssembledRoadmap with phases
console.log(result.keywords);         // Extracted keywords

3. Generate Knowledge Graph (Math/Science)

import { generateKnowledgeGraph } from '@thanh01.pmt/domain-kit';

const result = await generateKnowledgeGraph({
  subject: 'Hình học K12',
  syllabusText: 'Chương 1: Phân loại góc...\nChương 2: Tam giác...',
  gradeBand: [6, 9],
  domain: 'math',
});

console.log(result.knowledgeGraph);   // KnowledgeGraph schema

4. Generate Hybrid Graph

import { generateHybridGraph } from '@thanh01.pmt/domain-kit';

const result = await generateHybridGraph({
  projectGraph: existingProjectGraph,
  knowledgeGraph: existingKnowledgeGraph,
});

console.log(result.hybridGraph);      // HybridGraph with links[] + phases[]

5. Emit Curriculum Feed (from any graph)

import { emitCurriculumFeed } from '@thanh01.pmt/domain-kit';

// `graph` is whichever graph object you just produced — note that step 4 above
// rebinds `result` to the HYBRID result, so name the graph explicitly:
const feed = emitCurriculumFeed(projectGraph);        // project_graph
// const feed = emitCurriculumFeed(knowledgeGraph);   // knowledge_graph
// const feed = emitCurriculumFeed(hybridGraph);       // hybrid_graph (phases filled)

console.log(feed.learning_nodes.length);    // unified nodes ready for planner
console.log(feed.dependency_edges.length);  // knowledge + task edges
console.log(feed.suggested_groupings);      // feature/category groupings
console.log(feed.phases.length);            // non-empty ⇒ learning_nodes are in teaching order (phases define units)
// Each node has: bloom_hint, depth_hint, keywords.new/.prerequisite, introduce_aspect, user_visible_deliverable

Architecture

| Directory | Files | Owns | |---|---|---| | schemas/ | projectGraphSchema, knowledgeGraphSchema, hybridGraphSchema, domainProfileSchema, curriculumFeedSchema, domainExtensions | every Zod schema; validateCurriculumFeed() lives in curriculumFeedSchema | | detector/ | inputTypeDetector, domainDetector, learningModeDetector, domainProfileDetector | layer 1 — pure, no LLM | | parsers/ | swift, typescript, python, cpp, syllabusParser | source + syllabus readers | | extractors/ | keywordExtractor, fileSelector, fileRelevanceScorer | keyword weights, LLM file relevance | | graph/ | scaffoldExtractor, overviewExtractor, stepExtractor, graphVerifier, conceptEscalator, hierarchicalLoAuthoring, loCodeStandard, loExemplars, depthAudit, knowledgeTreeCatalog, conceptStandardizer, knowledgeGraphPipeline, knowledgeGraphVerifier, hybridGraphPipeline | layer 2 — the three graph generators | | concepts/ | conceptResolver | keyword → Master-Tree code, ULO/CIO-aware | | assembly/ | roadmapAssembler | milestones, phases, keyword ledger, time budget | | pipeline/ | domainRouter, projectGraphPipeline, knowledgeGraphPipeline, hybridGraphPipeline | the three orchestrators (thin) + the router; only projectGraphPipeline holds the 8 real steps | | feed/ | curriculumFeedEmitter | layer 3 — projects any graph onto CurriculumFeed v2 | | utils/ | llmClient, fileUtils | provider chain + pacing, file budget |

graph/ and pipeline/ both contain files named knowledgeGraphPipeline.ts and hybridGraphPipeline.ts. They are not duplicates: the graph/ ones do the LLM work, the pipeline/ ones parse the syllabus, call the graph/ one, and emit the feed.


Input → Processing → Output

Input

User provides one or more of:

| Field | Type | Description | |---|---|---| | repoUrl | string | Git repository URL | | repoDir | string | Local repository path | | goal | string | Learning goal or project description | | description | string | Subject/project description | | syllabusText | string | Syllabus or curriculum text | | chapters | string[] | Chapter/section names | | techStack | string | Comma-separated technologies | | fileExtensions | string[] | File extensions in repo | | gradeBand | [number, number] | Target grade range |

Processing

Input
  │
  ▼
Step 1: DETECT
  ├── detectInputType()       → repository | syllabus | description | files | mixed
  ├── detectDomain()          → software | hardware_iot | math | science | ...
  │     └── context-dependent: .cpp→software (default), hardware only with ESP32/Arduino context
  ├── detectLearningMode()    → product | concept | hybrid
  └── → DomainProfile (with ambiguity flag if domains overlap)
  │
Step 2: ROUTE (by graphType)
  │
  ├── project_graph   → parsers → extractors → graph/LLM → verify → concepts → assembly
  ├── knowledge_graph → LLM decompose → ULO/CIO/SIO → cycle detect → topo sort → problem_types
  └── hybrid_graph    → project_graph + knowledge_graph + LLM link features↔concepts
  │
Step 3: OUTPUT
  ├── the graph object   (project_graph | knowledge_graph | hybrid_graph)
  └── CurriculumFeed v2  (learning_nodes / dependency_edges / groupings / phases)
      ↑ always produced by emitCurriculumFeed(); hybrid is the only branch that fills phases[]

Note: domain-kit returns objects in memory — it never writes *.json itself.
Serialising them is the host's job.

Output

Always one of three schemas:

| Graph Type | Schema | When | |---|---|---| | project_graph | ProjectGraph | Product-driven (software, hardware, 3D design) | | knowledge_graph | KnowledgeGraph | Concept-driven (math, science, language) | | hybrid_graph | HybridGraph | Both product + concept linked |


Domain Detection

How it works

detectDomainProfile() runs three detectors in sequence:

  1. Input Type Detector — Checks for repo URLs, syllabus text, file extensions, descriptions
  2. Domain Detector — Multi-signal scoring with 340+ keywords across 10 domains, multi-language support (EN, VI, ZH, JA, KO, FR), and context-dependent extension mapping (e.g., .cpp → software by default, hardware only when Arduino/ESP32/sensor context present)
  3. Learning Mode Detector — Combines input signals + domain heuristics + verb patterns

Ambiguous Domain Handling

Some file extensions map to multiple domains (e.g., .cpp → software or hardware). The detector:

  1. Default-maps ambiguous extensions to software (most common use)
  2. Checks context keywords (e.g., arduino, ESP32, GPIO, sensor) to upgrade to hardware_iot
  3. Sets ambiguous field in output when detection is borderline
  4. Agent should ask user to confirm when ambiguous is set or confidence.domainCategory < 0.6

Domain Categories

| Category | Examples | Keywords (sample) | |---|---|---| | software | App, web, API | swift, react, typescript, docker | | hardware_iot | Arduino, robotics | arduino, sensor, ESP32, GPIO | | 3d_design | Tinkercad, Fusion360 | shape, dimension, STL, CAD | | math | Hình học, đại số | tam giác, phương trình, calculus | | science | Lý, hóa, sinh | thí nghiệm, năng lượng, cell | | language | Tiếng Anh, writing | grammar, vocabulary, essay | | arts | Âm nhạc, thiết kế | piano, color theory, typography | | research | Nghiên cứu, thống kê | methodology, hypothesis, regression | | business | Kinh doanh, tài chính | revenue, agile, kế hoạch kinh doanh | | other | Fallback | — |

Confidence & Fallback

const profile = detectDomainProfile({ ... });

// Check for ambiguous detection
if (profile.ambiguous) {
  // E.g., { domain1: 'software', domain2: 'hardware_iot', reason: 'C++ files + ESP32 context' }
  // → Ask user to confirm which domain
}

if (profile.confidence.domainCategory < 0.6) {
  // Low confidence — ask user to confirm
}

if (profile.domainCategory === 'other') {
  // Unknown domain — ask user to specify
}

Graph Types

Project Graph (Product-Driven)

For projects where the learner builds something: apps, robots, 3D models.

{
  "project": { "id": "...", "tech_stack": {...} },
  "product": { "goals": [...], "development_stages": [...] },
  "features": [
    {
      "id": "F1",
      "name": "Authentication",
      "steps": [
        {
          "id": "F1-S1",
          "name": "Implement AuthViewModel",
          "keywords": ["@Published", "@EnvironmentObject"],
          "api_usage": ["FirebaseAuth"],
          "effort": { "estimated_minutes": 30 }
        }
      ]
    }
  ]
}

Knowledge Graph (Concept-Driven)

For subjects where the learner understands concepts: math, science, language.

{
  "subject": { "id": "geometry-k12", "domain": "math" },
  "concepts": [
    {
      "id": "G1",
      "name": "Angle Classification",
      "ulo": "WHAT: Angles are... WHY: We classify to compare...",
      "cio": "HOW: Measured in degrees, compared using protractor...",
      "sio": ["protractor usage", "angle notation"],
      "prerequisites": [],
      "problem_types": [{ "bloom_level": "Remember", "difficulty": "easy" }]
    }
  ],
  "learning_path": [
    { "concept_id": "G1", "order": 1 },
    { "concept_id": "G2", "order": 2, "depends_on": ["G1"] }
  ]
}

Hybrid Graph

For projects that require both building and understanding.

{
  "project_graph": { ... },
  "knowledge_graph": { ... },
  "links": [
    {
      "feature_id": "F2",
      "concept_id": "G3",
      "relationship": "requires",
      "depth": "cio",
      "estimated_prereq_minutes": 30,
      "phase_id": "PH2"
    }
  ],
  // phases[] is the teaching order — and the only source of CurriculumFeed.phases
  "phases": [
    {
      "id": "PH1",
      "order": 1,
      "name": "Runnable slice",
      "product_completion": "App launches and shows an empty conversation list",
      "feature_ids": ["F0", "F1"],
      "introduces": [
        { "concept_id": "G1", "aspect": "new", "note": "..." },
        { "concept_id": "G3", "aspect": "preview", "note": "vocabulary only" }
      ]
    }
  ],
  "total_project_minutes": 480,
  "total_concept_minutes": 210,
  "linked_prereq_minutes": 540
}

Schemas

All schemas are Zod-validated TypeScript types.

| Schema | File | Purpose | |---|---|---| | ProjectGraphSchema | projectGraphSchema.ts | Software/hardware/3D project structure | | KnowledgeGraphSchema | knowledgeGraphSchema.ts | Concept-driven subject structure | | HybridGraphSchema | hybridGraphSchema.ts | Combined project + knowledge | | DomainProfileSchema | domainProfileSchema.ts | Domain detection output | | HardwareFeatureExtensionSchema | domainExtensions.ts | Hardware overlay (wiring, materials) | | ThreeDesignFeatureExtensionSchema | domainExtensions.ts | 3D Design overlay (shapes, dimensions) | | MathConceptExtensionSchema | domainExtensions.ts | Math overlay (theorems) | | ScienceConceptExtensionSchema | domainExtensions.ts | Science overlay (experiment procedures) | | LanguageConceptExtensionSchema | domainExtensions.ts | Language overlay (grammar rules) | | ConceptSchema | knowledgeGraphSchema.ts | One knowledge concept | | LearningPathStepSchema | knowledgeGraphSchema.ts | One topological-order step | | CurriculumFeedSchema | curriculumFeedSchema.ts | Planner-ready normalized feed (nodes, edges, groupings, phases) | | FeedNodeSchema | curriculumFeedSchema.ts | Individual learning node (concept, skill, or product step) | | FeedEdgeSchema | curriculumFeedSchema.ts | Dependency edge (knowledge or task) | | FeedGroupingSchema | curriculumFeedSchema.ts | Suggested grouping of nodes (feature or category) | | FeedPhaseSchema | curriculumFeedSchema.ts | Development phase — when non-empty, defines teaching sequence and unit structure | | FeedDiagnosticSchema | curriculumFeedSchema.ts | Typed finding (code + severity + node ids) | | FeedProvenanceSchema | curriculumFeedSchema.ts | Which LLM providers served the run |

Key Types

// Project Graph
type ProjectGraph = {
  schema_version: 3;
  project: ProjectInfo;
  product: Product;
  features: Feature[];
  capabilities: Capability[];
  implementation: { tasks: Task[] };
  missing_gaps: MissingGap[];
  tech_debt: TechDebt[];
}

type Feature = {
  id: string;
  name: string;
  steps: Step[];
  depends_on: string[];
}

type Step = {
  id: string;
  name: string;
  keywords: string[];
  api_usage: string[];
  completion_level: 'base' | 'mvp' | 'extend' | 'polish';
  outcome?: { user_visible: string; technical: string };   // → FeedNode.user_visible_deliverable
  // all effort fields are OPTIONAL — downstream totals fall back to 15 minutes
  effort?: { estimated_minutes?: number; complexity?: 'low' | 'medium' | 'high';
             concepts_count?: number; files_touched?: number };
}

// Knowledge Graph
type KnowledgeGraph = {
  schema_version: 1;
  type: 'knowledge_graph';
  subject: SubjectInfo;
  concepts: Concept[];
  categories: ConceptCategory[];
  learning_path: LearningPathStep[];
}

type Concept = {
  id: string;
  name: string;
  description: string;
  keywords: string[];   // SIO-level tech terms — drives the keyword ledger, STEP_5b enrichment
                        // and the SIO-binding audit; NEVER silently empty (fail-closed)
  ulo: string;      // WHAT + WHY
  cio: string;       // HOW
  sio: string[];     // Specific implementations
  prerequisites: string[];
  estimated_minutes: number;   // calibrated 15–60 (STEP_5c)
  problem_types: ProblemType[];
  techniques: string[];
  common_mistakes: string[];
  materials: string[];         // physical lab equipment
  visual_aids: string[];       // diagrams / images / animations
  // codes — a knowledge-tree code from STEP_2_5 always wins over a generated one
  code?: string;
  standardization?: 'standardized' | 'proposed_new';
  tree_code?: string;
  ulo_code?: string;
  cio_code?: string;
  sio_codes?: string[];
}

// Domain Detection
type DomainProfile = {
  inputType: 'repository' | 'syllabus' | 'description' | 'files' | 'mixed';
  domainCategory: 'software' | 'math' | 'science' | ...;
  learningMode: 'product' | 'concept' | 'hybrid';
  graphType: 'project_graph' | 'knowledge_graph' | 'hybrid_graph';
  confidence: { inputType: number; domainCategory: number; learningMode: number };
  ambiguous?: { domain1: DomainCategory; domain2: DomainCategory; reason: string };
}

// Concept Escalation (ULO/CIO/SIO depth)
type ConceptMapping = {
  keyword: string;
  concept_code: string;
  concept_name: string;
  depth: 'ulo' | 'cio' | 'sio';     // WHAT (ulo), HOW (cio), IMPLEMENTATION (sio)
  depth_source: 'llm' | 'heuristic' | 'override';  // who decided it — the regex
                                  // fallback (comment→ulo, import→sio) has no
                                  // pedagogical basis and must stay distinguishable
  depth_rationale: string;
  evidence_files: string[];
}

// Assembled Roadmap (with time budget) — TypeScript interface, not a Zod schema
type AssembledRoadmap = {
  project: ProjectGraph['project'];
  product: ProjectGraph['product'];
  phases: AssembledPhase[];        // { phase_id, phase_name, milestones, total_minutes,
                                   //   concept_progression }
  feature_concepts: Map<string, ConceptMapping[]>;
  time_budget: {
    session_minutes: number;          // 90
    overhead_factor: number;          // 1.15
    total_estimated_minutes: number;  // Σ milestone.estimated_minutes (raw)
    total_adjusted_minutes: number;   // Σ milestone.adjusted_minutes (after overhead)
  };
};

type AssembledMilestone = {
  id: string; name: string; description: string;
  feature_id: string; feature_name: string;
  concept_code: string; depth: 'ulo' | 'cio' | 'sio';
  all_keywords: string[];             // keyword ledger lives HERE, per milestone
  new_keywords: string[];
  prerequisite_keywords: string[];
  steps: Step[];
  estimated_minutes: number;
  adjusted_minutes: number;           // estimated_minutes × overhead_factor
  completion_level: string;
};

// Curriculum Feed (planner-ready projection)
type CurriculumFeed = {
  schema_version: 2;
  source: {
    graph_type: 'project_graph' | 'knowledge_graph' | 'hybrid_graph';
    warnings: string[];             // DERIVED from diagnostics (severity ≠ 'info')
    hallucination_count: number;
    diagnostics: FeedDiagnostic[];   // { code, severity, message, node_ids }
    provenance?: FeedProvenance;     // providers / models / calls / degraded
  };
  learning_nodes: FeedNode[];   // in TEACHING ORDER when phases[] is non-empty
  dependency_edges: FeedEdge[];
  suggested_groupings: FeedGrouping[];
  phases: FeedPhase[];          // when non-empty, THESE — not suggested_groupings — define the teaching sequence and unit structure
}

type FeedNode = {
  id: string;
  kind: 'skill' | 'concept' | 'product_step';
  name: string;
  concept_codes: string[];
  keywords: { all: string[]; new: string[]; prerequisite: string[] };
  estimated_minutes: number;
  bloom_hint: 'Remember' | 'Understand' | 'Apply' | 'Analyze' | 'Evaluate' | 'Create' | null;
  depth_hint: 'ulo' | 'cio' | 'sio' | null;
  completion_level: 'mvp' | 'standard' | 'stretch';
  scaffolding: { scaffold_candidates: ScaffoldCandidate[]; core_value_minutes: number };
  references: { file: string; evidence: string }[];
  feature_id: string | null;
  concept_ref: string | null;
  phase_id: string;                          // development phase that introduces/uses this node ('' on legacy feeds)
  introduce_aspect: 'new' | 'advanced' | 'preview' | null;  // first intro / advanced revisit / preview priming
  user_visible_deliverable: string;          // step's outcome.user_visible — checkpoint candidate for session cutting ('' on concept nodes)
  depth_variants?: { ulo: number; cio: number; sio: number };  // minutes per depth level — reconciler price list (ULO ≤ CIO ≤ SIO)
  depth_scaffold_candidates?: { from_depth: 'ulo' | 'cio' | 'sio'; to_depth: 'ulo' | 'cio' | 'sio'; minutes_saved: number; reason: string }[];  // DEPTH downgrades (SIO→CIO, CIO→ULO) — parallel to scaffold_candidates
  is_core: boolean;                          // Master-Tree core concept — never depth-downgraded; escalate instead
  external_prerequisites: string[];          // what the learner must already know that this feed does not teach
}

type FeedPhase = {
  id: string;
  order: number;             // 1-based teaching order
  name: string;
  product_completion: string;
  node_ids: string[];        // nodes of this phase, already in teaching order
}

type FeedEdge = {
  from: string;   // node id
  to: string;     // node id
  kind: 'knowledge' | 'task';
  reason: string;
}

type FeedGrouping = {
  id: string;
  name: string;
  node_ids: string[];
  rationale: string;
}

API Reference

detectDomainProfile(options) → DomainProfile

Detects input type, domain category, learning mode, and graph type.

import { detectDomainProfile } from '@thanh01.pmt/domain-kit';

const profile = detectDomainProfile({
  repoUrl?: string;
  repoDir?: string;
  goal?: string;
  description?: string;
  syllabusText?: string;
  chapters?: string[];
  fileExtensions?: string[];
  techStack?: string;
  gradeBand?: [number, number];
  parsedSyllabus?: ParsedSyllabus;   // pre-parsed syllabus; skips parseSyllabus()
});

runProjectGraphPipeline(options) → PipelineResult

Full pipeline for product-driven projects. Requires source code repository.

import { runProjectGraphPipeline } from '@thanh01.pmt/domain-kit';

const result = await runProjectGraphPipeline({
  repoDir: string;           // Required: local repo path
  goal: string;              // Required: project goal
  techStack: string;         // Required: comma-separated technologies
  llmConfig?: LlmClientConfig;
  embeddings?: Record<string, { embedding?: number[] }>;
  onProgress?: (step: string, message: string) => void;
});

// NOTE: sessionMinutes / overheadFactor / maxMilestoneMinutes are NOT options here.
// STEP_8 hardcodes 90 / 1.15 / 120 when it calls assembleRoadmap(). Call
// assembleRoadmap() yourself if you need different values.

// Returns:
{
  projectGraph: ProjectGraph;
  resolvedConcepts: ResolveConceptsResult;
  roadmap: AssembledRoadmap;
  hallucinations: Hallucination[];
  keywords: Keyword[];
  featureConcepts: Map<string, ConceptMapping[]>;
  provenance: LlmCallRecord[];   // every provider attempt this run made
}

generateKnowledgeGraph(options) → KnowledgeGraphPipelineResult

Generates knowledge graph for concept-driven subjects.

import { generateKnowledgeGraph } from '@thanh01.pmt/domain-kit';

const result = await generateKnowledgeGraph({
  subject: string;           // Required: subject name
  description?: string;
  syllabusText?: string;     // Full syllabus text (multi-language supported)
  gradeBand?: [number, number];
  domain?: string;
  techStack?: string;        // target tech for SIO keyword binding (e.g. "python")
  knowledgeTreePath?: string; // path to mlo-knowlege-tree.tsv → enables STEP_2_5 standardization
  proposalsPath?: string;     // where proposed-new concepts are persisted for the later merge
  llmConfig?: LlmClientConfig;
  onProgress?: (step: string, message: string) => void;
});
// Internally runs STEP_1 … STEP_6 — see [Pipeline Details](#pipeline-details).

// Returns:
{
  knowledgeGraph: KnowledgeGraph;
  warnings: string[];
  verificationReport?: KnowledgeGraphVerificationReport;  // uncovered topics, missing ULO/CIO/SIO…
}

Prefer runKnowledgeGraphPipeline() over generateKnowledgeGraph(): it parses the syllabus, applies syllabus-derived defaults (subject, domain, grade band) and emits the CurriculumFeed for you.

generateHybridGraph(options) → HybridGraphPipelineResult

Links project graph with knowledge graph.

import { generateHybridGraph } from '@thanh01.pmt/domain-kit';

const result = await generateHybridGraph({
  projectGraph: ProjectGraph;
  knowledgeGraph: KnowledgeGraph;   // both are required
  llmConfig?: LlmClientConfig;
  onProgress?: (step: string, message: string) => void;
});

// Returns:
{
  hybridGraph: HybridGraph;   // links[] + phases[] + total_project_minutes /
                              // total_concept_minutes / linked_prereq_minutes
  warnings: string[];
}

emitCurriculumFeed(graph, opts?) → CurriculumFeed

Converts any domain graph (project, knowledge, or hybrid) into a unified, planner-ready CurriculumFeed. Pure transformation — no LLM calls. Validates output (fail-closed).

import { emitCurriculumFeed } from '@thanh01.pmt/domain-kit';

// From any graph type:
const feed = emitCurriculumFeed(projectGraph);        // project_graph
const feed = emitCurriculumFeed(knowledgeGraph);      // knowledge_graph
const feed = emitCurriculumFeed(hybridGraph);          // hybrid_graph
const feed = emitCurriculumFeed(existingFeed);         // pass-through (already a feed)

// With options:
const feed = emitCurriculumFeed(graph, {
  hallucinationCount: 3,      // metadata for source info
  warnings: ['Feature F5 had low confidence'],
  provenance: { providers: ['nvidia'], models: ['nemotron'], calls: 7, failed_calls: 0, degraded: false },
});

// Gate on diagnostics rather than substring-matching warning text:
const blockers = feed.source.diagnostics.filter((d) => d.severity === 'error');
if (blockers.length) throw new Error(blockers.map((d) => d.code).join(', '));

// Returns CurriculumFeed with:
// - learning_nodes[]     — unified node format (concept | skill | product_step), in teaching order when phases[] is non-empty
// - dependency_edges[]   — knowledge (prerequisite) or task (build order) edges
// - suggested_groupings[] — feature or category groupings (layout hint only)
// - phases[]             — development phases; when non-empty, the teaching sequence + unit structure
// - Each node has: bloom_hint, depth_hint (ulo/cio/sio), keywords.new vs .prerequisite,
//   introduce_aspect (new/advanced/preview), user_visible_deliverable,
//   depth_variants + depth_scaffold_candidates + is_core (depth-reconcile inputs)

Key behaviors:

  • Teaching-order emission (contract, 2026-09-18): when a hybrid graph carries phases[], the nodes array follows the phase teaching sequence — the order in the file IS the order in the classroom. The former "concepts emitted first, then steps" array layout is ABOLISHED: it contradicted phase order, fabricated backwards prerequisite edges, and caused the planner's "Teaching-order violation" failure (see postmortem 2026-09-17 §9)
  • No fabricated backwards edges: when a keyword consumer sits in an EARLIER phase than the concept's anchor teach (and no preview covers it), the emitter does NOT invent a dependency edge — it records a drift warning instead (hybrid link drift: …); the host decides, the emitter reports
  • Overtake guard: a product step must never become the introducer of a concept's vocabulary; the first appearance of a keyword belongs to the concept (or its preview node) that teaches it
  • Preview priming (aspect preview, kit ≥0.3.0): a phase may declare a light awareness pass for a concept whose anchor teach lives in a LATER phase — emitted as node <cid>__PREVxx with depth ulo, bloom Understand, ~25% minutes (min 5), keywords.all (making it the legitimate vocabulary introducer), no prerequisite edges, no scaffolding. Five fail-closed emission rules: unknown concept, already-introduced concept, duplicate preview, no anchor anywhere, anchor not in a later phase
  • Knowledge graphs: concepts + prerequisite edges + category groupings
  • Project graphs: features → steps + depends_on edges + feature groupings
  • Pass-through: if input already has learning_nodes[], validates and returns as-is
  • Legacy rejection: throws descriptive error if graph has implementation.tasks but no features[].steps (old knowledge-tree format)
  • New/prerequisite keywords: computed in teaching order (phase sequence when phases exist, array order otherwise) — first appearance = new, seen before = prerequisite. Anchoring keywords to array position instead of teaching order is the exact assumption that caused the 2026-09-17 incident
  • Fail-closed validation: output is validated before return — duplicate IDs, unresolvable edges, self-loops, non-empty, and the teaching-order invariant: when phases define the sequence, a prerequisite must live in the same or an EARLIER phase than its consumer (a violation throws at emission, never defers to the planner)

Parsers

import { parseSwiftFile, parseTsFile, parsePythonFile, parseCppFile } from '@thanh01.pmt/domain-kit';

const swiftResult = parseSwiftFile('AuthViewModel.swift', fileContent);
// → { imports: ['SwiftUI', 'FirebaseAuth'], types: [...], functions: [...] }

Keyword Extraction

import { extractKeywords } from '@thanh01.pmt/domain-kit';

const keywords = extractKeywords(parsedSourceContext);
// → [{ keyword: 'SwiftUI', source: 'import', weight: 1.0, platform: 'app' }]

Pipeline Details

Project Graph Pipeline (product-driven)

runProjectGraphPipeline() — the only pipeline whose steps live inside its orchestrator. Step ids below are the ones it actually logs.

| Step | What happens | Fail mode | |---|---|---| | STEP_1 | scan the file tree (names only) → LLM selects relevant files → read only those → parse → merge by primary language | parse failure is per-file, non-fatal | | STEP_2 | extractKeywords() — filter stdlib, assign weights, tag platform (app / esp32) | — | | STEP_3 | SDK API index, derived from STEP_2 keywords | — | | STEP_4_C0 | scaffold → feature F0 "FOUNDATION & SETUP" | scaffold extractor returns null; the pipeline continues without F0 | | STEP_4_C1 | overview → product meta (goals, users, journeys, development stages) + the feature list | — | | STEP_4_C2 | steps per feature — one batched LLM call, falling back to per-feature | a feature that still fails is logged and left step-less; no throw | | STEP_5 | verifyProjectGraph() — strip files / APIs / keywords not found in code; F0 keywords exempt | findings returned as hallucinations[], graph is repaired | | STEP_6 | escalateAndMapConcepts() — keyword → neutral concept + ulo/cio/sio + evidence files. Depth from the LLM; regex fallback on LLM failure (import → sio, code body → sio, comment → ulo) | LLM failure degrades to the regex fallback | | STEP_7 | resolveConcepts() — concept → Master-Tree code, scored ULO/CIO-aware | unmatched keywords become proposed concepts | | STEP_8 | assembleRoadmap() — milestones grouped into phases; each milestone gets all / new / prerequisite keywords and a time budget (90 min sessions, ×1.15 overhead, 120 min split cap — hardcoded) | — |

There is no STEP 9 here. Feed emission happens one layer up: runDomainPipeline() calls emitCurriculumFeed() for the product branch, and the knowledge / hybrid orchestrators call it for theirs.

Knowledge Graph Pipeline (concept-driven)

generateKnowledgeGraph() — the LLM work. Step ids as logged:

| Step | What happens | Fail mode | |---|---|---| | STEP_1 | parse the syllabus into structured units/topics, then LLM-decompose the subject into concepts, prerequisites, problem_types, categories | syllabus truncated at KG_MAX_SYLLABUS_CHARS (default 32000) + warning | | STEP_2 | validate & clean — description ≥ 50 chars; keyword registry fail-closed (empty keywords are derived from name/description; nothing derivable → throw); dangling prerequisite edges removed | throw when keywords cannot be derived | | STEP_2_5 | only when knowledgeTreePath is supplied — standardize every concept against mlo-knowlege-tree.tsv into standardized or proposed_new; adopted tech-stack keywords merge into the registry; tree prerequisite codes seed internal edges | missing decision for a concept → throw | | STEP_3 | detectPrerequisiteCycles() → breakPrerequisiteCycles() at the weakest edge | warnings; edges are removed | | STEP_4 | topologicalSort() (Kahn) — learning_path is rebuilt from the sorted order | incomplete sort → warning | | STEP_5 | hierarchical ULO/CIO/SIO authoring + deterministic ulo_code / cio_code / sio_codes (a STEP_2_5 tree code always wins) | — | | STEP_5b | mine SIO text for tech identifiers (backticked tokens, @Annotations, a.b(), .modifiers, camelCase, cap 8) → merge into keywords | — | | STEP_5c | minute calibration — each signal normalised against its own cap before weighting (0.30·sio + 0.25·problem_types + 0.20·prereqs + 0.25·highBloom, all over min(count/4, 1)), minutes = 15 + 45·score ⇒ 15–60 min. Overrides the LLM only when the two diverge by more than 50% | — | | STEP_6 | verifyKnowledgeGraph() — referential integrity + reverse coverage (every syllabus topic covered by ≥ 1 concept) | warnings, never throws |

Feed emission is not a step of generateKnowledgeGraph() — it is the next line of runKnowledgeGraphPipeline() in src/pipeline/.

Hybrid Graph Pipeline (both)

generateHybridGraph() — see Layer 2 · Branch C for the annotated diagram. STEP_1 links, STEP_2 phases (up to 3 repair attempts, then throw), STEP_3 totals.


AI Agent Guide

This section is specifically for AI agents that need to use domain-kit programmatically.

When to Use domain-kit

Use domain-kit when you need to:

  1. Analyze a project and understand what it builds, what technologies it uses, what concepts it teaches
  2. Decompose a subject into structured learning concepts with prerequisites
  3. Generate a domain graph that curriculum-kit can consume to create lessons, activities, assessments

Step-by-Step Usage

Step 1: Detect the Domain

Always start here. This tells you what kind of input you have and what graph type to generate.

import { detectDomainProfile } from '@thanh01.pmt/domain-kit';

const profile = detectDomainProfile({
  // Provide whatever the user gave you:
  repoUrl: userRepoUrl,
  goal: use