@namewta/speculo 0.8.5 → 0.8.6
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/README.md +4 -3
- package/dist/src/structured.js +162 -4
- package/dist/src/structured.js.map +1 -1
- package/package.json +2 -2
- package/template/canonical/canonical-specdev-goal-plan.md +0 -1
- package/template/canonical/canonical-specdev-grill-with-docs.md +0 -1
- package/template/canonical/canonical-specdev-spec.md +0 -1
- package/template/canonical/canonical-specdev-tickets.md +0 -1
- package/template/commands/status.md +3 -3
- package/template/skills/archive-and-consolidate/SKILL.md +19 -14
- package/template/workflows/learning/A-archive-and-consolidate/A-archive-and-consolidate.md +38 -0
- package/template/workflows/learning/A-archive-and-consolidate/promotion-plan-template.md +23 -0
- package/template/workflows/learning/A-assess-and-plan/A-assess-and-plan.md +39 -0
- package/template/workflows/learning/A-assess-and-plan/change-status-template.json +28 -0
- package/template/workflows/learning/A-assess-and-plan/learning-plan-template.md +28 -0
- package/template/workflows/learning/E-eli5/E-eli5.md +37 -0
- package/template/workflows/learning/E-eli5/lesson-template.md +31 -0
- package/template/workflows/learning/I-init-setup/I-init-setup.md +36 -0
- package/template/workflows/learning/I-init-setup/context-index-template.md +6 -0
- package/template/workflows/learning/I-init-setup/learner-profile-template.md +17 -0
- package/template/workflows/learning/I-init-setup/review-index-template.md +4 -0
- package/template/workflows/learning/INDEX.md +26 -0
- package/template/workflows/learning/P-practice/P-practice.md +34 -0
- package/template/workflows/learning/P-practice/practice-template.md +16 -0
- package/template/workflows/learning/Q-quiz/Q-quiz.md +34 -0
- package/template/workflows/learning/Q-quiz/quiz-artifact-template.md +16 -0
- package/template/workflows/learning/R-review/R-review.md +35 -0
- package/template/workflows/learning/R-review/review-template.md +12 -0
- package/template/workflows/learning/README.md +121 -0
- package/template/workflows/learning/_state/archive/.gitkeep +1 -0
- package/template/workflows/learning/_state/changes/.gitkeep +1 -0
- package/template/workflows/learning/_state/status.json +6 -0
- package/template/workflows/learning/common/rules/artifact-contract.md +27 -0
- package/template/workflows/learning/common/rules/assessment-policy.md +7 -0
- package/template/workflows/learning/common/rules/knowledge-organization.md +9 -0
- package/template/workflows/learning/common/rules/mastery-policy.md +21 -0
- package/template/workflows/learning/common/rules/path-reference-contract.md +7 -0
- package/template/workflows/learning/common/rules/teaching-policy.md +15 -0
- package/template/workflows/learning/common/schemas/change-status.schema.json +41 -0
- package/template/workflows/learning/common/schemas/status.schema.json +32 -0
- package/template/workflows/learning/common/skills/knowledge-promotion/SKILL.md +46 -0
- package/template/workflows/learning/common/skills/knowledge-promotion/domain-index-template.md +6 -0
- package/template/workflows/learning/common/skills/knowledge-promotion/domain-overview-template.md +19 -0
- package/template/workflows/learning/common/skills/knowledge-promotion/knowledge-template.md +25 -0
- package/template/workflows/learning/common/tools/validate-learning.mjs +356 -0
- package/template/workflows/learning/runtime-contract.json +11 -0
- package/template/workflows/specdev/INDEX.md +2 -2
- package/template/workflows/specdev/README.md +1 -6
- package/template/workflows/specdev/common/rules/artifact-contract.md +0 -1
- package/template/workflows/specdev/common/tools/validate-specdev.mjs +0 -68
- package/template/canonical/canonical-specdev-engineering-cognitive-mentor.md +0 -1968
- package/template/workflows/specdev/E-eli5/E-eli5.md +0 -97
- package/template/workflows/specdev/E-engineering-cognitive-mentor/E-engineering-cognitive-mentor.md +0 -254
- package/template/workflows/specdev/E-engineering-cognitive-mentor/architecture-guidance.md +0 -90
- package/template/workflows/specdev/E-engineering-cognitive-mentor/bug-guidance.md +0 -80
- package/template/workflows/specdev/E-engineering-cognitive-mentor/codebase-guidance.md +0 -107
- package/template/workflows/specdev/E-engineering-cognitive-mentor/comprehension-and-closure.md +0 -95
- package/template/workflows/specdev/E-engineering-cognitive-mentor/domain-learning-guidance.md +0 -62
- package/template/workflows/specdev/E-engineering-cognitive-mentor/evidence-and-options.md +0 -132
- package/template/workflows/specdev/E-engineering-cognitive-mentor/interaction-protocol.md +0 -116
- package/template/workflows/specdev/E-engineering-cognitive-mentor/mentor-report-template.md +0 -135
- package/template/workflows/specdev/E-engineering-cognitive-mentor/mode-routing.md +0 -47
- package/template/workflows/specdev/E-engineering-cognitive-mentor/persistence-and-resume.md +0 -132
- package/template/workflows/specdev/E-engineering-cognitive-mentor/requirements-guidance.md +0 -92
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "urn:speculo:learning:status:v1",
|
|
4
|
+
"title": "Learning Global Status",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schema_version", "workflow", "active", "archived"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema_version": {"const": 1},
|
|
9
|
+
"workflow": {"const": "learning"},
|
|
10
|
+
"active": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"items": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"required": ["change", "domain", "topic", "current_work", "works_run"],
|
|
15
|
+
"properties": {
|
|
16
|
+
"change": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(?:-[a-z0-9]+)*$"},
|
|
17
|
+
"domain": {"type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"},
|
|
18
|
+
"topic": {"type": "string", "minLength": 1},
|
|
19
|
+
"current_work": {"type": ["string", "null"], "pattern": "^learning/"},
|
|
20
|
+
"works_run": {"type": "array", "items": {"type": "string", "pattern": "^learning/"}, "uniqueItems": true}
|
|
21
|
+
},
|
|
22
|
+
"additionalProperties": false
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"archived": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"items": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(?:-[a-z0-9]+)*$"},
|
|
28
|
+
"uniqueItems": true
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"additionalProperties": false
|
|
32
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: knowledge-promotion
|
|
3
|
+
description: 在 Learning 内按掌握证据规划或应用 Markdown 知识提升,并为周期复习更新当前知识状态和分层索引。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Knowledge Promotion
|
|
7
|
+
|
|
8
|
+
本 Skill 由 R-review 和 A-archive 调用,写入路径必须由调用方提供且位于 Learning context。默认 `plan-promotion` 只分析;任何 context 写入均需调用方获得用户对精确路径和动作的确认。
|
|
9
|
+
|
|
10
|
+
## 模式
|
|
11
|
+
|
|
12
|
+
- `plan-promotion`:读取 completed change 和现有索引,产生 create/merge/supersede/archive-only 计划。
|
|
13
|
+
- `apply-promotion`:只执行已确认且重新验证无 drift 的计划。
|
|
14
|
+
- `mark-review-state`:更新指定知识的 `mastered | review_due | needs_refresh`、最近验证、下次复习和 REVIEW 行。
|
|
15
|
+
|
|
16
|
+
## Plan Promotion
|
|
17
|
+
|
|
18
|
+
1. 验证即时/保持 result、关键目标、迁移、误区和 change 状态;任一门不满足返回 blocked。
|
|
19
|
+
2. 每个通过 OBJ 形成一个候选 Knowledge ID。读取总目录、目标领域 INDEX 和精确同 id 文件;不得以全文相似搜索代替 id/索引判断。
|
|
20
|
+
3. 无同 id 时 `create`;当前结论兼容时 `merge`;新证据推翻当前结论时 `supersede`;仅会话性内容 `archive-only`。
|
|
21
|
+
4. 计划必须列出所有叶子、领域 overview/INDEX、总 INDEX、REVIEW 和证据链接变化,并把完整候选 Markdown 写入调用方 change 的 `promotion-staging/`。重复 id、断链或来源冲突标记 needs-confirmation;staging 不等于 context 写入。
|
|
22
|
+
|
|
23
|
+
## Apply Promotion
|
|
24
|
+
|
|
25
|
+
1. 重读计划涉及的全部文件并核对 drift;目标意外出现、内容 hash 变化或证据状态变化即停止。
|
|
26
|
+
2. 在调用方 change 的 `promotion-rollback/` 为每个目标保存原 Markdown;原目标不存在时在 `rollback-manifest.md` 明确记录。快照完整前不写 context。
|
|
27
|
+
3. 知识文件按 `<Path>{roots.workflows}/learning/common/skills/knowledge-promotion/knowledge-template.md</Path>` 创建/合并;新领域同时使用 domain index 和 overview 模板。掌握证据指向即将形成的 archive 路径,只使用 `promotion-staging/` 中已确认的候选内容。
|
|
28
|
+
4. 自叶到根更新领域 INDEX、总 INDEX 和 REVIEW。表格按稳定 Knowledge ID 排序,同一 id 只有一行。
|
|
29
|
+
5. 重读所有链接并运行 Learning validator。任一失败按 manifest 逆序恢复:原文件覆盖回去,原本不存在的文件删除;恢复后再次验证。恢复成功前不得继续归档移动。
|
|
30
|
+
|
|
31
|
+
## Mark Review State
|
|
32
|
+
|
|
33
|
+
只修改调用方指定的知识文件和 REVIEW 行。失败标记 `needs_refresh`,通过更新 `mastered`、最近证据和下一日期;不删除或改写 archive,也不修改无关知识正文。
|
|
34
|
+
|
|
35
|
+
## 完成标准
|
|
36
|
+
|
|
37
|
+
- 没有 RAG、向量或隐式相似匹配;
|
|
38
|
+
- 每个动作由双门证据和一个 Knowledge ID 支持;
|
|
39
|
+
- 计划模式无写入,执行模式有明确确认和 drift 检查;
|
|
40
|
+
- 更新后从总 INDEX 可以通过真实链接到达每个当前知识文件。
|
|
41
|
+
|
|
42
|
+
## 模板引用
|
|
43
|
+
|
|
44
|
+
- 知识正文:`<Path>{roots.workflows}/learning/common/skills/knowledge-promotion/knowledge-template.md</Path>`
|
|
45
|
+
- 领域目录:`<Path>{roots.workflows}/learning/common/skills/knowledge-promotion/domain-index-template.md</Path>`
|
|
46
|
+
- 领域概览:`<Path>{roots.workflows}/learning/common/skills/knowledge-promotion/domain-overview-template.md</Path>`
|
package/template/workflows/learning/common/skills/knowledge-promotion/domain-overview-template.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# <领域名称>概览
|
|
2
|
+
|
|
3
|
+
| 字段 | 内容 |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| Domain ID | `<domain>` |
|
|
6
|
+
| 类型 | `<project/product/subject/language/skill>` |
|
|
7
|
+
| 入口 | `[领域目录](INDEX.md)` |
|
|
8
|
+
|
|
9
|
+
## 我目前能做什么
|
|
10
|
+
|
|
11
|
+
## 领域地图
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
+----------+ +------+ +------+
|
|
15
|
+
| 基础概念 | --> | 方法 | --> | 应用 |
|
|
16
|
+
+----------+ +------+ +------+
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 当前边界与待学习内容
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# <知识标题>
|
|
2
|
+
|
|
3
|
+
| 字段 | 内容 |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| Knowledge ID | `<domain>/<topic>` |
|
|
6
|
+
| 状态 | `mastered` |
|
|
7
|
+
| 前置知识 | `<相对 Markdown 链接或无>` |
|
|
8
|
+
| 相关知识 | `<相对 Markdown 链接或无>` |
|
|
9
|
+
| 掌握证据 | `<归档 change 内 result 路径>` |
|
|
10
|
+
| 最近验证 | `YYYY-MM-DD` |
|
|
11
|
+
| 下次复习 | `YYYY-MM-DD` |
|
|
12
|
+
|
|
13
|
+
## 当前理解
|
|
14
|
+
|
|
15
|
+
## 心智模型
|
|
16
|
+
|
|
17
|
+
## 示例与应用
|
|
18
|
+
|
|
19
|
+
## 常见误区
|
|
20
|
+
|
|
21
|
+
## 来源与证据
|
|
22
|
+
|
|
23
|
+
## 替代关系
|
|
24
|
+
|
|
25
|
+
`<没有 supersedes 时写无>`
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
statSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const CHANGE_NAME = /^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
13
|
+
const DOMAIN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
14
|
+
const WORK_ID = /^learning\/[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
15
|
+
const CHANGE_STATUS = new Set(["active", "blocked", "awaiting_retention", "completed", "archived"]);
|
|
16
|
+
const PHASE = new Set(["intake", "assessment", "teaching", "practice", "immediate_quiz", "retention", "ready_to_archive", "archived"]);
|
|
17
|
+
const DOMAIN_TYPE = new Set(["project", "product", "subject", "language", "skill"]);
|
|
18
|
+
const RESULT = new Set(["not_attempted", "failed", "passed", "needs_review"]);
|
|
19
|
+
const KNOWLEDGE_STATUS = new Set(["mastered", "review_due", "needs_refresh", "superseded"]);
|
|
20
|
+
const EXPECTED_WORKS = new Set([
|
|
21
|
+
"A-archive-and-consolidate",
|
|
22
|
+
"A-assess-and-plan",
|
|
23
|
+
"E-eli5",
|
|
24
|
+
"I-init-setup",
|
|
25
|
+
"P-practice",
|
|
26
|
+
"Q-quiz",
|
|
27
|
+
"R-review",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const result = { workflowRoot: null, stateRoot: null, stage: null, change: null, selfCheck: false };
|
|
32
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
33
|
+
const arg = argv[index];
|
|
34
|
+
if (arg === "--workflow-root") result.workflowRoot = resolve(argv[++index] ?? "");
|
|
35
|
+
else if (arg === "--state-root") result.stateRoot = resolve(argv[++index] ?? "");
|
|
36
|
+
else if (arg === "--stage") result.stage = argv[++index] ?? null;
|
|
37
|
+
else if (arg === "--change") result.change = argv[++index] ?? null;
|
|
38
|
+
else if (arg === "--self-check") result.selfCheck = true;
|
|
39
|
+
else throw new Error(`unknown option: ${arg}`);
|
|
40
|
+
}
|
|
41
|
+
if (!result.workflowRoot && !result.stateRoot && !result.selfCheck) {
|
|
42
|
+
throw new Error("use --workflow-root <path>, --state-root <path>, or --self-check");
|
|
43
|
+
}
|
|
44
|
+
if (result.stage && !new Set(["pre-archive", "complete"]).has(result.stage)) {
|
|
45
|
+
throw new Error("--stage must be pre-archive or complete");
|
|
46
|
+
}
|
|
47
|
+
if (result.stage && !result.change) throw new Error("--stage requires --change");
|
|
48
|
+
if (result.change && !CHANGE_NAME.test(result.change)) throw new Error("--change has an invalid name");
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isFile(path) {
|
|
53
|
+
return existsSync(path) && statSync(path).isFile();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isDirectory(path) {
|
|
57
|
+
return existsSync(path) && statSync(path).isDirectory();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readJson(path, label, errors) {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
errors.push(`${label}: invalid JSON (${error.message})`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function walk(root) {
|
|
70
|
+
if (!isDirectory(root)) return [];
|
|
71
|
+
const files = [];
|
|
72
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
73
|
+
const path = join(root, entry.name);
|
|
74
|
+
if (entry.isDirectory()) files.push(...walk(path));
|
|
75
|
+
else if (entry.isFile()) files.push(path);
|
|
76
|
+
}
|
|
77
|
+
return files;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sameMembers(actual, expected) {
|
|
81
|
+
return actual.size === expected.size && [...actual].every((item) => expected.has(item));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function duplicates(values) {
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
return values.filter((value) => seen.has(value) || !seen.add(value));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function validDateTime(value) {
|
|
90
|
+
return typeof value === "string" && !Number.isNaN(Date.parse(value));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validateWorkflowRoot(root, errors) {
|
|
94
|
+
const indexPath = join(root, "INDEX.md");
|
|
95
|
+
const readmePath = join(root, "README.md");
|
|
96
|
+
const seedPath = join(root, "_state", "status.json");
|
|
97
|
+
const runtimePath = join(root, "runtime-contract.json");
|
|
98
|
+
for (const path of [indexPath, readmePath, seedPath, runtimePath]) {
|
|
99
|
+
if (!isFile(path)) errors.push(`${path}: required workflow file is missing`);
|
|
100
|
+
}
|
|
101
|
+
if (errors.length) return;
|
|
102
|
+
|
|
103
|
+
const index = readFileSync(indexPath, "utf8");
|
|
104
|
+
const readme = readFileSync(readmePath, "utf8");
|
|
105
|
+
if (!/^id: learning$/m.test(index) || !/^type: workflow$/m.test(index) || !/^workflow: learning$/m.test(index)) {
|
|
106
|
+
errors.push("INDEX.md: identity must be learning/type workflow");
|
|
107
|
+
}
|
|
108
|
+
for (const heading of ["## 永久知识", "## Work 激活"]) {
|
|
109
|
+
if (!index.includes(heading)) errors.push(`INDEX.md: missing ${heading}`);
|
|
110
|
+
}
|
|
111
|
+
if (!index.includes("<Path>{roots.workflows}/learning/README.md</Path>")) errors.push("INDEX.md: missing activation pointer");
|
|
112
|
+
if ((readme.match(/AUTO-INDEX-START/g) ?? []).length !== 1 || (readme.match(/AUTO-INDEX-END/g) ?? []).length !== 1) {
|
|
113
|
+
errors.push("README.md: requires one AUTO-INDEX marker pair");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const works = new Set(readdirSync(root, { withFileTypes: true })
|
|
117
|
+
.filter((entry) => entry.isDirectory() && /^[A-Z]-/.test(entry.name))
|
|
118
|
+
.map((entry) => entry.name));
|
|
119
|
+
if (!sameMembers(works, EXPECTED_WORKS)) {
|
|
120
|
+
errors.push(`workflow works mismatch: expected ${[...EXPECTED_WORKS].sort().join(", ")}; found ${[...works].sort().join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
for (const work of works) {
|
|
123
|
+
const path = join(root, work, `${work}.md`);
|
|
124
|
+
if (!isFile(path)) {
|
|
125
|
+
errors.push(`${work}: missing same-named entry`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const text = readFileSync(path, "utf8");
|
|
129
|
+
if (!text.includes("<Path>{roots.workflows}/learning/README.md</Path>")) errors.push(`${work}: missing activation contract reference`);
|
|
130
|
+
if (!/^workflow: learning$/m.test(text) || !/^type: workflow-entry$/m.test(text)) errors.push(`${work}: invalid frontmatter identity`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const seed = readJson(seedPath, "_state/status.json", errors);
|
|
134
|
+
if (seed && (seed.schema_version !== 1 || seed.workflow !== "learning" || !Array.isArray(seed.active) || !Array.isArray(seed.archived))) {
|
|
135
|
+
errors.push("_state/status.json: expected empty learning schema v1 status");
|
|
136
|
+
}
|
|
137
|
+
const runtime = readJson(runtimePath, "runtime-contract.json", errors);
|
|
138
|
+
if (runtime && (runtime.schema_version !== 1 || runtime.workflow !== "learning" || runtime.config !== null || runtime.opaque_default !== "preserve-byte-for-byte")) {
|
|
139
|
+
errors.push("runtime-contract.json: invalid learning contract");
|
|
140
|
+
}
|
|
141
|
+
for (const schema of ["status.schema.json", "change-status.schema.json"]) {
|
|
142
|
+
readJson(join(root, "common", "schemas", schema), `common/schemas/${schema}`, errors);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const path of walk(root).filter((candidate) => candidate.toLowerCase().endsWith(".md"))) {
|
|
146
|
+
if (/```mermaid\b/i.test(readFileSync(path, "utf8"))) {
|
|
147
|
+
errors.push(`${relative(root, path)}: Learning diagrams must use fenced ASCII text, not Mermaid`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const teachingContracts = [
|
|
152
|
+
[join(root, "E-eli5", "E-eli5.md"), ["5 岁的小孩", "大一新生", "ASCII", "不得生成 Mermaid"]],
|
|
153
|
+
[join(root, "E-eli5", "lesson-template.md"), ["## 教学表达基线", "5 岁的小孩", "大一新生", "```text"]],
|
|
154
|
+
[join(root, "common", "rules", "teaching-policy.md"), ["5 岁的小孩", "大一新生", "默认使用", "纯文本 ASCII"]],
|
|
155
|
+
[join(root, "I-init-setup", "learner-profile-template.md"), ["教学表达基线", "5 岁的小孩", "仅纯文本 ASCII"]],
|
|
156
|
+
];
|
|
157
|
+
for (const [path, requiredMarkers] of teachingContracts) {
|
|
158
|
+
if (!isFile(path)) {
|
|
159
|
+
errors.push(`${relative(root, path)}: required teaching contract is missing`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const text = readFileSync(path, "utf8");
|
|
163
|
+
for (const marker of requiredMarkers) {
|
|
164
|
+
if (!text.includes(marker)) errors.push(`${relative(root, path)}: missing '${marker}'`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function validateGlobalStatus(status, errors) {
|
|
170
|
+
if (!status) return;
|
|
171
|
+
if (status.schema_version !== 1 || status.workflow !== "learning" || !Array.isArray(status.active) || !Array.isArray(status.archived)) {
|
|
172
|
+
errors.push("status.json: expected learning schema v1 with active and archived arrays");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const activeNames = [];
|
|
176
|
+
for (const [index, entry] of status.active.entries()) {
|
|
177
|
+
const label = `status.json active[${index}]`;
|
|
178
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
179
|
+
errors.push(`${label}: must be an object`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!CHANGE_NAME.test(entry.change ?? "")) errors.push(`${label}: invalid change`);
|
|
183
|
+
if (!DOMAIN.test(entry.domain ?? "")) errors.push(`${label}: invalid domain`);
|
|
184
|
+
if (typeof entry.topic !== "string" || !entry.topic.trim()) errors.push(`${label}: topic is required`);
|
|
185
|
+
if (!(entry.current_work === null || WORK_ID.test(entry.current_work ?? ""))) errors.push(`${label}: invalid current_work`);
|
|
186
|
+
if (!Array.isArray(entry.works_run) || entry.works_run.some((item) => !WORK_ID.test(item)) || duplicates(entry.works_run ?? []).length) {
|
|
187
|
+
errors.push(`${label}: works_run must contain unique learning work ids`);
|
|
188
|
+
}
|
|
189
|
+
activeNames.push(entry.change);
|
|
190
|
+
}
|
|
191
|
+
if (duplicates(activeNames).length || duplicates(status.archived).length) errors.push("status.json: duplicate change names");
|
|
192
|
+
for (const name of status.archived) {
|
|
193
|
+
if (!CHANGE_NAME.test(name)) errors.push(`status.json: invalid archived change ${name}`);
|
|
194
|
+
if (activeNames.includes(name)) errors.push(`status.json: ${name} is both active and archived`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function validateMasteryGate(changeStatus, label, errors) {
|
|
199
|
+
const mastery = changeStatus.mastery;
|
|
200
|
+
if (!mastery || typeof mastery !== "object" || Array.isArray(mastery)) {
|
|
201
|
+
errors.push(`${label}: mastery is required`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!RESULT.has(mastery.immediate) || !RESULT.has(mastery.retention)) errors.push(`${label}: invalid mastery result`);
|
|
205
|
+
if (mastery.score !== null && (typeof mastery.score !== "number" || mastery.score < 0 || mastery.score > 100)) errors.push(`${label}: invalid mastery score`);
|
|
206
|
+
if (typeof mastery.critical_objectives_passed !== "boolean" || typeof mastery.transfer_passed !== "boolean") errors.push(`${label}: mastery booleans are required`);
|
|
207
|
+
if (!Array.isArray(mastery.blocking_misconceptions) || !Array.isArray(mastery.evidence)) errors.push(`${label}: mastery arrays are required`);
|
|
208
|
+
if (!(mastery.next_review_at === null || validDateTime(mastery.next_review_at))) errors.push(`${label}: invalid next_review_at`);
|
|
209
|
+
|
|
210
|
+
if (changeStatus.change_status === "awaiting_retention" && mastery.immediate !== "passed") {
|
|
211
|
+
errors.push(`${label}: awaiting_retention requires immediate passed`);
|
|
212
|
+
}
|
|
213
|
+
if (["completed", "archived"].includes(changeStatus.change_status)) {
|
|
214
|
+
if (mastery.immediate !== "passed" || mastery.retention !== "passed") errors.push(`${label}: completed knowledge requires both mastery gates passed`);
|
|
215
|
+
if (typeof mastery.score !== "number" || mastery.score < 80) errors.push(`${label}: completed knowledge requires score >= 80`);
|
|
216
|
+
if (!mastery.critical_objectives_passed) errors.push(`${label}: completed knowledge requires all critical objectives`);
|
|
217
|
+
if (!mastery.transfer_passed) errors.push(`${label}: completed knowledge requires transfer evidence`);
|
|
218
|
+
if (mastery.blocking_misconceptions.length) errors.push(`${label}: completed knowledge has blocking misconceptions`);
|
|
219
|
+
if (mastery.evidence.length < 2 || !mastery.evidence.some((item) => /\/quiz\/immediate-/.test(item)) || !mastery.evidence.some((item) => /\/quiz\/retention-/.test(item))) {
|
|
220
|
+
errors.push(`${label}: completed knowledge requires immediate and retention result evidence`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validateChangeStatus(value, expectedName, expectedArchived, errors) {
|
|
226
|
+
const label = `${expectedName}/.status.json`;
|
|
227
|
+
if (!value) return;
|
|
228
|
+
if (value.schema_version !== 1 || value.artifact !== "learning-change-status") errors.push(`${label}: invalid identity/schema`);
|
|
229
|
+
if (value.change !== expectedName) errors.push(`${label}: change must match directory`);
|
|
230
|
+
if (!DOMAIN.test(value.domain ?? "") || !DOMAIN_TYPE.has(value.domain_type)) errors.push(`${label}: invalid domain or domain_type`);
|
|
231
|
+
if (typeof value.topic !== "string" || !value.topic.trim()) errors.push(`${label}: topic is required`);
|
|
232
|
+
if (!CHANGE_STATUS.has(value.change_status) || !PHASE.has(value.phase)) errors.push(`${label}: invalid status or phase`);
|
|
233
|
+
if (!(value.current_work === null || WORK_ID.test(value.current_work ?? ""))) errors.push(`${label}: invalid current_work`);
|
|
234
|
+
if (!Array.isArray(value.works_run) || value.works_run.some((item) => !WORK_ID.test(item)) || duplicates(value.works_run ?? []).length) errors.push(`${label}: invalid works_run`);
|
|
235
|
+
if (!validDateTime(value.created_at) || !validDateTime(value.updated_at)) errors.push(`${label}: created_at and updated_at must be date-times`);
|
|
236
|
+
if (!Array.isArray(value.blockers)) errors.push(`${label}: blockers must be an array`);
|
|
237
|
+
if (expectedArchived && (value.change_status !== "archived" || value.phase !== "archived" || !validDateTime(value.archived_at) || typeof value.archive_path !== "string")) {
|
|
238
|
+
errors.push(`${label}: archived index requires archived status, phase, time, and path`);
|
|
239
|
+
}
|
|
240
|
+
if (expectedArchived && value.archive_path !== `<Path>{roots.state}/learning/archive/${expectedName.slice(0, 7)}/${expectedName}</Path>`) {
|
|
241
|
+
errors.push(`${label}: archive_path does not match the indexed archive location`);
|
|
242
|
+
}
|
|
243
|
+
if (!expectedArchived && value.change_status === "archived") errors.push(`${label}: active index cannot point to archived change`);
|
|
244
|
+
if (value.change_status === "completed" && (!validDateTime(value.completed_at) || value.phase !== "ready_to_archive")) errors.push(`${label}: completed requires completed_at and ready_to_archive phase`);
|
|
245
|
+
validateMasteryGate(value, label, errors);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function statePathFromTag(stateRoot, taggedPath) {
|
|
249
|
+
const match = /^<Path>\{roots\.state\}\/learning\/(.+)<\/Path>$/.exec(taggedPath);
|
|
250
|
+
return match ? join(stateRoot, ...match[1].split("/")) : null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function validateContext(stateRoot, errors) {
|
|
254
|
+
const contextRoot = join(stateRoot, "context");
|
|
255
|
+
if (!isDirectory(contextRoot)) return;
|
|
256
|
+
for (const required of ["INDEX.md", "REVIEW.md"]) {
|
|
257
|
+
if (!isFile(join(contextRoot, required))) errors.push(`context/${required}: missing`);
|
|
258
|
+
}
|
|
259
|
+
const ids = new Map();
|
|
260
|
+
for (const file of walk(contextRoot).filter((path) => path.endsWith(".md"))) {
|
|
261
|
+
const text = readFileSync(file, "utf8");
|
|
262
|
+
const fileLabel = relative(stateRoot, file).split(sep).join("/");
|
|
263
|
+
for (const match of text.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
|
|
264
|
+
const target = match[1].split("#")[0].trim();
|
|
265
|
+
if (!target || /^(?:https?:|mailto:|#)/.test(target) || target.includes("<")) continue;
|
|
266
|
+
const resolved = resolve(dirname(file), decodeURIComponent(target));
|
|
267
|
+
if (!resolved.startsWith(resolve(contextRoot) + sep) || !existsSync(resolved)) errors.push(`${fileLabel}: broken or escaping Markdown link ${target}`);
|
|
268
|
+
}
|
|
269
|
+
const id = /^\|\s*Knowledge ID\s*\|\s*([^|]+?)\s*\|\s*$/m.exec(text)?.[1];
|
|
270
|
+
if (!id) continue;
|
|
271
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*\/[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)) errors.push(`${fileLabel}: invalid Knowledge ID ${id}`);
|
|
272
|
+
if (ids.has(id)) errors.push(`${fileLabel}: duplicate Knowledge ID ${id} also in ${ids.get(id)}`);
|
|
273
|
+
else ids.set(id, fileLabel);
|
|
274
|
+
const state = /^\|\s*状态\s*\|\s*([^|]+?)\s*\|\s*$/m.exec(text)?.[1];
|
|
275
|
+
if (!KNOWLEDGE_STATUS.has(state)) errors.push(`${fileLabel}: invalid or missing knowledge 状态`);
|
|
276
|
+
for (const heading of ["## 当前理解", "## 心智模型", "## 示例与应用", "## 常见误区", "## 来源与证据"]) {
|
|
277
|
+
if (!text.includes(heading)) errors.push(`${fileLabel}: missing ${heading}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function validateStateRoot(root, options, errors) {
|
|
283
|
+
const statusPath = join(root, "status.json");
|
|
284
|
+
if (!isFile(statusPath)) {
|
|
285
|
+
errors.push("status.json: missing");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const status = readJson(statusPath, "status.json", errors);
|
|
289
|
+
validateGlobalStatus(status, errors);
|
|
290
|
+
if (!status || !Array.isArray(status.active) || !Array.isArray(status.archived)) return;
|
|
291
|
+
|
|
292
|
+
for (const entry of status.active) {
|
|
293
|
+
if (!entry || typeof entry.change !== "string") continue;
|
|
294
|
+
const path = join(root, "changes", entry.change, ".status.json");
|
|
295
|
+
if (!isFile(path)) {
|
|
296
|
+
errors.push(`${entry.change}: indexed active change is missing .status.json`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const changeStatus = readJson(path, `${entry.change}/.status.json`, errors);
|
|
300
|
+
validateChangeStatus(changeStatus, entry.change, false, errors);
|
|
301
|
+
if (changeStatus && (changeStatus.domain !== entry.domain || changeStatus.topic !== entry.topic || changeStatus.current_work !== entry.current_work || JSON.stringify(changeStatus.works_run) !== JSON.stringify(entry.works_run))) {
|
|
302
|
+
errors.push(`${entry.change}: global and change status projections differ`);
|
|
303
|
+
}
|
|
304
|
+
if (options.stage === "pre-archive" && (!options.change || options.change === entry.change) && changeStatus?.change_status !== "completed") {
|
|
305
|
+
errors.push(`${entry.change}: pre-archive requires completed status`);
|
|
306
|
+
}
|
|
307
|
+
if (options.stage === "complete" && (!options.change || options.change === entry.change)) {
|
|
308
|
+
errors.push(`${entry.change}: complete stage cannot leave the change active`);
|
|
309
|
+
}
|
|
310
|
+
for (const evidence of changeStatus?.mastery?.evidence ?? []) {
|
|
311
|
+
const resolved = statePathFromTag(root, evidence);
|
|
312
|
+
if (!resolved || !isFile(resolved)) errors.push(`${entry.change}: mastery evidence does not exist ${evidence}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const name of status.archived) {
|
|
317
|
+
const archiveRoot = join(root, "archive", name.slice(0, 7), name);
|
|
318
|
+
const path = join(archiveRoot, ".status.json");
|
|
319
|
+
if (!isFile(path)) {
|
|
320
|
+
errors.push(`${name}: indexed archive is missing .status.json`);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
validateChangeStatus(readJson(path, `${name}/.status.json`, errors), name, true, errors);
|
|
324
|
+
if (isDirectory(join(root, "changes", name))) errors.push(`${name}: archived change still exists under changes`);
|
|
325
|
+
if (options.stage === "complete" && (!options.change || options.change === name) && !isFile(join(archiveRoot, "promotion-plan.md"))) {
|
|
326
|
+
errors.push(`${name}: complete stage requires archived promotion-plan.md`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (options.change && !status.active.some((entry) => entry.change === options.change) && !status.archived.includes(options.change)) errors.push(`${options.change}: change is not indexed`);
|
|
330
|
+
validateContext(root, errors);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function main() {
|
|
334
|
+
const options = parseArgs(process.argv.slice(2));
|
|
335
|
+
const errors = [];
|
|
336
|
+
if (options.selfCheck) {
|
|
337
|
+
const ownRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
338
|
+
validateWorkflowRoot(ownRoot, errors);
|
|
339
|
+
}
|
|
340
|
+
if (options.workflowRoot) validateWorkflowRoot(options.workflowRoot, errors);
|
|
341
|
+
if (options.stateRoot) validateStateRoot(options.stateRoot, options, errors);
|
|
342
|
+
if (errors.length) {
|
|
343
|
+
console.error(`Learning validation failed (${errors.length})`);
|
|
344
|
+
for (const error of errors) console.error(` - ${error}`);
|
|
345
|
+
process.exitCode = 1;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
console.log("Learning validation: OK");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
main();
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error(`Learning validation failed: ${error.message}`);
|
|
355
|
+
process.exitCode = 1;
|
|
356
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"workflow": "learning",
|
|
4
|
+
"config": null,
|
|
5
|
+
"structured_state": [
|
|
6
|
+
".speculo/learning/status.json",
|
|
7
|
+
".speculo/learning/changes/*/.status.json",
|
|
8
|
+
".speculo/learning/archive/*/*/.status.json"
|
|
9
|
+
],
|
|
10
|
+
"opaque_default": "preserve-byte-for-byte"
|
|
11
|
+
}
|
|
@@ -3,8 +3,8 @@ id: specdev
|
|
|
3
3
|
type: workflow
|
|
4
4
|
workflow: specdev
|
|
5
5
|
name: SpecDev Workflow
|
|
6
|
-
description:
|
|
7
|
-
keywords: [specdev, local-first, 规格驱动开发, decision-complete,
|
|
6
|
+
description: 以本地工件为唯一开发权威,从来源冻结、诊断、设计、原型、规格、Ticket、编排和审查推进到证据驱动实现、远程 reconcile 与知识归档。
|
|
7
|
+
keywords: [specdev, local-first, 规格驱动开发, decision-complete, prototype, code-review, TDD, 证据]
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# SpecDev Index
|
|
@@ -18,7 +18,7 @@ SpecDev 将“理解、决定、规划、执行、验证、沉淀”拆成职责
|
|
|
18
18
|
↓
|
|
19
19
|
Triage 冻结为本地 Source
|
|
20
20
|
↓
|
|
21
|
-
Diagnose / Grill /
|
|
21
|
+
Diagnose / Grill / Wayfinder / Prototype / Code Review / Architecture Review
|
|
22
22
|
↓
|
|
23
23
|
Spec 外部行为、范围、验收合同与关键约束
|
|
24
24
|
↓
|
|
@@ -52,7 +52,6 @@ Archive 归档历史并将经验证知识提升为当前长期知识
|
|
|
52
52
|
- `<Path>{roots.state}/specdev/changes/{change}/reviews/</Path>`
|
|
53
53
|
- `<Path>{roots.state}/specdev/changes/{change}/prototypes/</Path>`
|
|
54
54
|
- `<Path>{roots.state}/specdev/changes/{change}/questionnaires/</Path>`
|
|
55
|
-
- `<Path>{roots.state}/specdev/changes/{change}/eli_index.md</Path>` 与 `<Path>{roots.state}/specdev/changes/{change}/<number>_<topic>.md</Path>`
|
|
56
55
|
|
|
57
56
|
工件职责和冲突裁决位于 `<Path>{roots.workflows}/specdev/common/rules/artifact-contract.md</Path>`。
|
|
58
57
|
|
|
@@ -101,7 +100,6 @@ Archive 归档历史并将经验证知识提升为当前长期知识
|
|
|
101
100
|
- `<Path>{roots.state}/specdev/changes/{change}/reviews/</Path>`
|
|
102
101
|
- `<Path>{roots.state}/specdev/changes/{change}/prototypes/</Path>`
|
|
103
102
|
- `<Path>{roots.state}/specdev/changes/{change}/questionnaires/</Path>`
|
|
104
|
-
- `<Path>{roots.state}/specdev/changes/{change}/eli_index.md</Path>` 与 `<Path>{roots.state}/specdev/changes/{change}/<number>_<topic>.md</Path>`
|
|
105
103
|
|
|
106
104
|
## 全局治理原则
|
|
107
105
|
|
|
@@ -190,7 +188,6 @@ Change 从 active/blocked 转为 completed 时加载 `<Path>{roots.workflows}/sp
|
|
|
190
188
|
| 本地 change 完成且来源可关闭 | T-triage reconcile | A |
|
|
191
189
|
| 疑难 bug 或性能回归 | D-diagnose-bugs | S / T / I / R / W |
|
|
192
190
|
| 模糊但可通过决策访谈收敛 | G-grill-with-docs | P / S / T / W |
|
|
193
|
-
| 需要向刚上大一、没有专业背景的读者做 Markdown 与 ASCII 图解 | E-eli5 | 返回用户 / 继续当前 change |
|
|
194
191
|
| 路径超出单次上下文 | W-wayfinder | G / P / D / S / T |
|
|
195
192
|
| 需要用代码回答逻辑/UI 问题 | P-prototype | G / S / T / I |
|
|
196
193
|
| 固定点 diff、branch 或 PR review | C-code-review | completed / T / S / G |
|
|
@@ -209,8 +206,6 @@ Change 从 active/blocked 转为 completed 时加载 `<Path>{roots.workflows}/sp
|
|
|
209
206
|
- **A-archive-and-consolidate** — 归档与沉淀:校验本地完成与远程 reconcile 门,复用全局归档能力移动 completed change 并提升当前知识,或从代码访谈形成可归档知识 change。
|
|
210
207
|
- **C-code-review** — 代码审查:将 commit、branch、tag、merge-base 或 PR 解析为本地不可变固定点,执行隔离的标准轴与规范轴审查并持久化可恢复报告。
|
|
211
208
|
- **D-diagnose-bugs** — 诊断 Bug:先建立会在精确症状上变红的紧凑反馈回路,再通过最小化、排名假设和单变量探针确认根因,输出修复契约而不实施生产修复。
|
|
212
|
-
- **E-eli5** — 零基础新生解释:面向刚上大一、没有专业背景的读者解释一个主题;用 Markdown 和 ASCII 图解说明概念、数据与调用如何流动。
|
|
213
|
-
- **E-engineering-cognitive-mentor** — 工程认知导师:面向 Bug、项目源码、需求技术方案、架构设计与陌生技术领域的非执行型认知指导 Work;以证据、因果 Why、候选方案对比和逐轮澄清帮助用户形成可复述理解,并将完整问答轨迹持续持久化到当前 change。
|
|
214
209
|
- **G-grill-with-docs** — 设计访谈(带文档):以完整 frontier 逐轮推进设计树,直到每个决策分支都已关闭并获得用户共识,同时持续维护当前 change 的设计树、日志、领域上下文和架构决策。
|
|
215
210
|
- **I-implement** — 实现:基于 Ready Ticket 或获批小型 Spec 执行设计检查、TDD、动态派单、双轴审查、按 Goal Plan 选择的 current workspace 或 Ticket worktree 提交、直接父分支或候选合并验证和 Lead Evidence 回写。
|
|
216
211
|
- **I-init-setup** — 初始化设置:初始化 SpecDev 的语言、配置、全局状态、本地 change 追踪、领域知识布局、验证命令和并发治理。
|
|
@@ -20,7 +20,6 @@ SpecDev 通过分层工件避免同一决策被多个模型反复重做。每个
|
|
|
20
20
|
| Evidence | `<Path>{roots.state}/specdev/changes/{change}/evidence/{ticket-id}.md</Path>` | 实际修改、命令、结果、验收映射、偏差、风险和提交引用 | 新的产品或架构决策 |
|
|
21
21
|
| 代码审查 | `<Path>{roots.state}/specdev/changes/{change}/reviews/CR-###.md</Path>` | 固定点、标准轴和规范轴 finding | 实施修复或合并两轴排名 |
|
|
22
22
|
| 原型记录 | `<Path>{roots.state}/specdev/changes/{change}/prototypes/{prototype-id}/record.md</Path>` | 一个问题、分支、资产、答案、promotion 和清理 | 生产实现或多个问题的计划 |
|
|
23
|
-
| 零基础新生图解 | `<Path>{roots.state}/specdev/changes/{change}/eli_index.md</Path>` 与 `<Path>{roots.state}/specdev/changes/{change}/<number>_<topic>.md</Path>` | 面向刚上大一、没有专业背景读者的 Markdown 与 ASCII 图解;索引按序号持续追加 | 产品决定、架构决定或实现授权 |
|
|
24
23
|
| Stakeholder 问卷 | `<Path>{roots.state}/specdev/changes/{change}/questionnaires/{slug}.md</Path>` | 第三方原始回答和恢复条件 | 未经转录确认的产品/架构决定 |
|
|
25
24
|
| Wayfinder 地图 | `<Path>{roots.state}/specdev/changes/{change}/wayfinder-map.md</Path>` | 目的地、说明、已关闭决策索引、战争迷雾和范围之外 | 开放 Ticket 正文或答案详情 |
|
|
26
25
|
| Wayfinder Ticket | `<Path>{roots.state}/specdev/changes/{change}/investigation/{investigation-id}.md</Path>` | 一个可精确陈述的问题、类型、阻塞和关闭状态 | 解决方案评论或交付目标 |
|