@namewta/speculo 0.8.4 → 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/.speculo/README.md +7 -6
- 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/A-archive-and-consolidate/A-archive-and-consolidate.md +2 -0
- package/template/workflows/specdev/C-code-review/C-code-review.md +2 -0
- package/template/workflows/specdev/D-diagnose-bugs/D-diagnose-bugs.md +2 -0
- package/template/workflows/specdev/G-grill-with-docs/G-grill-with-docs.md +2 -0
- package/template/workflows/specdev/I-implement/I-implement.md +2 -0
- package/template/workflows/specdev/I-init-setup/I-init-setup.md +3 -1
- package/template/workflows/specdev/INDEX.md +9 -241
- package/template/workflows/specdev/P-goal-plan/P-goal-plan.md +2 -0
- package/template/workflows/specdev/P-prototype/P-prototype.md +2 -0
- package/template/workflows/specdev/R-review-architecture/R-review-architecture.md +2 -0
- package/template/workflows/specdev/README.md +244 -0
- package/template/workflows/specdev/S-spec/S-spec.md +2 -0
- package/template/workflows/specdev/T-tickets/T-tickets.md +2 -0
- package/template/workflows/specdev/T-triage/T-triage.md +3 -1
- package/template/workflows/specdev/W-wayfinder/W-wayfinder.md +2 -0
- 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 -95
- package/template/workflows/specdev/E-engineering-cognitive-mentor/E-engineering-cognitive-mentor.md +0 -252
- 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
|
+
}
|
|
@@ -9,6 +9,8 @@ keywords: [归档, consolidation, ADR, context, research, knowledge, 代码库
|
|
|
9
9
|
|
|
10
10
|
# 归档与沉淀
|
|
11
11
|
|
|
12
|
+
> 激活本 Work 后,先读取 `<Path>{roots.workflows}/specdev/README.md</Path>`,再执行本入口。
|
|
13
|
+
|
|
12
14
|
A 是 SpecDev 的归档 wrapper:它拥有模式选择、SpecDev 完成门和代码访谈;机械扫描、dry-run、知识毕业、合并、清理、移动与重读由 `<Path>{roots.skills}/archive-and-consolidate/SKILL.md</Path>` 单一维护。
|
|
13
15
|
|
|
14
16
|
## 模式
|
|
@@ -9,6 +9,8 @@ keywords: [code-review, review, diff, fixed-point, PR, 标准, 规范]
|
|
|
9
9
|
|
|
10
10
|
# 代码审查
|
|
11
11
|
|
|
12
|
+
> 激活本 Work 后,先读取 `<Path>{roots.workflows}/specdev/README.md</Path>`,再执行本入口。
|
|
13
|
+
|
|
12
14
|
C 是独立 review 入口,不实施修复。它拥有 `<Path>{roots.state}/specdev/changes/{change}/reviews/</Path>`;I 的最终审查由 I 写入 Evidence,不写本目录。
|
|
13
15
|
|
|
14
16
|
## 输入
|
|
@@ -9,6 +9,8 @@ keywords: [bug, 诊断, 红灯, tight-loop, 根因, 复现, 假设]
|
|
|
9
9
|
|
|
10
10
|
# 诊断 Bug
|
|
11
11
|
|
|
12
|
+
> 激活本 Work 后,先读取 `<Path>{roots.workflows}/specdev/README.md</Path>`,再执行本入口。
|
|
13
|
+
|
|
12
14
|
D 的主导词是**红灯**:没有一条已执行且能在此 bug 上变红的紧凑命令,就没有可进入的假设阶段。D 默认只读项目代码,可以创建 change 诊断工件和经授权的临时可撤销探针;生产修复由 Implement 拥有。
|
|
13
15
|
|
|
14
16
|
## 输入与所有权
|
|
@@ -9,6 +9,8 @@ keywords: [设计访谈, grilling, design-tree, frontier, ADR, LOG, CONTEXT, 决
|
|
|
9
9
|
|
|
10
10
|
# 设计访谈(带文档)
|
|
11
11
|
|
|
12
|
+
> 激活本 Work 后,先读取 `<Path>{roots.workflows}/specdev/README.md</Path>`,再执行本入口。
|
|
13
|
+
|
|
12
14
|
不留情面地访谈用户,直到达成共识。把这件事映射为一棵**设计树(design tree)**:每个决策都会分出挂在它下面的后续决策。
|
|
13
15
|
|
|
14
16
|
按**轮次**推进这棵树。**前沿(frontier)** 是所有前置条件已经确定的决策——那些现在就能问、不必猜测尚未得到答案的问题。每轮询问完整 frontier;用户的答案会重塑设计树并解除下一层问题的阻塞。
|
|
@@ -9,6 +9,8 @@ keywords: [实现, TDD, Lead, subagent, worktree, current workspace, direct-pare
|
|
|
9
9
|
|
|
10
10
|
# 实现
|
|
11
11
|
|
|
12
|
+
> 激活本 Work 后,先读取 `<Path>{roots.workflows}/specdev/README.md</Path>`,再执行本入口。
|
|
13
|
+
|
|
12
14
|
本 work 保留模块设计检查、design-it-twice、TDD 红绿循环、双轴审查和证据治理。Ticket 模式按 Goal Plan 的 `ticket_workspace_policy` 选择 current workspace 串行直接父分支或独立 worktree candidate-merge;Lead 根据实际情况自行实现或动态派单。
|
|
13
15
|
|
|
14
16
|
## 执行模式
|
|
@@ -9,11 +9,13 @@ keywords: [初始化, 配置, status, tracking, 验证命令]
|
|
|
9
9
|
|
|
10
10
|
# 初始化设置
|
|
11
11
|
|
|
12
|
+
> 激活本 Work 后,先读取 `<Path>{roots.workflows}/specdev/README.md</Path>`,再执行本入口。
|
|
13
|
+
|
|
12
14
|
首次使用 SpecDev、状态根不存在或治理契约发生变化后运行。此 work 只初始化 SpecDev 的状态与配置,不修改项目业务代码。
|
|
13
15
|
|
|
14
16
|
## 规范输入
|
|
15
17
|
|
|
16
|
-
-
|
|
18
|
+
- 工作流运行合同:`<Path>{roots.workflows}/specdev/README.md</Path>`
|
|
17
19
|
- 路径引用契约:`<Path>{roots.workflows}/specdev/common/rules/path-reference-contract.md</Path>`
|
|
18
20
|
- 配置模板:`<Path>{roots.workflows}/specdev/I-init-setup/config-template.json</Path>`
|
|
19
21
|
- 配置 Schema:`<Path>{roots.workflows}/specdev/common/schemas/config.schema.json</Path>`
|