---
name: graphnet-create
description: >
Convert source material (article, lecture notes, textbook chapter, video transcript, or any
topic the user names) into a GraphLMS **course** learning graph in lean JSON v3 format.
Identifies atomic concepts, builds prerequisite edges, writes multiple-choice quizzes with
misconception traps. Outputs a JSON file ready to import into a project of type "Курс" via
"Импорт JSON" in the project settings panel. Use when the user asks to "сделать курс",
"сделать обучающий граф", "разбить материал", "сделать урок", "build a learning graph",
or hands you content to atomize for teaching. For personal wikis (read-only, no quizzes,
topical organization) use the `graphnet-wiki` skill instead.
---
# GraphLMS Content Builder — Course Mode
Turn educational source material into a graph of small, learnable units that GraphLMS
(а PWA + Telegram Mini App for learning via knowledge graphs) can import as a **course**.
> **Project type:** import the produced JSON into a project created with type **«Курс»**.
> A separate skill (`graphnet-wiki`) handles wiki-type projects (no quizzes, no scoring,
> topical browsing). The JSON wire format is the same — but a course leans on quizzes
> + prerequisite edges, while a wiki ignores quizzes and treats edges as related-to links.
## Mental model
GraphLMS treats learning as **traversing a directed graph of atomic ideas**:
- A **graph** is one topic. Multiple graphs make a **project** (e.g. "Линейная алгебра",
"Эконометрика для социологов"). Студенты учатся внутри проекта. A project is either
a **course** (this skill) or a **wiki** (`graphnet-wiki` skill).
- A **node** is **one atomic idea**, learnable in 2–5 minutes of focused attention.
- An **edge** A → B means *"you need to understand A before B is learnable"* (prerequisite).
Direction is the only semantic — there are no edge "types"; the picker and progress
rules look only at direction.
- Each node carries **content** (markdown / images / audio / files) and exactly one **quiz**
(multiple choice, one correct, optional misconception trap).
- Студент видит одну ноду в момент. Правильный ответ помечает ноду `mastered` и
открывает следующие. Неправильный — `failed` (можно повторить). Попадание в
**misconception trap** — `misconception`, что **блокирует** все ноды ниже по графу:
студент несёт системно ложное представление, его нельзя пускать дальше пока не исправит.
The smaller and more atomic the nodes, the better the system works — it's easier to
diagnose what a student doesn't understand when each node is one tight idea.
## Output format — lean JSON v3
The lean format is deliberately minimal: only content, no display state. The app
fills in positions / timestamps / view settings on import.
### Single graph
```json
{
"version": 3,
"name": "Topic name",
"defaultLayout": "force",
"nodes": [ /* see below */ ],
"edges": [ /* see below */ ]
}
```
### Project bundle (multi-graph)
```json
{
"kind": "project",
"version": 2,
"name": "Project name",
"exportedAt": "2026-04-30T10:00:00Z",
"graphs": [ /* lean graph objects */ ]
}
```
### Node
```json
{
"id": "limits-intro",
"label": "Что такое предел",
"sections": [
{ "type": "text", "markdown": "Предел f(x) при x → a — это значение, к которому стремится f(x), когда x приближается к a." },
{ "type": "image", "svg": "" },
{ "type": "image", "name": "limit-diagram.png", "caption": "График функции с пределом" }
],
"quiz": {
"question": "Чему равен lim(x→2) x²?",
"options": ["2", "4", "0", "x²"],
"correctIndex": 1,
"misconceptionIndex": 3
},
"tags": ["calculus", "limits"]
}
```
**Required:** `id`, `label`. Everything else is optional.
**`id` rules:** kebab-case, ASCII, unique within the graph. Used for edge references and
URL fragments. Keep stable — changing an id breaks any cross-graph `portalTo` links.
> **About `color` on a node:** the lean schema accepts a per-node `color` field, but the
> live UI ignores it. Node fill color is now driven by **tags** (per-project Tag entities
> with hex colors) — see "Tags & colors" below. Don't emit `color` on nodes.
### Edge
```json
{ "source": "limits-intro", "target": "limits-properties" }
```
**Required:** `source`, `target` (must reference existing node `id`s in the same graph).
**Optional:** `label` (rare, only if a relation needs naming for the reader).
> **Don't emit a `type` field on edges.** The schema has it as a legacy holdover but no
> behaviour depends on it; setting it just adds visual noise (icons on the canvas) without
> changing how the picker, scoring, or progress works.
### Section types
| Type | Fields | Use for |
|---|---|---|
| `text` | `markdown` | Prose, formulas (TeX in markdown), lists, code blocks |
| `image` | `svg` OR `name` OR `fileId` (+ `caption?`) | Diagrams, photos. SVG inline (preferred for AI), raster by filename |
| `audio` | `name` OR `fileId` (+ `title?`) | Lectures, pronunciation samples |
| `media` | `embedUrl?` (YouTube/VK) OR `embedHtml?` | Embedded video / iframe |
| `file` | `name` OR `fileId` + `fileName` + `fileSize` | Downloadable PDFs, datasets |
For raster/audio/file: emit `name` referencing a filename. The user uploads these
files via "Загрузить медиа" in project settings, with the same name. **Names are
unique per project** — collisions fail the upload.
### Tags & colors
Tags in GraphLMS are now **first-class entities scoped to the project**: each tag has a
`label` and a `color` (hex). The first tag on a node drives its **fill color** on the
canvas in author and wiki views (in course-mode learner view, status colors win, so
tag color is hidden during a study session).
The lean JSON v3 file format still carries tags as **a string array per node**
(`"tags": ["calculus", "limits"]`). The Tag entities themselves are not in the JSON —
they live in PocketBase, scoped to the project. After import, the user creates Tag
records with colors via the node editor (NodePanel → "Теги" → "Создать тег"). The
strings on imported nodes do **not** auto-link to created Tag records yet — that's a
known v1 limitation; the user manually re-tags imported nodes if they want colored
chips.
**What this means for your output:**
- Emit tags as strings, as before: `"tags": ["category", "subcategory"]`.
- Use 1–3 tags per node. The **first** tag is the "primary" — it'll drive the node's
fill color, so put the most informative category first.
- Across a graph, prefer ~3–8 distinct tag labels — fewer is better for color
legibility.
- In your **final user message**, include a "Suggested tag palette" section listing
each unique tag label with a recommended hex color. Pick colors that match the
domain semantics where possible (e.g. red for warnings, blue for fundamentals,
green for applied). Avoid clashing colors and white/near-black on default theme.
Example: `- "calculus" → #3b82f6`. The user copies the colors when creating the
Tag entities in the editor.
### Quiz
```json
{
"question": "Plain text question",
"options": ["A", "B", "C", "D"],
"correctIndex": 2,
"misconceptionIndex": 0
}
```
- `options`: 3–5 strings. 4 is the sweet spot.
- `correctIndex`: 0-based index of the right answer.
- `misconceptionIndex`: 0-based index of the trap. Optional. Must differ from `correctIndex`.
## How to decompose any content
1. **Read the whole source first.** Do not start chopping until you have the full picture
and can identify what the student needs to walk away with.
2. **List candidate concepts.** Brainstorm ~20–50 ideas. Don't filter yet.
3. **Atomize.** A node is atomic when:
- A reasonably motivated student can absorb it in **2–5 minutes**.
- You can write **one** quiz question that distinguishes mastery from confusion.
- Removing any sentence from the explanation breaks understanding.
Symptoms of bad atomicity:
- **Too coarse:** "Calculus" — split into Limits / Derivatives / Integrals / etc.
- **Too fine:** separate nodes for "+" and "−" — merge into "Arithmetic operations".
- **Mixed concerns:** "Vectors and matrices" — split.
- **Two distinct techniques bundled** (e.g. "Acronyms and acrostics") — split into
separate nodes unless you can justify why they share one quiz question.
4. **Order by prerequisites.** For each pair (A, B) ask: *"If a student doesn't know A,
does B make sense?"* If no → A → B edge. Be conservative: too many edges is better
than too few; the picker uses them to recommend the next node.
5. **Check for cycles.** A → B → A makes the graph unlearnable. Sort topologically; if
you can't, you have a cycle. Break it by either (a) merging the cycle into one node
or (b) inverting one edge.
6. **Sanity-check coverage.** The graph should let a student starting from zero reach
every concept by following edges. Identify root nodes (no incoming edges) — these
are entry points.
7. **Domain-expert pass.** Before writing JSON, ask yourself: *"If a domain expert read
this graph as 'introductory ', what 1–3 fundamental concepts would they say
are missing?"* Add them, or list them in the final message to the user as known gaps.
## How to write quiz questions
Each node's quiz should **discriminate mastery** — getting it right means the student
*understands*, not just memorized.
### Patterns that work
- **Apply to a concrete example.** "If f(x) = 3x², what is f'(x)?"
- **Distinguish from a similar concept.** "Median vs mean — which is robust to outliers?"
- **Predict an outcome.** "What happens to inflation if the central bank raises rates?"
- **Spot the error.** "Find the bug in this proof / code / argument."
### Anti-patterns — strict bans
- ❌ **Pure recall.** If your question can be rephrased as "Which definition / statement
is true?" or "What is the definition of X?" — rewrite it as application or
distinguishing. Recognising the definition isn't understanding.
- ❌ **Yes/no questions** — only 2 effective options, low signal.
- ❌ **Trick wording / negation chains** — tests reading, not the concept.
### Distractor quality bar
For each wrong option, ask: *"Could a real, motivated student plausibly choose this
believing it's correct?"* If no → it's a filler, replace it.
Filler examples to avoid (all real outputs from a previous run):
- ❌ "Запомнить только цифру 8 — она важнее" (no one would think this)
- ❌ "Использовать образ цифры 28 как единого символа" (nonsense)
- ❌ "Чтобы фраза была написана на иностранном языке" (silly)
- ❌ "Чтобы каждое слово было существительным" (random)
A 4-option quiz with 2 silly distractors is effectively a 2-option quiz. **Aim for
1 correct + 1 misconception trap (when applicable) + 2 plausibly-wrong options.**
### The misconception trap (`misconceptionIndex`)
This is the **most powerful pedagogical feature** of GraphLMS. Use it deliberately.
A misconception trap is a wrong option that represents a **common, systematic
student error** — a *predictable wrong mental model*. Selecting it doesn't just
mark the node failed; it sets `misconception` status, which **blocks all
downstream nodes** because the student is now operating on a wrong premise that
will pollute everything they learn next.
#### Specificity gate — run before setting `misconceptionIndex`
Write down, in **one sentence**: *"A student who selects this option believes that ___."*
If you can't fill in that blank with a specific wrong belief — **don't set
`misconceptionIndex`**. Leave the option as a regular distractor (causes `failed`,
which is retryable, not blocking).
Concrete examples:
| Question | Trap option | Wrong belief filled in | Set trap? |
|---|---|---|---|
| "lim(x→0) sin(x)/x = ?" | "0" | *"0 / 0 = 0"* | ✅ yes |
| "Pearson r = 0.8 means..." | "X causes Y" | *"correlation implies causation"* | ✅ yes |
| "What makes an image memorable?" | "shorter is better" | (can't name a wrong belief) | ❌ no |
| "Loci method's advantage?" | "doesn't need imagery" | (no one believes this) | ❌ no |
#### Trap-theme diversification
Across a graph, **vary the trap themes**. If 5 quizzes all use "passive repetition is
enough" as the trap, students learn to avoid that option pattern after the first node
and the trap stops diagnosing anything.
**Hard rule:** No single misconception theme should be the trap in more than ~30%
of the graph's quizzes. Before emitting JSON, scan the traps you've set and
ask: *"How many distinct wrong beliefs am I testing here?"* If the answer is fewer
than half the trap count, rewrite some traps or drop them to plain distractors.
## Media handling
- **SVG diagrams**: write inline in `sections[].svg`. AI-friendly, no upload needed.
**Use them.** A graph with ≥10 nodes and zero visual sections is leaving signal on
the table — at least 1–2 nodes should have SVG diagrams (timeline, decision tree,
state diagram, simple chart). For procedural / spatial / structural concepts, a
diagram is often clearer than 200 words of prose.
- **Raster images, audio, files**: emit `sections[].name` referencing a filename. The
user uploads matching files via "Загрузить медиа" in the project settings panel
before/after import. Names must be unique within the project.
- **Embeds (YouTube, etc.)**: use `{ "type": "media", "embedUrl": "https://youtu.be/..." }`.
- **Plain text with formatting**: `{ "type": "text", "markdown": "..." }`.
## Working flow
When invoked, follow these steps:
1. **Clarify.** Ask the user (skip questions where the answer is obvious from context):
- Source material? (paste / file path / URL / topic name to research)
- Target audience? (beginner / intermediate / expert) — affects atomicity
- Project name? Or single-graph?
- Language? (default: detect from source; Russian if unclear)
- Output path? (default: `./graphnet-output/.json`)
2. **Read the source thoroughly.** If it's a file, read it. If it's a URL, fetch it.
If it's a topic, lean on what you know — but be honest about gaps.
3. **Sketch the graph.** In a scratch list, write candidate node labels. Map
prerequisites between them. **Show this sketch to the user before writing JSON**
so they can correct the structure before you spend tokens on content.
4. **Validate the sketch:**
- Atomic? (each node 2–5 min, one quiz question fits, no two-techniques-in-one)
- Acyclic? (topologically sortable)
- Complete? (domain-expert pass — see decomposition step 7)
- Right granularity? (5–30 nodes per graph; split into multiple if larger)
5. **Write content + quiz for each node.** Keep markdown short and direct (3–7
sentences plus optional formula/list). For formulas, use TeX-style notation in
markdown (the renderer is lightweight — keep it simple: `*x²*` style or unicode
super/subscripts for basic cases). Plan SVG diagrams for at least 1–2 nodes if
the topic has visual structure.
6. **Emit the JSON file** at the chosen path. Use `Write` tool, not console output.
7. **Tell the user the next steps:**
- Create the project as **«Курс»** (the wiki type would hide quizzes and the
scoring picker — different skill).
- Open the project's settings → Импорт JSON → select the file
- List any media filenames referenced via `name` that they need to upload via
"Загрузить медиа" — names must match exactly
- **Suggested tag palette:** for every distinct tag label you used, propose a hex
color. The user creates these via NodePanel → "Теги" → "Создать тег" and
re-tags imported nodes if they want colored chips on the canvas.
- Mention any nodes where you weren't confident about the misconception trap so
they can review
- List any domain-expert gaps you noticed but chose not to add
## Quality checklist (run before writing the file)
Structure:
- [ ] Every node has `id` and `label`
- [ ] All node `id`s are unique
- [ ] Every edge's `source` and `target` reference an existing node `id`
- [ ] No edges have a `type` field
- [ ] No circular dependencies (topologically sortable)
- [ ] Total nodes per graph: 5–30. Larger → split into a project bundle
- [ ] No node combines two distinct techniques without justification
Quizzes:
- [ ] Each quiz has 3–5 options, `correctIndex` in bounds
- [ ] Each `misconceptionIndex` (if set) is in bounds and ≠ `correctIndex`
- [ ] No quiz is pure recall — each tests application / distinguishing / prediction
- [ ] No silly / filler distractors — every wrong option is plausibly choosable
- [ ] For every trap set, you can name the specific wrong belief in one sentence
- [ ] Trap themes are diversified — no single theme dominates >30% of traps
Content:
- [ ] Content language matches the source / user request
- [ ] Markdown is concise and well-formatted (no raw HTML in `text` sections)
- [ ] At least 1–2 nodes have SVG / visual sections if the topic warrants it
Tags:
- [ ] Each node has 1–3 tags; primary (first) tag is the most informative category
- [ ] Total distinct tag labels in the graph is ≤ 8 (color legibility)
- [ ] No `color` field on nodes (deprecated; tag colors win)
- [ ] Final user message includes a "Suggested tag palette" with hex colors
## Compact full example
A two-node graph teaching introductory derivatives:
```json
{
"version": 3,
"name": "Производные — основы",
"defaultLayout": "force",
"nodes": [
{
"id": "rate-of-change",
"label": "Скорость изменения",
"sections": [
{
"type": "text",
"markdown": "**Скорость изменения** функции — насколько быстро меняется её выход при изменении входа. Если f(t) — пройденный путь, то скорость — это отношение Δf/Δt за маленький промежуток времени."
}
],
"quiz": {
"question": "Машина едет 60 км/ч. Что описывает это число?",
"options": [
"Положение машины в момент времени",
"Насколько быстро машина проезжает расстояние",
"Изменение скорости — ускорение",
"Среднюю скорость за поездку"
],
"correctIndex": 1,
"misconceptionIndex": 3
},
"tags": ["calculus", "fundamentals"]
},
{
"id": "derivative-def",
"label": "Определение производной",
"sections": [
{
"type": "text",
"markdown": "Производная — это **мгновенная** скорость изменения. Формально:\n\nf'(x) = lim(h→0) (f(x+h) − f(x)) / h\n\nЭто предел отношения Δf/Δh, когда промежуток Δh стягивается в ноль."
}
],
"quiz": {
"question": "Производная функции f(x) = 5x² в точке x = 3 равна:",
"options": ["5", "30", "10", "15"],
"correctIndex": 1,
"misconceptionIndex": 0
},
"tags": ["calculus", "fundamentals"]
}
],
"edges": [
{ "source": "rate-of-change", "target": "derivative-def" }
]
}
```
In the second quiz, the trap `"5"` represents the wrong belief *"the derivative of a
constant times x² is the constant itself"* (forgetting the power rule). The third
option `"10"` is plausibly-wrong (computed at x = 1 by mistake). The fourth `"15"` is
plausibly-wrong (multiplication of 5 × 3 without applying the rule). All three wrong
options are choosable; only the first is a **systematic** error worth marking as a
misconception.
## Reference
The full schema and validators live in `src/entities/graph/model/fileSchema.ts`
(`parseLeanGraphFile`, `parseLeanProjectFile`). Cross-check there if you hit a
validation error after import.