@arcaneorion/dsh-teaching-board 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +35 -4
  2. package/cordis.patch.yml +6 -2
  3. package/package.json +5 -3
  4. package/skills/stage-panel/SKILL.md +107 -0
  5. package/skills/stage-panel/references/diagram-selection.md +80 -0
  6. package/skills/stage-panel/references/html-patterns.md +93 -0
  7. package/skills/stage-panel/references/visual-style.md +43 -0
  8. package/skills/stage-panel/submode/code/SKILL.md +17 -0
  9. package/skills/stage-panel/submode/code/examples/go-hello-channel.json +36 -0
  10. package/skills/stage-panel/submode/code/examples/go-http-api-basics.json +58 -0
  11. package/skills/stage-panel/submode/code/examples/py-agent-loop.json +79 -0
  12. package/skills/stage-panel/submode/code/examples/py-ai-parameters-intro.json +65 -0
  13. package/skills/stage-panel/submode/code/examples/py-fizzbuzz-intro.json +74 -0
  14. package/skills/stage-panel/submode/code/examples/py-training-loop.json +114 -0
  15. package/skills/stage-panel/submode/code/examples/rs-move-basics.json +118 -0
  16. package/skills/stage-panel/submode/code/references/host-toolchains.md +61 -0
  17. package/skills/stage-panel/submode/code/schemas/lesson.schema.json +225 -0
  18. package/skills/stage-panel/submode/code/templates/lab.html +561 -0
  19. package/skills/stage-panel/submode/game-study/SKILL.md +16 -0
  20. package/skills/stage-panel/submode/game-study/examples/hft-microstructure.json +60 -0
  21. package/skills/stage-panel/submode/game-study/examples/securities-law.json +57 -0
  22. package/skills/stage-panel/submode/game-study/schemas/quest.schema.json +95 -0
  23. package/skills/stage-panel/submode/game-study/templates/quiz.html +516 -0
  24. package/skills/stage-panel/submode/learn/SKILL.md +16 -0
  25. package/skills/stage-panel/submode/learn/examples/epsilon-delta.json +140 -0
  26. package/skills/stage-panel/submode/learn/examples/template-showcase.json +147 -0
  27. package/skills/stage-panel/submode/learn/schemas/lesson.schema.json +184 -0
  28. package/skills/stage-panel/submode/learn/templates/lesson.html +782 -0
  29. package/skills/stage-panel/submode/ml/SKILL.md +9 -0
  30. package/skills/stage-panel/submode/ml/examples/gd.json +7 -0
  31. package/skills/stage-panel/submode/ml/templates/gd.html +217 -0
  32. package/skills/stage-panel/submode/quant/SKILL.md +9 -0
  33. package/skills/stage-panel/submode/quant/references/export_backtest_data.py +192 -0
  34. package/skills/stage-panel/submode/quant/templates/dashboard.html +381 -0
  35. package/src/client.js +177 -15
  36. package/src/index.js +11 -0
  37. package/src/skills.js +88 -0
@@ -0,0 +1,65 @@
1
+ {
2
+ "schema_version": 1,
3
+ "id": "py-ai-parameters-intro",
4
+ "title": "AI 的参数:模型如何学习",
5
+ "subtitle": "从最简单的例子理解 AI 参数调整",
6
+ "summary": "用一个简单的温度预测例子,理解 AI 模型的参数(权重和偏置)如何影响预测结果。不用复杂语法,只用基础 Python。",
7
+ "domain": "machine-learning",
8
+ "lang": "python",
9
+ "level": "intro",
10
+ "objectives": [
11
+ "理解什么是模型参数(权重 w 和偏置 b)",
12
+ "看到参数改变如何影响预测结果",
13
+ "理解预测值和真实值的误差"
14
+ ],
15
+ "starter": {
16
+ "filename": "ai_params.py",
17
+ "code": "# AI 模型的核心:参数\n# 假设我们要预测温度:输入小时数,输出温度\n\n# 真实数据:(小时, 温度)\ndata = [\n (6, 15),\n (12, 25),\n (18, 20),\n]\n\n# 模型参数(这就是 AI 要学习的东西)\nw = 1.0\nb = 10.0\n\nprint(\"当前参数\")\nprint(\"权重 w =\", w)\nprint(\"偏置 b =\", b)\nprint()\n\n# 用当前参数做预测\nprint(\"预测结果\")\nfor hour, real_temp in data:\n predicted_temp = w * hour + b\n error = predicted_temp - real_temp\n print(\"小时\", hour, \": 真实温度\", real_temp, \"预测温度\", predicted_temp, \"误差\", error)\n\nprint()\nprint(\"参数不好,预测就不准!\")\n"
18
+ },
19
+ "run": {},
20
+ "activities": [
21
+ {
22
+ "id": "predict-output",
23
+ "type": "predict",
24
+ "prompt": "运行前先预测:当 w=1.0, b=10.0 时,中午12点的预测温度是多少度?",
25
+ "required": true,
26
+ "before_run": true
27
+ },
28
+ {
29
+ "id": "run-and-observe",
30
+ "type": "trace",
31
+ "prompt": "点击运行,观察输出。预测值和真实值的误差有多大?哪个时间点的误差最大?",
32
+ "required": false
33
+ },
34
+ {
35
+ "id": "adjust-params",
36
+ "type": "implement",
37
+ "prompt": "试着修改参数 w 和 b,再运行一次。看看误差有没有变小?",
38
+ "required": true
39
+ },
40
+ {
41
+ "id": "explain-params",
42
+ "type": "explain",
43
+ "prompt": "用自己的话解释:什么是模型参数?为什么调整参数能改变预测结果?",
44
+ "required": true
45
+ }
46
+ ],
47
+ "checks": [
48
+ {
49
+ "id": "check-runs",
50
+ "label": "代码能运行",
51
+ "type": "exit_code",
52
+ "expected": 0
53
+ },
54
+ {
55
+ "id": "check-output",
56
+ "label": "输出包含预测结果",
57
+ "type": "stdout_contains",
58
+ "expected": "预测温度"
59
+ }
60
+ ],
61
+ "submission": {
62
+ "require_run": true,
63
+ "require_reflection": true
64
+ }
65
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "schema_version": 1,
3
+ "id": "py-fizzbuzz-intro",
4
+ "title": "Python:先跑起来再改 FizzBuzz",
5
+ "subtitle": "用一次成功运行建立 Code Lab 手感",
6
+ "lang": "python",
7
+ "langs": [
8
+ "python"
9
+ ],
10
+ "level": "intro",
11
+ "stage": "编辑 → 运行 → 改输出",
12
+ "summary": "先让脚本打印 1..15 的 FizzBuzz,再按练习改规则。运行在本机 python3 上完成。",
13
+ "objectives": [
14
+ "成功运行 starter 并看到输出",
15
+ "把 15 的倍数改成同时打印 FizzBuzz",
16
+ "向导师说明 if 顺序为什么重要"
17
+ ],
18
+ "focus": {
19
+ "concept": "control-flow",
20
+ "trap": "wrong-if-order"
21
+ },
22
+ "starter": {
23
+ "filename": "main.py",
24
+ "code": "def fizzbuzz(n: int) -> str:\n if n % 3 == 0:\n return \"Fizz\"\n if n % 5 == 0:\n return \"Buzz\"\n return str(n)\n\n\nif __name__ == \"__main__\":\n for i in range(1, 16):\n print(fizzbuzz(i))\n"
25
+ },
26
+ "snippets": [
27
+ {
28
+ "id": "main",
29
+ "lang": "python",
30
+ "title": "FizzBuzz starter",
31
+ "filename": "main.py",
32
+ "role": "primary",
33
+ "editable": true,
34
+ "code": "def fizzbuzz(n: int) -> str:\n if n % 3 == 0:\n return \"Fizz\"\n if n % 5 == 0:\n return \"Buzz\"\n return str(n)\n\n\nif __name__ == \"__main__\":\n for i in range(1, 16):\n print(fizzbuzz(i))\n",
35
+ "annotations": [
36
+ {
37
+ "line": 2,
38
+ "kind": "warn",
39
+ "text": "只判断 %3 时,15 会过早返回 Fizz"
40
+ },
41
+ {
42
+ "line": 10,
43
+ "kind": "info",
44
+ "text": "入口:打印 1..15"
45
+ }
46
+ ]
47
+ }
48
+ ],
49
+ "run": {
50
+ "snippet_id": "main",
51
+ "filename": "main.py"
52
+ },
53
+ "exercises": [
54
+ {
55
+ "id": "e1",
56
+ "type": "predict_output",
57
+ "prompt": "当前实现对 15 会打印什么?",
58
+ "snippet_id": "main"
59
+ },
60
+ {
61
+ "id": "e2",
62
+ "type": "fix_code",
63
+ "prompt": "改到 15 打印 FizzBuzz,且 3/5 仍正确。",
64
+ "snippet_id": "main"
65
+ }
66
+ ],
67
+ "hints": [
68
+ "先处理 n % 15 == 0(或同时整除 3 与 5)。",
69
+ "改完点「运行」,看 1..15 整表。"
70
+ ],
71
+ "next": [
72
+ "py-list-comprehension"
73
+ ]
74
+ }
@@ -0,0 +1,114 @@
1
+ {
2
+ "schema_version": 1,
3
+ "id": "py-training-loop",
4
+ "title": "模型训练:亲手跑一次梯度下降",
5
+ "subtitle": "不用框架,直接观察参数、损失与预测如何更新",
6
+ "summary": "用纯 Python 拟合 y = 2x + 1。代码展示训练数据、前向计算、损失、梯度和参数更新之间的完整关系。",
7
+ "domain": "machine-learning",
8
+ "lang": "python",
9
+ "level": "intro",
10
+ "stage": "预测 → 训练 → 观察 → 迁移",
11
+ "objectives": [
12
+ "指出训练循环的五个组成部分",
13
+ "解释 loss 下降意味着什么",
14
+ "修改学习率并比较收敛"
15
+ ],
16
+ "prerequisites": [
17
+ "Python 循环和函数",
18
+ "导数的直觉"
19
+ ],
20
+ "starter": {
21
+ "filename": "train.py",
22
+ "code": "data = [(0.0, 1.0), (1.0, 3.0), (2.0, 5.0), (3.0, 7.0)]\n\nw = 0.0\nb = 0.0\nlr = 0.05\n\nfor step in range(201):\n predictions = [w * x + b for x, _ in data]\n errors = [pred - y for pred, (_, y) in zip(predictions, data)]\n loss = sum(error ** 2 for error in errors) / len(data)\n\n dw = 2 * sum(error * x for error, (x, _) in zip(errors, data)) / len(data)\n db = 2 * sum(errors) / len(data)\n w -= lr * dw\n b -= lr * db\n\n if step in {0, 20, 100, 200}:\n print(f\"step={step:3d} loss={loss:.6f} w={w:.4f} b={b:.4f}\")\n\nprint(f\"prediction={w * 5 + b:.2f}\")\n"
23
+ },
24
+ "snippets": [
25
+ {
26
+ "id": "main",
27
+ "lang": "python",
28
+ "title": "最小训练循环",
29
+ "filename": "train.py",
30
+ "role": "primary",
31
+ "editable": true,
32
+ "code": "data = [(0.0, 1.0), (1.0, 3.0), (2.0, 5.0), (3.0, 7.0)]\n\nw = 0.0\nb = 0.0\nlr = 0.05\n\nfor step in range(201):\n predictions = [w * x + b for x, _ in data]\n errors = [pred - y for pred, (_, y) in zip(predictions, data)]\n loss = sum(error ** 2 for error in errors) / len(data)\n\n dw = 2 * sum(error * x for error, (x, _) in zip(errors, data)) / len(data)\n db = 2 * sum(errors) / len(data)\n w -= lr * dw\n b -= lr * db\n\n if step in {0, 20, 100, 200}:\n print(f\"step={step:3d} loss={loss:.6f} w={w:.4f} b={b:.4f}\")\n\nprint(f\"prediction={w * 5 + b:.2f}\")\n",
33
+ "annotations": [
34
+ {
35
+ "line": 7,
36
+ "kind": "info",
37
+ "text": "前向计算:当前参数产生预测"
38
+ },
39
+ {
40
+ "line": 9,
41
+ "kind": "info",
42
+ "text": "目标函数:均方误差"
43
+ },
44
+ {
45
+ "line": 11,
46
+ "kind": "warn",
47
+ "text": "梯度决定参数应向哪个方向变化"
48
+ },
49
+ {
50
+ "line": 13,
51
+ "kind": "info",
52
+ "text": "优化器最小形式:参数减去学习率乘梯度"
53
+ }
54
+ ]
55
+ }
56
+ ],
57
+ "run": {
58
+ "snippet_id": "main",
59
+ "filename": "train.py"
60
+ },
61
+ "activities": [
62
+ {
63
+ "id": "predict-loss",
64
+ "type": "predict",
65
+ "prompt": "训练过程中 loss 应整体上升还是下降?w 和 b 最终应接近什么值?",
66
+ "required": true,
67
+ "before_run": true
68
+ },
69
+ {
70
+ "id": "trace-loop",
71
+ "type": "trace",
72
+ "prompt": "用自己的话串起:数据 → 预测 → 误差 → loss → 梯度 → 参数更新。",
73
+ "required": true
74
+ },
75
+ {
76
+ "id": "transfer-lr",
77
+ "type": "transfer",
78
+ "prompt": "把 lr 改为 0.5 和 0.005 分别运行。比较收敛速度或不稳定现象。",
79
+ "required": true
80
+ }
81
+ ],
82
+ "checks": [
83
+ {
84
+ "id": "exit-ok",
85
+ "label": "训练脚本正常退出",
86
+ "type": "exit_code",
87
+ "expected": 0
88
+ },
89
+ {
90
+ "id": "has-loss",
91
+ "label": "输出包含训练损失",
92
+ "type": "stdout_contains",
93
+ "expected": "loss="
94
+ },
95
+ {
96
+ "id": "predict-eleven",
97
+ "label": "模型对 x=5 的预测接近 11",
98
+ "type": "stdout_contains",
99
+ "expected": "prediction=11.00"
100
+ }
101
+ ],
102
+ "hints": [
103
+ "线性模型只有两个参数:斜率 w 和截距 b。",
104
+ "学习率过大可能越过最低点,过小则需要更多步。"
105
+ ],
106
+ "submission": {
107
+ "require_run": true,
108
+ "require_reflection": true
109
+ },
110
+ "next": [
111
+ "py-autograd",
112
+ "py-train-validation-split"
113
+ ]
114
+ }
@@ -0,0 +1,118 @@
1
+ {
2
+ "schema_version": 1,
3
+ "id": "rs-move-basics",
4
+ "title": "Rust move:为什么 s 不能再读",
5
+ "subtitle": "用一次编译失败学会所有权转移",
6
+ "lang": "rust",
7
+ "level": "intro",
8
+ "stage": "编辑 → 运行 → 读编译错误",
9
+ "summary": "Rust 默认 move。String 被赋值给另一个变量后,旧名字不能再使用;编译器在运行前拦住 use-after-move。Python 同一写法只是多了一个引用名。",
10
+ "objectives": [
11
+ "指出哪一行发生 move",
12
+ "根据 E0382 说明为什么第 3 行非法",
13
+ "用一句话对比 Python 的引用语义"
14
+ ],
15
+ "focus": {
16
+ "concept": "ownership-move",
17
+ "trap": "use-after-move"
18
+ },
19
+ "starter": {
20
+ "filename": "main.rs",
21
+ "code": "fn main() {\n let s = String::from(\"hi\");\n let t = s;\n println!(\"{s}\");\n}\n"
22
+ },
23
+ "snippets": [
24
+ {
25
+ "id": "main",
26
+ "lang": "rust",
27
+ "title": "会编译失败的例子",
28
+ "filename": "main.rs",
29
+ "role": "primary",
30
+ "editable": true,
31
+ "code": "fn main() {\n let s = String::from(\"hi\");\n let t = s;\n println!(\"{s}\");\n}\n",
32
+ "annotations": [
33
+ {
34
+ "line": 2,
35
+ "kind": "info",
36
+ "text": "在堆上创建 String,s 拥有它"
37
+ },
38
+ {
39
+ "line": 3,
40
+ "kind": "move",
41
+ "text": "所有权从 s 移到 t,s 失效"
42
+ },
43
+ {
44
+ "line": 4,
45
+ "kind": "error",
46
+ "text": "use after move → 编译期 E0382"
47
+ }
48
+ ]
49
+ }
50
+ ],
51
+ "run": {
52
+ "snippet_id": "main",
53
+ "filename": "main.rs"
54
+ },
55
+ "trace": {
56
+ "steps": [
57
+ {
58
+ "id": "s1",
59
+ "title": "创建 s",
60
+ "focus_snippet": "main",
61
+ "lines": [
62
+ 2
63
+ ],
64
+ "note": "s 拥有堆上的 String。"
65
+ },
66
+ {
67
+ "id": "s2",
68
+ "title": "move 到 t",
69
+ "focus_snippet": "main",
70
+ "lines": [
71
+ 3
72
+ ],
73
+ "note": "赋值转移所有权,不是共享。"
74
+ },
75
+ {
76
+ "id": "s3",
77
+ "title": "再读 s",
78
+ "focus_snippet": "main",
79
+ "lines": [
80
+ 4
81
+ ],
82
+ "note": "编译器拒绝;可改为用 t,或 s.clone(),或只传 &s。"
83
+ }
84
+ ]
85
+ },
86
+ "exercises": [
87
+ {
88
+ "id": "e1",
89
+ "type": "spot_bug",
90
+ "prompt": "标出会触发 use-after-move 的那一行(相对 main 片段)。",
91
+ "snippet_id": "main",
92
+ "hint": "move 发生在赋值之后的下一次使用。"
93
+ },
94
+ {
95
+ "id": "e2",
96
+ "type": "fix_code",
97
+ "prompt": "改到能编译并打印 hi。至少一种:打印 t、clone、或借用。",
98
+ "snippet_id": "main"
99
+ },
100
+ {
101
+ "id": "e3",
102
+ "type": "short",
103
+ "prompt": "为什么 Python 对照片段能直接 print(s)?"
104
+ }
105
+ ],
106
+ "hints": [
107
+ "先让 rustc 报错,再读 error code,比背规则快。",
108
+ "Copy 类型(如 i32)赋值是复制,不会 move;String 不是 Copy。"
109
+ ],
110
+ "solution": {
111
+ "code": "fn main() {\n let s = String::from(\"hi\");\n let t = s.clone();\n println!(\"{s}\");\n println!(\"{t}\");\n}\n",
112
+ "explanation": "clone 显式复制堆数据;或直接 println!(\"{t}\") 使用新主人;需要共享只读时用 &s。"
113
+ },
114
+ "next": [
115
+ "rs-borrow-intro",
116
+ "rs-copy-vs-clone"
117
+ ]
118
+ }
@@ -0,0 +1,61 @@
1
+ # 宿主工具链检测与安装提示
2
+
3
+ Code Lab **不**捆绑编译器。运行前检测本机工具;缺失则 `blocked` 并提示安装。
4
+
5
+ ## 检测命令(doctor 可用)
6
+
7
+ ```bash
8
+ python3 --version
9
+ uv --version
10
+ rustc --version
11
+ cargo --version
12
+ go version
13
+ ```
14
+
15
+ 项目信号文件:
16
+
17
+ | 语言 | 信号 |
18
+ |------|------|
19
+ | Python | `pyproject.toml`, `uv.lock`, `requirements.txt` |
20
+ | Rust | `Cargo.toml`, `Cargo.lock` |
21
+ | Go | `go.mod`, `go.sum` |
22
+
23
+ ## MVP runner 最小依赖
24
+
25
+ | profile | 需要 |
26
+ |---------|------|
27
+ | `python-script` | `python3` |
28
+ | `python-uv-script` | `uv`(推荐 NixOS/本环境) |
29
+ | `rust-rustc-single` | `rustc` |
30
+ | `go-run-single` | `go` |
31
+
32
+ `cargo` / `go test` 为 Phase 2。
33
+
34
+ ## NixOS 提示(示例,按用户 flake 调整)
35
+
36
+ ```bash
37
+ # 临时 shell 示例
38
+ nix-shell -p python3 uv rustc cargo go
39
+
40
+ # 或进入项目 devShell(若有 flake)
41
+ nix develop
42
+ ```
43
+
44
+ 不要在 skill 内 `nix-build` 出一套私有工具链当默认路径;应复用用户环境。
45
+
46
+ ## 库从哪里来
47
+
48
+ - **单文件课**:尽量 std only,降低依赖摩擦。
49
+ - **需要第三方库的课**:在**用户项目**里用 uv/cargo/go.mod 声明;lesson 文档写明前置。
50
+ - skill `examples/` 只放 lesson JSON,不 vendoring site-packages / target。
51
+
52
+ ## 失败语义
53
+
54
+ | 情况 | run status | 对用户 |
55
+ |------|------------|--------|
56
+ | 无 rustc | `blocked` | 显示缺失工具 + 安装提示 |
57
+ | 编译/运行失败 | `failed` | 展示 stderr,供教学 |
58
+ | 超时 | `timeout` | 提示减小输入或查死循环 |
59
+ | 成功 | `ok` | 展示 stdout |
60
+
61
+ 禁止在工具缺失时回退到「浏览器假执行」而不告知。
@@ -0,0 +1,225 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://localweb.dev/schemas/code.lesson.schema.json",
4
+ "title": "LocalWeb Code Lab Lesson",
5
+ "type": "object",
6
+ "additionalProperties": true,
7
+ "required": ["title", "lang"],
8
+ "properties": {
9
+ "schema_version": {
10
+ "type": "integer",
11
+ "const": 1
12
+ },
13
+ "id": {
14
+ "type": "string",
15
+ "pattern": "^[A-Za-z0-9_-]+$"
16
+ },
17
+ "title": {
18
+ "type": "string",
19
+ "minLength": 1
20
+ },
21
+ "subtitle": {
22
+ "type": "string"
23
+ },
24
+ "summary": {
25
+ "type": "string"
26
+ },
27
+ "lang": {
28
+ "type": "string",
29
+ "enum": ["python", "rust", "go"]
30
+ },
31
+ "langs": {
32
+ "type": "array",
33
+ "items": {
34
+ "type": "string",
35
+ "enum": ["python", "rust", "go"]
36
+ },
37
+ "description": "本课涉及的语言;跨语言对照时使用。"
38
+ },
39
+ "level": {
40
+ "type": "string",
41
+ "enum": ["intro", "intermediate", "advanced"]
42
+ },
43
+ "stage": {
44
+ "type": "string",
45
+ "description": "教学阶段标签,如 编辑练习 / 读错信息 / 对照语义"
46
+ },
47
+ "objectives": {
48
+ "type": "array",
49
+ "items": { "type": "string" }
50
+ },
51
+ "focus": {
52
+ "type": "object",
53
+ "additionalProperties": true,
54
+ "properties": {
55
+ "concept": { "type": "string" },
56
+ "trap": { "type": "string" }
57
+ }
58
+ },
59
+ "starter": {
60
+ "type": "object",
61
+ "description": "单文件课的快速写法;与 snippets 二选一或同时提供。",
62
+ "required": ["code"],
63
+ "additionalProperties": true,
64
+ "properties": {
65
+ "filename": { "type": "string" },
66
+ "code": { "type": "string" }
67
+ }
68
+ },
69
+ "snippets": {
70
+ "type": "array",
71
+ "items": {
72
+ "type": "object",
73
+ "required": ["id", "lang", "code"],
74
+ "additionalProperties": true,
75
+ "properties": {
76
+ "id": { "type": "string" },
77
+ "lang": {
78
+ "type": "string",
79
+ "enum": ["python", "rust", "go"]
80
+ },
81
+ "title": { "type": "string" },
82
+ "filename": { "type": "string" },
83
+ "code": { "type": "string" },
84
+ "role": {
85
+ "type": "string",
86
+ "enum": ["primary", "contrast", "solution", "tests"],
87
+ "default": "primary"
88
+ },
89
+ "editable": {
90
+ "type": "boolean",
91
+ "default": false
92
+ },
93
+ "annotations": {
94
+ "type": "array",
95
+ "items": {
96
+ "type": "object",
97
+ "required": ["line", "text"],
98
+ "additionalProperties": true,
99
+ "properties": {
100
+ "line": { "type": "integer", "minimum": 1 },
101
+ "kind": {
102
+ "type": "string",
103
+ "enum": ["info", "move", "error", "alias", "hint", "warn"]
104
+ },
105
+ "text": { "type": "string" }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ },
112
+ "run": {
113
+ "type": "object",
114
+ "additionalProperties": true,
115
+ "properties": {
116
+ "snippet_id": {
117
+ "type": "string",
118
+ "description": "默认展示哪个 snippet;缺省取 role=primary 或 starter。"
119
+ },
120
+ "filename": { "type": "string" }
121
+ }
122
+ },
123
+ "compare": {
124
+ "type": "object",
125
+ "additionalProperties": true,
126
+ "properties": {
127
+ "axes": {
128
+ "type": "array",
129
+ "items": { "type": "string" }
130
+ },
131
+ "rows": {
132
+ "type": "array",
133
+ "items": {
134
+ "type": "object",
135
+ "required": ["lang", "cells"],
136
+ "additionalProperties": true,
137
+ "properties": {
138
+ "lang": { "type": "string" },
139
+ "cells": {
140
+ "type": "array",
141
+ "items": { "type": "string" }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+ },
148
+ "trace": {
149
+ "type": "object",
150
+ "additionalProperties": true,
151
+ "properties": {
152
+ "steps": {
153
+ "type": "array",
154
+ "items": {
155
+ "type": "object",
156
+ "required": ["id", "title"],
157
+ "additionalProperties": true,
158
+ "properties": {
159
+ "id": { "type": "string" },
160
+ "title": { "type": "string" },
161
+ "focus_snippet": { "type": "string" },
162
+ "lines": {
163
+ "type": "array",
164
+ "items": { "type": "integer", "minimum": 1 }
165
+ },
166
+ "note": { "type": "string" }
167
+ }
168
+ }
169
+ }
170
+ }
171
+ },
172
+ "exercises": {
173
+ "type": "array",
174
+ "items": {
175
+ "type": "object",
176
+ "required": ["id", "type", "prompt"],
177
+ "additionalProperties": true,
178
+ "properties": {
179
+ "id": { "type": "string" },
180
+ "type": {
181
+ "type": "string",
182
+ "enum": [
183
+ "predict_output",
184
+ "spot_bug",
185
+ "fix_code",
186
+ "short",
187
+ "choice"
188
+ ]
189
+ },
190
+ "prompt": { "type": "string" },
191
+ "hint": { "type": "string" },
192
+ "snippet_id": { "type": "string" },
193
+ "choices": {
194
+ "type": "array",
195
+ "items": {
196
+ "type": "object",
197
+ "required": ["id", "label"],
198
+ "properties": {
199
+ "id": { "type": "string" },
200
+ "label": { "type": "string" }
201
+ }
202
+ }
203
+ }
204
+ }
205
+ }
206
+ },
207
+ "hints": {
208
+ "type": "array",
209
+ "items": { "type": "string" }
210
+ },
211
+ "solution": {
212
+ "type": "object",
213
+ "additionalProperties": true,
214
+ "description": "默认不渲染;由 agent 决定是否展示。",
215
+ "properties": {
216
+ "code": { "type": "string" },
217
+ "explanation": { "type": "string" }
218
+ }
219
+ },
220
+ "next": {
221
+ "type": "array",
222
+ "items": { "type": "string" }
223
+ }
224
+ }
225
+ }