dsh-project-based-learning 1.1.0
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.
- package/CHANGELOG.md +65 -0
- package/CONTRIBUTING.md +80 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/README.zh.md +118 -0
- package/cordis.patch.yml +15 -0
- package/docs/DESIGN-AUDIT.md +505 -0
- package/docs/ENGINE-REVISION-2.zh.md +487 -0
- package/docs/installing.zh.md +105 -0
- package/docs/original-workflow.zh.md +379 -0
- package/docs/releasing.zh.md +85 -0
- package/docs/review-round1-A-edu.zh.md +66 -0
- package/docs/review-round1-B-eng.zh.md +60 -0
- package/docs/review-round1-C-bounded.zh.md +55 -0
- package/docs/zero-knowledge-path.zh.md +60 -0
- package/examples/PROGRESS.demo.md +72 -0
- package/examples/state.demo.json +185 -0
- package/examples/state.selftest-invalid.json +58 -0
- package/lib/index.js +64 -0
- package/package.json +77 -0
- package/skills/dsh-coach/SKILL.md +108 -0
- package/skills/dsh-coach/assets/review-report.md +40 -0
- package/skills/dsh-coach/assets/stage-acceptance.md +51 -0
- package/skills/dsh-coach/assets/state.template.json +59 -0
- package/skills/dsh-coach/assets/task-card.md +29 -0
- package/skills/dsh-coach/references/domains/unity-csharp/archetypes.md +306 -0
- package/skills/dsh-coach/references/domains/unity-csharp/diagnosis-bank.md +978 -0
- package/skills/dsh-coach/references/domains/unity-csharp/example.md +356 -0
- package/skills/dsh-coach/references/domains/unity-csharp/glossary.md +110 -0
- package/skills/dsh-coach/references/domains/unity-csharp/manifest.yml +14 -0
- package/skills/dsh-coach/references/domains/unity-csharp/pitfalls.md +400 -0
- package/skills/dsh-coach/references/domains/unity-csharp/verification.md +308 -0
- package/skills/dsh-coach/references/engine/adapt.md +48 -0
- package/skills/dsh-coach/references/engine/diagnosis.md +76 -0
- package/skills/dsh-coach/references/engine/domain-contract.md +73 -0
- package/skills/dsh-coach/references/engine/intake.md +63 -0
- package/skills/dsh-coach/references/engine/permissions.md +44 -0
- package/skills/dsh-coach/references/engine/review-acceptance.md +67 -0
- package/skills/dsh-coach/references/engine/route.md +51 -0
- package/skills/dsh-coach/references/engine/state.md +116 -0
- package/skills/dsh-coach/references/engine/task-loop.md +68 -0
- package/skills/dsh-coach/scripts/coach-install.mjs +98 -0
- package/skills/dsh-coach/scripts/coach-selftest.mjs +205 -0
- package/skills/dsh-coach/scripts/coach-validate.mjs +817 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Review & Acceptance:审阅与验收
|
|
2
|
+
|
|
3
|
+
来源:原文 §八;改动经审核裁定 W07(检索式复述)、W08(证据充分性定义)、W12(依据与核对状态)、W18(userOnly 核对)。
|
|
4
|
+
|
|
5
|
+
## A. 审阅
|
|
6
|
+
|
|
7
|
+
### 问题分级
|
|
8
|
+
|
|
9
|
+
- **阻塞**:导致功能、数据或核心结论错误,无法继续;
|
|
10
|
+
- **重要**:当前可能工作,但存在明显风险;
|
|
11
|
+
- **建议**:可读性、命名、简化或长期质量问题。
|
|
12
|
+
|
|
13
|
+
### 每条反馈五要素 + 两项对照
|
|
14
|
+
|
|
15
|
+
1. 位置或对应内容;
|
|
16
|
+
2. 发生机制;
|
|
17
|
+
3. 实际影响;
|
|
18
|
+
4. 最小修复方向;
|
|
19
|
+
5. 修复后的验证方式;
|
|
20
|
+
6. **依据**:引用的材料——**知识类结论**用问答记录(题目编号+学员原话摘录)或文档章节;**行为类结论**用文件路径与行号、日志片段、截图位置、可复现步骤。必须具体到能核对;
|
|
21
|
+
7. **核对状态**:`已核对`(教练实际看过该材料)或 `推测`(未核对)。
|
|
22
|
+
|
|
23
|
+
**无法核对的判断必须降级为提问**,不得以断言形式给出。宁可少报一条,也不虚构一条"看起来很专业"的问题。
|
|
24
|
+
|
|
25
|
+
### 处理方式
|
|
26
|
+
|
|
27
|
+
优先让用户修复,不默认替用户重写全部成果。用户要求直接修时,按 `task-loop.md` 的"直接答案"路径处理并记录。
|
|
28
|
+
|
|
29
|
+
不要只询问"明白了吗"。应要求用户预测行为、处理变化、复现问题或解释为什么修复有效。
|
|
30
|
+
|
|
31
|
+
模板见 `assets/review-report.md`。
|
|
32
|
+
|
|
33
|
+
## B. 验收
|
|
34
|
+
|
|
35
|
+
> **适用范围(重要)**:本节的"充分证据"三条件**只用于阶段验收**;**单个知识结论、单题诊断不适用**——那类结论按"知识类/行为类"分层判定(见 `diagnosis.md` 的证据分层表)。本节同时被"审阅成果"指令引用(`SKILL.md` 映射表),**不得**把验收的严格度整体套到单题或单条结论上:不得因为"无法复现"就要求学生为一道概念题去跑一次实测。
|
|
36
|
+
|
|
37
|
+
### 三档结论
|
|
38
|
+
|
|
39
|
+
- **通过**:要求均有充分证据支持;
|
|
40
|
+
- **有条件通过**:核心成果成立,但仍有明确待修项;
|
|
41
|
+
- **未通过**:核心目标尚未实现或证据不足。
|
|
42
|
+
|
|
43
|
+
### "充分证据"的可操作定义
|
|
44
|
+
|
|
45
|
+
证据充分 = **可复现 + 可解释 + 可修改**(三条同时满足):
|
|
46
|
+
|
|
47
|
+
- 可复现:别人按提交的步骤/材料能得到同一结果;
|
|
48
|
+
- 可解释:用户能说明机制与取舍,而不只是"跑通了";
|
|
49
|
+
- 可修改:用户能按新要求改动它,并说明改动的后果。
|
|
50
|
+
|
|
51
|
+
缺任意一条 → 最高只能判"有条件通过"。核心目标未达成,或存在未清的**阻塞**项 → "未通过"。
|
|
52
|
+
|
|
53
|
+
**与单条结论的关系**:三条件针对**阶段成果**。单条结论的强度按类型判定——知识类看"无提示解释机制+迁移",行为类看运行结果或材料(见 `diagnosis.md`)。**单题回答正确不足以判"已验证",但也不需要为此要求实测。**
|
|
54
|
+
|
|
55
|
+
**不得用 `userOnly` 通道给概念题取证**:`route[].userOnly`(用户必须亲自完成的部分)用于**阶段的可交付成果**,不得把"概念题结论"塞进它来要求学员"亲手跑一次"——那与 R3/R10 的意图相反。概念类结论按知识类分层判定,不要求运行验证。
|
|
56
|
+
|
|
57
|
+
### 验收动作(缺一不可)
|
|
58
|
+
|
|
59
|
+
1. 逐项核对阶段的 `userOnly`(用户必须亲自完成的部分),未完成不得判"通过";
|
|
60
|
+
2. 逐项核对验收标准,每条给出证据引用;
|
|
61
|
+
3. **检索式复述**:请用户在不看资料的情况下复述本阶段的核心机制、一次关键取舍与一个失败模式,限时 5 分钟,结果写入 `retrievalRecap`;
|
|
62
|
+
4. AI 参与生成的成果,确认用户能解释、修改、验证后,才计入能力证据;
|
|
63
|
+
5. 给出结论与理由,并写明"通过后进入"或"待修项"。
|
|
64
|
+
|
|
65
|
+
### 记录
|
|
66
|
+
|
|
67
|
+
验收结果写入 `state.current`、`state.evidence[]` 与 `state.open[]`(未清的阻塞/重要项)。存在未清阻塞项时,校验器会拒绝把任务状态标为"已通过"。
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Route:阶段路线
|
|
2
|
+
|
|
3
|
+
来源:原文 §六;改动经审核裁定 W16、W18。
|
|
4
|
+
|
|
5
|
+
## 方向
|
|
6
|
+
|
|
7
|
+
以最终成果为方向,优先建立**可验证的最小闭环**。
|
|
8
|
+
|
|
9
|
+
- 不把完整大型项目作为第一个阶段;
|
|
10
|
+
- 不过早引入与当前成果无关的复杂框架;
|
|
11
|
+
- 首阶段必须能在有限时间内产出用户可亲眼看到的结果。
|
|
12
|
+
|
|
13
|
+
## 阶段定义(字段全部必填)
|
|
14
|
+
|
|
15
|
+
| 字段 | 要求 |
|
|
16
|
+
|---|---|
|
|
17
|
+
| 可交付成果 | 可观察、可演示、可交付 |
|
|
18
|
+
| 本阶段非目标 | 明确不做什么,防止范围蔓延 |
|
|
19
|
+
| 训练能力 | 对应 `capability` 维度 |
|
|
20
|
+
| 必要前置知识 | 缺失则先补齐,不硬推进 |
|
|
21
|
+
| 具体任务 | 每个任务可在领域包时间区间内完成 |
|
|
22
|
+
| **用户必须亲自完成的部分** | `userOnly`,非空,验收时逐项核对 |
|
|
23
|
+
| 验收标准 | 可判定,与目标 `doneCriteria` 呼应 |
|
|
24
|
+
| 常见风险 | 来自领域包 `pitfalls` |
|
|
25
|
+
| 预计投入 | 时间估计 |
|
|
26
|
+
| 通过后进入 | 下一阶段或分支条件 |
|
|
27
|
+
|
|
28
|
+
`userOnly` 不是装饰字段:验收时必须逐项确认用户确实亲手完成,未完成的项导致结论不得为"通过"。
|
|
29
|
+
|
|
30
|
+
## 展开粒度
|
|
31
|
+
|
|
32
|
+
只详细展开**当前阶段**与**下一阶段**;更远内容保留为一行概要,避免路线僵化与上下文膨胀。
|
|
33
|
+
|
|
34
|
+
## 任务时长
|
|
35
|
+
|
|
36
|
+
单次任务时长区间取自领域包 `manifest.yml` 的 `taskMinutes`(默认 `[30, 90]`)。超过上限的任务必须拆成多个独立验收的子任务;估计值写入 `state.current.task.estimateMin`。用户可用"调整节奏"覆盖本次区间。
|
|
37
|
+
|
|
38
|
+
## 调整触发
|
|
39
|
+
|
|
40
|
+
出现下列任一情况时调整路线,并说明原因,同时写入 `state.routeChanges`:
|
|
41
|
+
|
|
42
|
+
- 项目目标发生变化;
|
|
43
|
+
- 用户时间或环境变化;
|
|
44
|
+
- 实际水平与初始判断不同;
|
|
45
|
+
- 用户持续卡在同一类问题;
|
|
46
|
+
- 用户进步速度明显快于计划;
|
|
47
|
+
- 实践暴露出关键前置知识缺失;
|
|
48
|
+
- 当前方案无法产生可验证成果;
|
|
49
|
+
- **阶段验收结论为"未通过"或有未清的阻塞项**。
|
|
50
|
+
|
|
51
|
+
调整只改受影响的部分,不重写整条路线;不因一次调整推翻已验证的能力结论。
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# State:状态文件协议(唯一事实源)
|
|
2
|
+
|
|
3
|
+
来源:原文 §五、§十;改动经审核裁定 W03、W04、W05、W15。
|
|
4
|
+
|
|
5
|
+
## 载体
|
|
6
|
+
|
|
7
|
+
| 路径 | 性质 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `.coach/state.json` | **唯一事实源**,机器校验,字段级更新 |
|
|
10
|
+
| `.coach/PROGRESS.md` | **生成物**,给人阅读;禁止手工编辑;与 json 冲突时以 json 为准 |
|
|
11
|
+
|
|
12
|
+
不使用 YAML:状态由模型写入,JSON 的严格语法与枚举可被机械校验,避免缩进、冒号、中文标点导致的静默歧义。
|
|
13
|
+
|
|
14
|
+
多项目时用显式路径(例如 `.coach/<project>/state.json`);同一路径同一时间只能有一个活动状态。
|
|
15
|
+
|
|
16
|
+
## 顶层字段
|
|
17
|
+
|
|
18
|
+
| 字段 | 类型 | 说明 |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `schemaVersion` | string | 当前 `"1.0"` |
|
|
21
|
+
| `engineVersion` | string | 引擎版本,与技能包一致 |
|
|
22
|
+
| `domain` | string | 领域包 id,须与领域包 `manifest.yml` 的 `id` 一致 |
|
|
23
|
+
| `domainVersion` | string | 领域包版本 |
|
|
24
|
+
| `assumeGoal` | boolean | 本次是否默认已知目标 |
|
|
25
|
+
| `revision` | integer | 每次写入 +1 |
|
|
26
|
+
| `updatedAt` | string | ISO 8601 |
|
|
27
|
+
| `goal` | object | `statement`、`deliverable`、`why`、`doneCriteria[]`、`constraints{time,tools,environment,permissions}`、`nonGoals[]` |
|
|
28
|
+
| `capability` | array | 7 个维度各一条:`dimension`、`level(1–5)`、`status`、`evidence[]`(引用证据 id)、`gap`、`impact` |
|
|
29
|
+
| `strategy` | object | `deferred[]`、`immediate[]`、`practice[]`、`assumptions[]` |
|
|
30
|
+
| `route` | array | 阶段:`n`、`name`、`deliverable`、`nonGoals[]`、`skills[]`、`prereq[]`、`tasks[]`、`userOnly[]`、`acceptance[]`、`risks[]`、`estimate`、`next` |
|
|
31
|
+
| `current` | object | `stage`、`stageStatus`、`task{title,deliverable,criteria[],limits[],nonGoals[],estimateMin,state}` |
|
|
32
|
+
| `evidence` | array | `id`、`stage`(**`0` = 诊断期证据**)、`claim`、`artifact`、`strength`、`note` |
|
|
33
|
+
| `open` | array | `id`、`issue`、`severity`、`status`(未解决/已解决)、`next`;**可选** `basis`(依据:文档章节或文件行号)与 `checkStatus`(`已核对`/`推测`,默认 `推测`)——R10 要求的实测依据必须落在这里才可复核 |
|
|
34
|
+
| `routeChanges` | array | `at`、`reason`、`change` |
|
|
35
|
+
| `directAnswers` | array | `at`、`topic`。**记录本身不计入能力证据**;该成果只有在用户能解释、修改、验证之后,才可另行作为证据计入(见 `task-loop.md`) |
|
|
36
|
+
| `authorizations` | array | `scope`、`mode`(read/write)、`grantedAt` |
|
|
37
|
+
| `completedTasks` | array | 已完成任务的摘要行(原文 §十 的档案项之一),可为空数组;只存摘要,不内联大段代码或日志 |
|
|
38
|
+
| `retrievalRecap` | string | 最近一次阶段验收的检索式复述结论 |
|
|
39
|
+
| `nextTask` | string | 下一步 |
|
|
40
|
+
|
|
41
|
+
枚举与格式细则:
|
|
42
|
+
|
|
43
|
+
- `evidence[].strength`:`已验证 / 部分验证 / 待验证`(三态,与 `capability[].status` 同枚举);
|
|
44
|
+
- `evidence[].id`:非空且唯一,格式约定 `E<数字>`;
|
|
45
|
+
- `evidence[].artifact`:非空,且**不得是占位符**(`-`、`无`、`N/A`、`待补` 等)。**下列都算可核对材料**:文件与行号、日志片段、截图位置、可复现步骤、**问答记录(题目编号+学员原话摘录)**、**操作自述记录(学员原话摘录+时间)**。后两类是知识类结论与操作自述的**合法材料**,不得因为"不是文件"而拒绝,也不得因此要求学员补交截图或重复实测;
|
|
46
|
+
- 时间字段:严格 ISO 8601(日期 + 时间 + 时区,如 `2026-03-08T20:15:00.000Z`);
|
|
47
|
+
- `current.task.estimateMin`:正整数分钟估计,超过领域包 `taskMinutes` 上限时给出提醒。
|
|
48
|
+
|
|
49
|
+
## 等级定义(1–5,源自原文 §五)
|
|
50
|
+
|
|
51
|
+
| 等级 | 含义 |
|
|
52
|
+
|---:|---|
|
|
53
|
+
| 1 | 需要大量引导 |
|
|
54
|
+
| 2 | 理解局部内容,但难以独立应用 |
|
|
55
|
+
| 3 | 能完成常规任务,边界和结构仍需指导 |
|
|
56
|
+
| 4 | 能独立完成并排查多数问题 |
|
|
57
|
+
| 5 | 能解释取舍、设计结构并迁移到新问题 |
|
|
58
|
+
|
|
59
|
+
领域包可在题目级给出更细的锚点(见其 `diagnosis` 小节的"1–5 等级锚点"),但**整体等级的口径以上表为准**。
|
|
60
|
+
|
|
61
|
+
枚举:
|
|
62
|
+
|
|
63
|
+
- `capability[].dimension`:基础知识 / 实际应用 / 问题拆解 / 调试与纠错 / 结构与质量 / 独立程度 / 解释与迁移
|
|
64
|
+
- `capability[].status`:已验证 / 部分验证 / 待验证
|
|
65
|
+
- `open[].severity`:阻塞 / 重要 / 建议
|
|
66
|
+
- `current.stageStatus`:未开始 / 进行中 / 待验收 / 已通过 / 有条件通过 / 未通过
|
|
67
|
+
|
|
68
|
+
## 机械不变量(由 `scripts/coach-validate.mjs` 强制)
|
|
69
|
+
|
|
70
|
+
1. 顶层键齐备、类型正确、枚举合法(**缺键不再中断后续检查**,避免一处缺失掩盖全部问题);
|
|
71
|
+
2. `capability` 覆盖全部 7 个维度,无重复;
|
|
72
|
+
3. `status=已验证` ⇒ `evidence` 非空、`level≥3`,**且被引用的证据中至少有一条强度为"已验证"**(只查 id 存在会被"引用一条待验证证据"绕过);
|
|
73
|
+
4. `evidence` 为空 ⇒ `level≤2` 且 `status=待验证`;`status=部分验证` ⇒ 引用证据强度不得全为"待验证";
|
|
74
|
+
5. `capability[].evidence` 中每个 id 必须存在于 `evidence[]`;
|
|
75
|
+
6. 每条 `evidence` 必须有非空 `artifact`,且**不得是占位符**(`-`/`无`/`N/A`/`待补` 等);问答记录与操作自述记录**都算合法材料**(见上文格式细则);
|
|
76
|
+
7. `route[].userOnly` 与 `route[].acceptance` 非空;`route[].n` **从 1 开始**、连续、唯一;
|
|
77
|
+
8. `route` 非空时 `current.stage` 必须存在于 `route`;`route` 为空时降为提醒(intake/基线阶段属正常),但**任务一旦开工而 `route` 仍为空则报错**;
|
|
78
|
+
9. 存在 `severity=阻塞` 且 `status=未解决` 的 `open` 项时,`current.stageStatus` 不得为 `已通过`;
|
|
79
|
+
10. `current.stageStatus=已通过` ⇒ `retrievalRecap` 非空;
|
|
80
|
+
11. `goal.statement`、`goal.deliverable`、`goal.doneCriteria` 非空;
|
|
81
|
+
12. `revision` 为 ≥1 的整数,所有时间字段为**严格 ISO 8601**;
|
|
82
|
+
13. **分层检查**:`SKILL.md`、`references/engine/*.md`、`assets/*.md` 不得出现学科专有词条。匹配方式为 NFKC 归一化 + 整词匹配(英文普通词不会因为包含某学科词根而误报,一行可报多个令牌);`scripts/` 不参与扫描(校验器自身含黑名单词表)。**边界**:只拦常见英文写法,全角/同义改写/拼音不在覆盖范围——它是**护栏,不是证明**。(本条与 6.x 审核记录中的 D7、D14 同源:说明文字里一旦举出具体词例,就会触发本条自身。)
|
|
83
|
+
14. `completedTasks` 为字符串数组;条目过长(>200 字符)时提醒"只保存结论摘要"(原文 §十);
|
|
84
|
+
15. `evidence[].artifact`/`note` 超长(>500 字符)时提醒疑似内联大段内容;
|
|
85
|
+
16. `current.task.estimateMin` 超过领域包 `taskMinutes` 上限时提醒"必须拆分或由用户显式调整节奏";
|
|
86
|
+
17. `goal.constraints` 四项为空时提醒缺少边界依据;
|
|
87
|
+
18. `evidence[].stage` 为 ≥0 的整数(`0` 表示**诊断期证据**:诊断、基线、路线阶段的问答记录与操作自述都落在这里);
|
|
88
|
+
19. **提醒(warn,不拦截)**:某维度 `status=已验证`,且其引用证据的 `artifact` **全部**形如「问答记录…」、去重后题目编号 **<2** → 提醒"知识类结论疑为单题即发已验证"。**warn 不构成门禁**(CI 退出码只看 error),其强制力由 `docs/zero-knowledge-path.zh.md` 的人工清单承担——这是 2.1 修订的明确取舍:漏报的代价是"可能漂移"(有清单兜底),误报的代价是每次正常教学都被阻塞;
|
|
89
|
+
20. `open[].basis`/`open[].checkStatus` 为**可选**字段(R10 的实测留痕):出现时校验类型与枚举;`checkStatus=推测` 时给出提醒 **ST-W8**(R10 规定推测状态不得据此要求实测)。**如实声明**:**省略这两个字段不会触发任何提醒**——留痕是否完整靠人工清单,不靠机械门禁;
|
|
90
|
+
21. `current.task.criteria` 为空且任务已开始 → 提醒 **ST-W2**(本条此前仅存在于校验器代码中,现补录于此)。
|
|
91
|
+
|
|
92
|
+
## 更新时机(原文 §十)
|
|
93
|
+
|
|
94
|
+
- 完成初步诊断;
|
|
95
|
+
- 完成一个可验收任务;
|
|
96
|
+
- 完成阶段复盘;
|
|
97
|
+
- 路线发生重大调整;
|
|
98
|
+
- 新证据改变能力判断。
|
|
99
|
+
|
|
100
|
+
## 信息优先级与合并规则(原文 §十 + 审核裁定 W04)
|
|
101
|
+
|
|
102
|
+
优先级:**用户当前明确说明 > 最近的实际成果与测试 > 状态文件 > 较早的自述或推测**。
|
|
103
|
+
|
|
104
|
+
冲突处理:
|
|
105
|
+
|
|
106
|
+
1. 高优先级信息覆盖低优先级,覆盖时在 `evidence` 或 `routeChanges` 留一条记录;
|
|
107
|
+
2. 不得因为"档案里写着"就拒绝用户当前的更正;
|
|
108
|
+
3. 不得为了保持档案"好看"而保留已被推翻的结论——过时结论标记为 `待验证`,不删除证据历史。
|
|
109
|
+
|
|
110
|
+
## 校验器边界(如实声明)
|
|
111
|
+
|
|
112
|
+
校验器只检查**状态层**:它能保证"无证据不得标已验证""阻塞项未清不得通过""目标必须落盘"等结构约束;它**不能**检查对话质量(例如是否真的只给了 3 级提示、是否泄露了完整答案)。不要把校验通过当作教学质量的证明。
|
|
113
|
+
|
|
114
|
+
## 无持久化环境
|
|
115
|
+
|
|
116
|
+
若运行环境无法写文件,则在每阶段结束输出《可复制学习状态摘要》(即状态文件的最小可读子集),交用户保存,并在下次对话中优先粘贴回来。
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Task Loop:单次学习任务
|
|
2
|
+
|
|
3
|
+
来源:原文 §七;改动经审核裁定 W14(直接答案只记录)、W16、W17。
|
|
4
|
+
|
|
5
|
+
## 循环五步
|
|
6
|
+
|
|
7
|
+
### 1 明确本次成果
|
|
8
|
+
|
|
9
|
+
先说明:本次要完成什么、完成标准、限制、暂时不做什么、预计时长。写入 `state.current.task`,模板见 `assets/task-card.md`。
|
|
10
|
+
|
|
11
|
+
### 2 用户先思考和尝试
|
|
12
|
+
|
|
13
|
+
除非用户明确要求完整答案,否则先让用户:描述思路 / 列出步骤 / 编写伪代码 / 预测结果 / 提交当前尝试。
|
|
14
|
+
|
|
15
|
+
**答案不完整不等于要给实现。** 用户沉默或说"不会"时,降一级提示;**已在第 1 级时不再降级,改为缩小任务或回补前置知识**(见 `adapt.md` 的受阻分支),而不是跳过脚手架直接给答案。**若缺失的是知识类内容**(学员明说没学过,或诊断显示前置缺失),**按 §4/R9 先讲清机制,再让他继续尝试**——讲授只改变"回补的形式",不改变"先尝试"的顺序。
|
|
16
|
+
|
|
17
|
+
### 3 分级提示(脚手架)
|
|
18
|
+
|
|
19
|
+
| 级别 | 内容 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| 1 | 提醒目标与约束 |
|
|
22
|
+
| 2 | 指出相关概念 |
|
|
23
|
+
| 3 | 给出解决方向 |
|
|
24
|
+
| 4 | 提供伪代码或局部示例 |
|
|
25
|
+
| 5 | 提供完整参考答案并解释取舍 |
|
|
26
|
+
|
|
27
|
+
- 默认停在**能推进的最低级别**;
|
|
28
|
+
- 用户可用"给提示,级别 N"指定级别;
|
|
29
|
+
- **未经用户请求不得跳到第 5 级**;
|
|
30
|
+
- 每次提示后要求用户继续推进,不停留在"懂了"。
|
|
31
|
+
|
|
32
|
+
### 4 即时补齐知识(可由 R9 在诊断阶段独立调用)
|
|
33
|
+
|
|
34
|
+
只讲当前任务所需,顺序固定(**五步,末步不可省**):
|
|
35
|
+
|
|
36
|
+
> 概念 → 当前任务为什么需要 → 最小示例 → **用户亲自应用** → **确认题**
|
|
37
|
+
|
|
38
|
+
- 知识类的"亲自应用"可由确认题承担;**技能类必须亲手实现**——第 4 步是唯一能产出**行为类证据**的环节,不得用确认题顶替。
|
|
39
|
+
- 本条亦可由 **R9** 在诊断阶段独立调用(学员明说没学过/前置缺失/事实性知识):此时不受"用户先尝试"的顺序约束,但**仍须走完五步**。**技能类内容的裁定**:技能类任务整体仍守"先尝试"的顺序,但**当学员原话明确表示"未学过/不会"时,讲授优先于"先尝试"**——先讲清其中最小的一个机制,再让他动手;讲授只改变回补的形式,**不得代做**。
|
|
40
|
+
- 一次讲授=一个机制+一个最小示例;讲完必须回到一次学员产出(预测下一步/确认题/亲手改一处)。
|
|
41
|
+
|
|
42
|
+
不为内容完整而插入当前阶段不需要的理论。超出范围的内容记入 `state.strategy.deferred`。
|
|
43
|
+
|
|
44
|
+
### 5 用户提交证据
|
|
45
|
+
|
|
46
|
+
证据类型(按领域择用):答案或解释、代码/文档/设计稿、运行结果、测试或断言、错误日志、截图或操作记录、可复现步骤、对设计取舍的说明。
|
|
47
|
+
|
|
48
|
+
证据写入 `state.evidence[]`,字段:`id`、`stage`、`claim`(这条证据支持什么能力结论)、`artifact`(指向具体材料)、`strength`(已验证 / 部分验证 / 待验证)。`stage` 允许 `0`,表示**诊断期证据**(尚未进入任何阶段)。
|
|
49
|
+
|
|
50
|
+
**三类自述区别对待**(与 R3 一致):
|
|
51
|
+
|
|
52
|
+
| 自述 | 处理 | 允许的 `artifact` |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| **能力自述**("我熟练") | 只是线索,按 `待验证` 记录 | —— |
|
|
55
|
+
| **缺口自述**("我没学过 X") | 直接采信,转入 R9 讲授 | —— |
|
|
56
|
+
| **操作自述**("我跑了一次,输出是 X"/"我按你说的改了") | 无反证时按 `部分验证` 接受;**不得要求重复实测,也不得要求补交实测材料** | 操作自述记录(学员原话摘录+时间) |
|
|
57
|
+
|
|
58
|
+
**知识类结论**(事实、机制、术语、顺序、规则)用**问答记录(题目编号+学员原话摘录)**作为 `artifact`;**行为类结论**(代码、功能、排错、设计取舍)的 `artifact` 必须指向运行结果或材料(文件与行号、日志片段、截图位置、可复现步骤);学员**自己执行并给出结果原文**的操作自述(如"我跑了一次,输出是 X")按部分验证接受,`artifact` 记「操作自述记录(学员原话摘录+结果原文+时间)」——**不含可观察结果的完成声明**("我写完了")按能力自述处理,记 `待验证`。强度判定见 `diagnosis.md` 与 `review-acceptance.md`。
|
|
59
|
+
|
|
60
|
+
## 直接答案
|
|
61
|
+
|
|
62
|
+
用户说"直接答案"时:
|
|
63
|
+
|
|
64
|
+
1. 给出完整参考实现,并解释关键取舍(为什么这样而不是那样、代价是什么);
|
|
65
|
+
2. 以少量问题验证理解(改一处会怎样、边界条件、如何验证);
|
|
66
|
+
3. 在 `state.directAnswers` 记一条(含时间与主题)。
|
|
67
|
+
|
|
68
|
+
记录的作用仅是复盘时提示"这条路径没有经过脚手架";该成果只有在用户能解释、修改、验证后才可作为能力证据。
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* coach-install.mjs — 把本技能安装到可被 DSH 发现的位置
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* node scripts/coach-install.mjs [--dest-root <dir>] [--link] [--force] [--dry-run]
|
|
7
|
+
*
|
|
8
|
+
* 默认目标: <cwd>/.dsh/skills/dsh-coach
|
|
9
|
+
* `.dsh/skills` 是 DSH 的 project-dsh 技能根(rank 100),按官方说明新增 skill
|
|
10
|
+
* 会在下一次模型步骤进入会话目录,无需重启宿主。
|
|
11
|
+
*
|
|
12
|
+
* --link 建立目录联接(Windows junction / 其它平台 symlink),源改动即时生效,
|
|
13
|
+
* 适合在源目录继续迭代;不指定时为目标处的一份副本。
|
|
14
|
+
* --force 目标已存在时先删除再安装。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const SOURCE = path.resolve(HERE, '..');
|
|
23
|
+
const SKILL_NAME = path.basename(SOURCE);
|
|
24
|
+
|
|
25
|
+
function parseArgs(argv) {
|
|
26
|
+
const out = { link: false, force: false, dryRun: false };
|
|
27
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
28
|
+
const a = argv[i];
|
|
29
|
+
if (a === '--dest-root') out.destRoot = argv[++i];
|
|
30
|
+
else if (a === '--link') out.link = true;
|
|
31
|
+
else if (a === '--force') out.force = true;
|
|
32
|
+
else if (a === '--dry-run') out.dryRun = true;
|
|
33
|
+
else if (a === '--help' || a === '-h') out.help = true;
|
|
34
|
+
else throw new Error(`未知参数:${a}`);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function main() {
|
|
40
|
+
let args;
|
|
41
|
+
try {
|
|
42
|
+
args = parseArgs(process.argv.slice(2));
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error(`参数错误:${e.message}`);
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
if (args.help) {
|
|
48
|
+
console.log('用法:node scripts/coach-install.mjs [--dest-root <dir>] [--link] [--force] [--dry-run]');
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const destRoot = path.resolve(args.destRoot || path.join(process.cwd(), '.dsh', 'skills'));
|
|
53
|
+
const target = path.join(destRoot, SKILL_NAME);
|
|
54
|
+
|
|
55
|
+
if (path.resolve(target) === SOURCE) {
|
|
56
|
+
console.error(`源与目标相同(${target}):请在源目录之外的当前工作目录运行。`);
|
|
57
|
+
process.exit(2);
|
|
58
|
+
}
|
|
59
|
+
if (target.startsWith(SOURCE + path.sep)) {
|
|
60
|
+
console.error(`目标位于源目录内部(${target}):会造成递归复制,已中止。`);
|
|
61
|
+
process.exit(2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const exists = fs.existsSync(target) || fs.lstatSync(target, { throwIfNoEntry: false }) !== undefined;
|
|
65
|
+
if (exists && !args.force) {
|
|
66
|
+
console.error(`目标已存在:${target}\n如需覆盖请加 --force。`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`源: ${SOURCE}`);
|
|
71
|
+
console.log(`目标:${target}`);
|
|
72
|
+
console.log(`方式:${args.link ? '目录联接(实时同步源改动)' : '复制副本'}`);
|
|
73
|
+
if (args.dryRun) {
|
|
74
|
+
console.log('(--dry-run:未执行任何写入)');
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fs.mkdirSync(destRoot, { recursive: true });
|
|
79
|
+
if (exists) {
|
|
80
|
+
console.log('移除既有目标…');
|
|
81
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (args.link) {
|
|
85
|
+
const type = process.platform === 'win32' ? 'junction' : 'dir';
|
|
86
|
+
fs.symlinkSync(SOURCE, target, type);
|
|
87
|
+
} else {
|
|
88
|
+
fs.cpSync(SOURCE, target, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log('\n完成。验证:');
|
|
92
|
+
console.log(' 1) 目录存在:' + fs.existsSync(target));
|
|
93
|
+
console.log(' 2) 结构自检:node ' + path.join(target, 'scripts', 'coach-validate.mjs') + ' --help');
|
|
94
|
+
console.log(' 3) 在 DSH 会话中让模型加载技能:skill("' + SKILL_NAME + '")');
|
|
95
|
+
console.log(' (若目录未刷新,新开一个会话或重启宿主)');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
main();
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* coach-selftest.mjs — 校验器自身的回归自测(不启动子进程,可在沙箱内运行)
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* node scripts/coach-selftest.mjs [--examples <dir>] [--skill-dir <dir>] [--keep-temp]
|
|
7
|
+
*
|
|
8
|
+
* 覆盖:
|
|
9
|
+
* 1. 反向夹具必须被拦下(并按规则号断言关键不变量真的触发)
|
|
10
|
+
* 2. 正向夹具在状态层必须零错误
|
|
11
|
+
* 3. 领域包结构(缺文件时记为 SKIP,不误判为通过)
|
|
12
|
+
* 4. 分层负例:往引擎副本注入学科硬令牌,必须报 LY01;原目录不得报
|
|
13
|
+
* 5. 迷你 YAML 子集解析与渲染函数的形状检查
|
|
14
|
+
*
|
|
15
|
+
* 退出码:0 全部通过(SKIP 不算失败);1 存在失败。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { Report, checkState, checkDomain, checkLayering, renderProgress, parseSimpleYaml, readText } from './coach-validate.mjs';
|
|
22
|
+
|
|
23
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const SKILL_DIR = path.resolve(HERE, '..');
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const opt = (name, fallback) => {
|
|
28
|
+
const i = args.indexOf(name);
|
|
29
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
|
|
30
|
+
};
|
|
31
|
+
const EXAMPLES = path.resolve(opt('--examples', path.resolve(SKILL_DIR, '..', '..', 'examples')));
|
|
32
|
+
const KEEP_TEMP = args.includes('--keep-temp');
|
|
33
|
+
|
|
34
|
+
const results = [];
|
|
35
|
+
const record = (name, status, detail) => {
|
|
36
|
+
results.push({ name, status, detail });
|
|
37
|
+
const mark = status === 'PASS' ? '✓' : status === 'SKIP' ? '-' : '✗';
|
|
38
|
+
console.log(`${mark} [${status}] ${name}${detail ? `\n ${detail}` : ''}`);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function readJsonIfExists(p) {
|
|
42
|
+
if (!fs.existsSync(p)) return null;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(readText(p));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return { __parseError: e.message };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── 1. 反向夹具必须被拦下 ────────────────────────────────────────────────────
|
|
51
|
+
const invalidPath = path.join(EXAMPLES, 'state.selftest-invalid.json');
|
|
52
|
+
const invalidState = readJsonIfExists(invalidPath);
|
|
53
|
+
if (!invalidState || invalidState.__parseError) {
|
|
54
|
+
record('反向夹具被拦下', 'FAIL', `${invalidPath} 读取失败:${invalidState ? invalidState.__parseError : '文件不存在'}`);
|
|
55
|
+
} else {
|
|
56
|
+
const r = new Report();
|
|
57
|
+
checkState(r, invalidState);
|
|
58
|
+
const rules = new Set(r.errors.map((e) => e.rule));
|
|
59
|
+
const required = ['ST02', 'ST03', 'ST04', 'ST06', 'ST07', 'ST09', 'ST10', 'ST11', 'ST12'];
|
|
60
|
+
const missing = required.filter((x) => !rules.has(x));
|
|
61
|
+
if (missing.length === 0 && r.errors.length >= 15) {
|
|
62
|
+
record('反向夹具被拦下', 'PASS', `命中 ${r.errors.length} 条 error,关键规则齐全:${required.join(', ')}`);
|
|
63
|
+
} else {
|
|
64
|
+
record('反向夹具被拦下', 'FAIL', `缺少规则:${missing.join(', ') || '无'};error 数 ${r.errors.length}(期望 ≥15)`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── 2. 正向夹具状态层零错误 ─────────────────────────────────────────────────
|
|
69
|
+
const demoPath = path.join(EXAMPLES, 'state.demo.json');
|
|
70
|
+
const demoState = readJsonIfExists(demoPath);
|
|
71
|
+
if (!demoState || demoState.__parseError) {
|
|
72
|
+
record('正向夹具状态层', 'FAIL', `${demoPath} 读取失败:${demoState ? demoState.__parseError : '文件不存在'}`);
|
|
73
|
+
} else {
|
|
74
|
+
const r = new Report();
|
|
75
|
+
checkState(r, demoState);
|
|
76
|
+
if (r.errors.length === 0) record('正向夹具状态层', 'PASS', `warn ${r.warns.length} 条`);
|
|
77
|
+
else record('正向夹具状态层', 'FAIL', r.errors.map((e) => `${e.rule}@${e.where}`).join('; '));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── 3. 领域包结构 ───────────────────────────────────────────────────────────
|
|
81
|
+
if (demoState && !demoState.__parseError) {
|
|
82
|
+
const domainDir = path.join(SKILL_DIR, 'references', 'domains', demoState.domain);
|
|
83
|
+
const r = new Report();
|
|
84
|
+
const manifest = checkDomain(r, domainDir, demoState);
|
|
85
|
+
if (r.errors.length === 0 && manifest) {
|
|
86
|
+
record('领域包结构', 'PASS', `${manifest.id}@${manifest.version},小节齐全`);
|
|
87
|
+
} else if (r.errors.every((e) => e.rule === 'DP06')) {
|
|
88
|
+
record('领域包结构', 'SKIP', `领域包尚未补齐:${r.errors.map((e) => path.basename(e.where)).join(', ')}`);
|
|
89
|
+
} else {
|
|
90
|
+
record('领域包结构', 'FAIL', r.errors.map((e) => `${e.rule}@${e.where}: ${e.msg}`).join('; '));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── 3.5 ST-W7:知识类"单题即发已验证"必须被提醒 ─────────────────────────────
|
|
95
|
+
// 修订 2.1 的取舍:这是 warn(只提醒、不拦截),所以断言必须同时检查
|
|
96
|
+
// "命中了 warn" 与 "没有升级成 error"——否则一次改动就可能把它变成门禁。
|
|
97
|
+
if (demoState && !demoState.__parseError) {
|
|
98
|
+
const probe = JSON.parse(JSON.stringify(demoState));
|
|
99
|
+
probe.evidence.push(
|
|
100
|
+
{ id: 'E90', stage: 0, claim: '能解释生命周期回调的顺序与次数', artifact: '问答记录:Q1-1 作答原文', strength: '已验证', note: '' },
|
|
101
|
+
{ id: 'E91', stage: 0, claim: '同一知识点的追问记录', artifact: '问答记录:Q1-1 加难追问', strength: '已验证', note: '' },
|
|
102
|
+
);
|
|
103
|
+
const target = probe.capability.find((c) => c.dimension === '解释与迁移');
|
|
104
|
+
target.status = '已验证';
|
|
105
|
+
target.level = 3;
|
|
106
|
+
target.evidence = ['E90', 'E91'];
|
|
107
|
+
const r = new Report();
|
|
108
|
+
checkState(r, probe);
|
|
109
|
+
const hit = r.warns.some((x) => x.rule === 'ST-W7');
|
|
110
|
+
const escalated = r.errors.some((x) => x.rule === 'ST-W7');
|
|
111
|
+
if (hit && !escalated) record('ST-W7 提醒(单题即发已验证)', 'PASS', '命中 warn,且未升级为 error');
|
|
112
|
+
else if (escalated) record('ST-W7 提醒(单题即发已验证)', 'FAIL', 'ST-W7 被当作 error 拦截(应为 warn-only)');
|
|
113
|
+
else record('ST-W7 提醒(单题即发已验证)', 'FAIL', '未命中 ST-W7');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── 3.6 不变量 3 的反绕过子句:强度不足的证据不得支撑"已验证" ───────────────
|
|
117
|
+
// 只断言规则号出现是不够的(level<3 等分支同样报 ST03),因此这里单独构造
|
|
118
|
+
// "已验证 + 引用的证据强度是待验证" 的探针,确认反绕过子句真的生效。
|
|
119
|
+
if (demoState && !demoState.__parseError) {
|
|
120
|
+
const probe = JSON.parse(JSON.stringify(demoState));
|
|
121
|
+
probe.evidence.push({ id: 'E92', stage: 0, claim: '某机制', artifact: '问答记录:Q9-9 作答原文', strength: '待验证', note: '' });
|
|
122
|
+
const target = probe.capability.find((c) => c.dimension === '结构与质量');
|
|
123
|
+
target.status = '已验证';
|
|
124
|
+
target.level = 3;
|
|
125
|
+
target.evidence = ['E92'];
|
|
126
|
+
const r = new Report();
|
|
127
|
+
checkState(r, probe);
|
|
128
|
+
const hit = r.errors.some((x) => x.rule === 'ST03' && /没有一条是/.test(x.msg));
|
|
129
|
+
if (hit) record('不变量 3 反绕过子句', 'PASS', '引用"待验证"证据却标已验证据此被拦下');
|
|
130
|
+
else record('不变量 3 反绕过子句', 'FAIL', '未命中"没有一条是已验证"的 ST03');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── 3.7 ST-W8:实测依据标为"推测"时必须提醒;省略字段时不提醒 ────────────────
|
|
134
|
+
if (demoState && !demoState.__parseError) {
|
|
135
|
+
const withSpec = JSON.parse(JSON.stringify(demoState));
|
|
136
|
+
withSpec.open.push({ id: 'O90', issue: '疑为文档与实测不一致', severity: '重要', status: '未解决', next: '要求实测', basis: '官方文档某节', checkStatus: '推测' });
|
|
137
|
+
const rWith = new Report();
|
|
138
|
+
checkState(rWith, withSpec);
|
|
139
|
+
const hit = rWith.warns.some((x) => x.rule === 'ST-W8');
|
|
140
|
+
|
|
141
|
+
const withoutSpec = JSON.parse(JSON.stringify(demoState));
|
|
142
|
+
withoutSpec.open.push({ id: 'O91', issue: '未填留痕字段', severity: '重要', status: '未解决', next: '照常推进' });
|
|
143
|
+
const rWithout = new Report();
|
|
144
|
+
checkState(rWithout, withoutSpec);
|
|
145
|
+
const noWarn = !rWithout.warns.some((x) => x.rule === 'ST-W8');
|
|
146
|
+
|
|
147
|
+
if (hit && noWarn) record('ST-W8 推测状态提醒', 'PASS', '标"推测"时提醒;省略留痕字段时不提醒(与 state.md 的如实声明一致)');
|
|
148
|
+
else record('ST-W8 推测状态提醒', 'FAIL', hit ? '省略字段时被误报' : '标"推测"时未提醒');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── 4. 分层负例:注入硬令牌必须被发现 ───────────────────────────────────────
|
|
152
|
+
const tempRoot = path.join(SKILL_DIR, '..', '.selftest-tmp');
|
|
153
|
+
try {
|
|
154
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
155
|
+
fs.cpSync(SKILL_DIR, tempRoot, { recursive: true });
|
|
156
|
+
const target = path.join(tempRoot, 'references', 'engine', 'state.md');
|
|
157
|
+
fs.appendFileSync(target, '\n<!-- 自测注入:MonoBehaviour Prefab asmdef -->\n', 'utf8');
|
|
158
|
+
|
|
159
|
+
const rBad = new Report();
|
|
160
|
+
checkLayering(rBad, tempRoot);
|
|
161
|
+
const rGood = new Report();
|
|
162
|
+
checkLayering(rGood, SKILL_DIR);
|
|
163
|
+
|
|
164
|
+
const badHits = rBad.errors.filter((e) => e.rule === 'LY01');
|
|
165
|
+
const goodHits = rGood.errors.filter((e) => e.rule === 'LY01');
|
|
166
|
+
if (badHits.length >= 3 && goodHits.length === 0) {
|
|
167
|
+
record('分层负例(注入 3 个令牌)', 'PASS', `副本报 ${badHits.length} 条 LY01,原目录 0 条`);
|
|
168
|
+
} else {
|
|
169
|
+
record('分层负例(注入 3 个令牌)', 'FAIL', `副本 LY01=${badHits.length}(期望 ≥3),原目录 LY01=${goodHits.length}(期望 0)`);
|
|
170
|
+
}
|
|
171
|
+
} catch (e) {
|
|
172
|
+
record('分层负例(注入 3 个令牌)', 'FAIL', `异常:${e.message}`);
|
|
173
|
+
} finally {
|
|
174
|
+
if (!KEEP_TEMP) fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
175
|
+
else console.log(` (--keep-temp:保留 ${tempRoot})`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── 5. 迷你 YAML 解析与渲染形状 ─────────────────────────────────────────────
|
|
179
|
+
try {
|
|
180
|
+
const parsed = parseSimpleYaml('id: x\nsections:\n a: a.md\n b: b.md\ntaskMinutes: [30, 90]\n', 'test.yml');
|
|
181
|
+
const ok = parsed.id === 'x' && parsed.sections && parsed.sections.a === 'a.md' && parsed.sections.b === 'b.md'
|
|
182
|
+
&& Array.isArray(parsed.taskMinutes) && parsed.taskMinutes[0] === 30 && parsed.taskMinutes[1] === 90;
|
|
183
|
+
record('迷你 YAML 子集解析', ok ? 'PASS' : 'FAIL', ok ? '嵌套映射与行内数组解析正确' : JSON.stringify(parsed));
|
|
184
|
+
} catch (e) {
|
|
185
|
+
record('迷你 YAML 子集解析', 'FAIL', e.message);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (demoState && !demoState.__parseError) {
|
|
189
|
+
try {
|
|
190
|
+
const md = renderProgress(demoState);
|
|
191
|
+
const need = ['# 学习进度', '## 目标', '## 能力画像', '## 当前阶段与任务', '## 证据', '## 待解决项'];
|
|
192
|
+
const missing = need.filter((h) => !md.includes(h));
|
|
193
|
+
if (missing.length === 0) record('渲染函数形状', 'PASS', `${md.split('\n').length} 行`);
|
|
194
|
+
else record('渲染函数形状', 'FAIL', `缺少小节:${missing.join(', ')}`);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
record('渲染函数形状', 'FAIL', e.message);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── 汇总 ────────────────────────────────────────────────────────────────────
|
|
201
|
+
const failed = results.filter((x) => x.status === 'FAIL');
|
|
202
|
+
const skipped = results.filter((x) => x.status === 'SKIP');
|
|
203
|
+
console.log(`\n自测结果:PASS ${results.length - failed.length - skipped.length} | SKIP ${skipped.length} | FAIL ${failed.length}`);
|
|
204
|
+
if (failed.length) console.log('存在失败项,需修复后才可依赖校验器的结论。');
|
|
205
|
+
process.exit(failed.length ? 1 : 0);
|