@namewta/speculo 0.8.13 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/src/cli.js +12 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/doctor.d.ts +10 -0
- package/dist/src/doctor.js +70 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/index.js +105 -56
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel.d.ts +98 -0
- package/dist/src/kernel.js +29 -0
- package/dist/src/kernel.js.map +1 -0
- package/dist/src/refresh.js +55 -24
- package/dist/src/refresh.js.map +1 -1
- package/dist/src/structured.d.ts +2 -2
- package/dist/src/structured.js +114 -241
- package/dist/src/structured.js.map +1 -1
- package/package.json +3 -2
- package/template/.speculo/README.md +1 -1
- package/template/.speculo/capabilities.json +14 -0
- package/template/.speculo/kernel/README.md +10 -0
- package/template/.speculo/kernel/capability-profile.schema.json +14 -0
- package/template/.speculo/kernel/checkpoint.schema.json +7 -0
- package/template/.speculo/kernel/trace-event.schema.json +8 -0
- package/template/.speculo/kernel/workflow-manifest.schema.json +13 -0
- package/template/.speculo/kernel.json +10 -0
- package/template/.speculo/refresh-contract.json +4 -1
- package/template/AGENTS.md +0 -3
- package/template/commands/archive-and-consolidate.md +13 -40
- package/template/commands/status.md +3 -3
- package/template/workflows/learning/A-archive/A-archive.md +25 -0
- package/template/workflows/learning/A-assess-and-plan/A-assess-and-plan.md +17 -23
- package/template/workflows/learning/A-assess-and-plan/background-template.md +13 -0
- package/template/workflows/learning/A-assess-and-plan/change-status-template.json +21 -14
- package/template/workflows/learning/A-assess-and-plan/course-template.md +28 -0
- package/template/workflows/learning/C-consolidate/C-consolidate.md +33 -0
- package/template/workflows/learning/H-homework/H-homework.md +31 -0
- package/template/workflows/learning/H-homework/homework-template.md +63 -0
- package/template/workflows/learning/I-init-setup/I-init-setup.md +14 -19
- package/template/workflows/learning/I-init-setup/context-index-template.md +3 -3
- package/template/workflows/learning/I-init-setup/learner-profile-template.md +12 -10
- package/template/workflows/learning/I-init-setup/review-index-template.md +1 -1
- package/template/workflows/learning/INDEX.md +7 -9
- package/template/workflows/learning/L-lesson/L-lesson.md +32 -0
- package/template/workflows/learning/L-lesson/lesson-template.md +51 -0
- package/template/workflows/learning/R-review/R-review.md +13 -18
- package/template/workflows/learning/R-review/review-template.md +13 -6
- package/template/workflows/learning/README.md +86 -81
- package/template/workflows/learning/_state/status.json +1 -1
- package/template/workflows/learning/common/rules/artifact-contract.md +11 -25
- package/template/workflows/learning/common/rules/assessment-policy.md +9 -4
- package/template/workflows/learning/common/rules/knowledge-organization.md +4 -6
- package/template/workflows/learning/common/rules/mastery-policy.md +3 -19
- package/template/workflows/learning/common/rules/path-reference-contract.md +3 -5
- package/template/workflows/learning/common/rules/teaching-policy.md +9 -9
- package/template/workflows/learning/common/schemas/change-status.schema.json +46 -18
- package/template/workflows/learning/common/schemas/status.schema.json +25 -24
- package/template/workflows/learning/common/skills/topic-synthesis/SKILL.md +17 -0
- package/template/workflows/learning/common/skills/topic-synthesis/claim-template.md +15 -0
- package/template/workflows/learning/common/tools/relocate-learning.mjs +208 -0
- package/template/workflows/learning/common/tools/validate-learning.mjs +195 -297
- package/template/workflows/learning/manifest.json +1 -0
- package/template/workflows/learning/runtime-contract.json +3 -2
- package/template/workflows/ops/manifest.json +1 -0
- package/template/workflows/person/manifest.json +1 -0
- package/template/workflows/specdev/manifest.json +1 -0
- package/template/workflows/workflow-manifest.schema.json +14 -0
- package/template/workflows/learning/A-archive-and-consolidate/A-archive-and-consolidate.md +0 -38
- package/template/workflows/learning/A-archive-and-consolidate/promotion-plan-template.md +0 -23
- package/template/workflows/learning/A-assess-and-plan/learning-plan-template.md +0 -28
- package/template/workflows/learning/E-eli5/E-eli5.md +0 -37
- package/template/workflows/learning/E-eli5/lesson-template.md +0 -31
- package/template/workflows/learning/P-practice/P-practice.md +0 -34
- package/template/workflows/learning/P-practice/practice-template.md +0 -16
- package/template/workflows/learning/Q-quiz/Q-quiz.md +0 -34
- package/template/workflows/learning/Q-quiz/quiz-artifact-template.md +0 -16
- package/template/workflows/learning/common/skills/knowledge-promotion/SKILL.md +0 -46
- package/template/workflows/learning/common/skills/knowledge-promotion/domain-index-template.md +0 -6
- package/template/workflows/learning/common/skills/knowledge-promotion/domain-overview-template.md +0 -19
- package/template/workflows/learning/common/skills/knowledge-promotion/knowledge-template.md +0 -25
|
@@ -1,356 +1,254 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
readFileSync,
|
|
6
|
-
readdirSync,
|
|
7
|
-
statSync,
|
|
8
|
-
} from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
9
5
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
10
6
|
import { fileURLToPath } from "node:url";
|
|
11
7
|
|
|
12
8
|
const CHANGE_NAME = /^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
13
9
|
const DOMAIN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
14
10
|
const WORK_ID = /^learning\/[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"E-eli5",
|
|
24
|
-
"I-init-setup",
|
|
25
|
-
"P-practice",
|
|
26
|
-
"Q-quiz",
|
|
27
|
-
"R-review",
|
|
28
|
-
]);
|
|
29
|
-
|
|
30
|
-
function parseArgs(argv) {
|
|
11
|
+
const LOCATOR = /^(?:changes\/.+|archive\/[0-9]{4}-[0-9]{2}\/.+)$/;
|
|
12
|
+
const EXPECTED_WORKS = new Set(["A-archive", "A-assess-and-plan", "C-consolidate", "H-homework", "I-init-setup", "L-lesson", "R-review"]);
|
|
13
|
+
const OLD_WORKS = new Set(["A-archive-and-consolidate", "E-eli5", "P-practice", "Q-quiz"]);
|
|
14
|
+
const PHASE = new Set(["planning", "teaching", "homework", "review", "consolidating", "closed", "archived"]);
|
|
15
|
+
const LIFECYCLE = new Set(["active", "blocked", "closed", "archived"]);
|
|
16
|
+
const EVIDENCE = new Set(["not_attempted", "passed", "needs_review"]);
|
|
17
|
+
|
|
18
|
+
function options(argv) {
|
|
31
19
|
const result = { workflowRoot: null, stateRoot: null, stage: null, change: null, selfCheck: false };
|
|
32
|
-
for (let
|
|
33
|
-
const arg = argv[
|
|
34
|
-
if (arg === "--workflow-root") result.workflowRoot = resolve(argv[++
|
|
35
|
-
else if (arg === "--state-root") result.stateRoot = resolve(argv[++
|
|
36
|
-
else if (arg === "--stage") result.stage = argv[++
|
|
37
|
-
else if (arg === "--change") result.change = argv[++
|
|
20
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
21
|
+
const arg = argv[i];
|
|
22
|
+
if (arg === "--workflow-root") result.workflowRoot = resolve(argv[++i] ?? "");
|
|
23
|
+
else if (arg === "--state-root") result.stateRoot = resolve(argv[++i] ?? "");
|
|
24
|
+
else if (arg === "--stage") result.stage = argv[++i] ?? null;
|
|
25
|
+
else if (arg === "--change") result.change = argv[++i] ?? null;
|
|
38
26
|
else if (arg === "--self-check") result.selfCheck = true;
|
|
39
27
|
else throw new Error(`unknown option: ${arg}`);
|
|
40
28
|
}
|
|
41
|
-
if (!result.workflowRoot && !result.stateRoot && !result.selfCheck)
|
|
42
|
-
|
|
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
|
-
}
|
|
29
|
+
if (!result.workflowRoot && !result.stateRoot && !result.selfCheck) throw new Error("use --workflow-root, --state-root, or --self-check");
|
|
30
|
+
if (result.stage && !new Set(["pre-archive", "complete"]).has(result.stage)) throw new Error("--stage must be pre-archive or complete");
|
|
47
31
|
if (result.stage && !result.change) throw new Error("--stage requires --change");
|
|
48
32
|
if (result.change && !CHANGE_NAME.test(result.change)) throw new Error("--change has an invalid name");
|
|
49
33
|
return result;
|
|
50
34
|
}
|
|
51
35
|
|
|
52
|
-
function isFile(path) {
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function isDirectory(path) {
|
|
57
|
-
return existsSync(path) && statSync(path).isDirectory();
|
|
58
|
-
}
|
|
59
|
-
|
|
36
|
+
function isFile(path) { return existsSync(path) && statSync(path).isFile(); }
|
|
37
|
+
function isDir(path) { return existsSync(path) && statSync(path).isDirectory(); }
|
|
60
38
|
function readJson(path, label, errors) {
|
|
61
|
-
try {
|
|
62
|
-
|
|
63
|
-
} catch (error) {
|
|
64
|
-
errors.push(`${label}: invalid JSON (${error.message})`);
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
39
|
+
try { return JSON.parse(readFileSync(path, "utf8")); }
|
|
40
|
+
catch (error) { errors.push(`${label}: invalid JSON (${error.message})`); return null; }
|
|
67
41
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const files = [];
|
|
42
|
+
function files(root) {
|
|
43
|
+
if (!isDir(root)) return [];
|
|
44
|
+
const result = [];
|
|
72
45
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
73
46
|
const path = join(root, entry.name);
|
|
74
|
-
if (entry.isDirectory())
|
|
75
|
-
else if (entry.isFile())
|
|
47
|
+
if (entry.isDirectory()) result.push(...files(path));
|
|
48
|
+
else if (entry.isFile()) result.push(path);
|
|
76
49
|
}
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function sameMembers(actual, expected) {
|
|
81
|
-
return actual.size === expected.size && [...actual].every((item) => expected.has(item));
|
|
50
|
+
return result;
|
|
82
51
|
}
|
|
83
|
-
|
|
84
52
|
function duplicates(values) {
|
|
85
53
|
const seen = new Set();
|
|
86
54
|
return values.filter((value) => seen.has(value) || !seen.add(value));
|
|
87
55
|
}
|
|
88
|
-
|
|
89
|
-
function
|
|
90
|
-
|
|
56
|
+
function validDate(value) { return typeof value === "string" && !Number.isNaN(Date.parse(value)); }
|
|
57
|
+
function frontmatter(text) {
|
|
58
|
+
const match = /^---\s*\n([\s\S]*?)\n---\s*/.exec(text);
|
|
59
|
+
const result = {};
|
|
60
|
+
if (!match) return result;
|
|
61
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
62
|
+
const field = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(line);
|
|
63
|
+
if (!field) continue;
|
|
64
|
+
const value = field[2].trim();
|
|
65
|
+
if (/^\[.*\]$/.test(value)) result[field[1]] = value.slice(1, -1).split(",").map((item) => item.trim()).filter(Boolean);
|
|
66
|
+
else if (/^\d+$/.test(value)) result[field[1]] = Number(value);
|
|
67
|
+
else result[field[1]] = value.replace(/^['"]|['"]$/g, "");
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
91
70
|
}
|
|
92
71
|
|
|
93
72
|
function validateWorkflowRoot(root, errors) {
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
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
|
-
}
|
|
73
|
+
const required = ["INDEX.md", "README.md", "_state/status.json", "runtime-contract.json"];
|
|
74
|
+
for (const file of required) if (!isFile(join(root, file))) errors.push(`${file}: required workflow file is missing`);
|
|
75
|
+
if (!isFile(join(root, "README.md"))) return;
|
|
76
|
+
const index = readFileSync(join(root, "INDEX.md"), "utf8");
|
|
77
|
+
const readme = readFileSync(join(root, "README.md"), "utf8");
|
|
78
|
+
if (!/^id: learning$/m.test(index) || !/^type: workflow$/m.test(index) || !/^workflow: learning$/m.test(index)) errors.push("INDEX.md: invalid identity");
|
|
79
|
+
if (!(index.includes("## 主题知识") || index.includes("## 永久知识")) || !index.includes("## Work 激活") || !index.includes("<Path>{roots.workflows}/learning/README.md</Path>")) errors.push("INDEX.md: missing passive/activation sections");
|
|
80
|
+
if ((readme.match(/AUTO-INDEX-START/g) ?? []).length !== 1 || (readme.match(/AUTO-INDEX-END/g) ?? []).length !== 1) errors.push("README.md: requires one AUTO-INDEX marker pair");
|
|
81
|
+
const works = new Set(readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^[A-Z]-/.test(entry.name) && isFile(join(root, entry.name, `${entry.name}.md`))).map((entry) => entry.name));
|
|
82
|
+
if (works.size !== EXPECTED_WORKS.size || [...EXPECTED_WORKS].some((work) => !works.has(work))) errors.push(`workflow works mismatch: expected ${[...EXPECTED_WORKS].sort().join(", ")}; found ${[...works].sort().join(", ")}`);
|
|
83
|
+
for (const old of OLD_WORKS) if (isDir(join(root, old)) && files(join(root, old)).length) errors.push(`${old}: obsolete v1 work is still shipped`);
|
|
122
84
|
for (const work of works) {
|
|
123
|
-
const
|
|
124
|
-
if (!isFile(path)) {
|
|
125
|
-
errors.push(`${work}: missing same-named entry`);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
const text = readFileSync(path, "utf8");
|
|
85
|
+
const text = readFileSync(join(root, work, `${work}.md`), "utf8");
|
|
129
86
|
if (!text.includes("<Path>{roots.workflows}/learning/README.md</Path>")) errors.push(`${work}: missing activation contract reference`);
|
|
130
87
|
if (!/^workflow: learning$/m.test(text) || !/^type: workflow-entry$/m.test(text)) errors.push(`${work}: invalid frontmatter identity`);
|
|
131
88
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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"]],
|
|
89
|
+
const seed = readJson(join(root, "_state/status.json"), "_state/status.json", errors);
|
|
90
|
+
if (seed && (seed.schema_version !== 2 || seed.workflow !== "learning" || !Array.isArray(seed.active) || !Array.isArray(seed.archived))) errors.push("_state/status.json: expected empty learning schema v2 status");
|
|
91
|
+
const runtime = readJson(join(root, "runtime-contract.json"), "runtime-contract.json", errors);
|
|
92
|
+
if (runtime && (runtime.schema_version !== 1 || runtime.workflow !== "learning" || runtime.config !== null || runtime.opaque_default !== "preserve-byte-for-byte" || !runtime.structured_state.includes(".speculo/learning/changes/**/.status.json"))) errors.push("runtime-contract.json: missing recursive v2 paths");
|
|
93
|
+
for (const schema of ["status.schema.json", "change-status.schema.json"]) readJson(join(root, "common", "schemas", schema), `common/schemas/${schema}`, errors);
|
|
94
|
+
const contracts = [
|
|
95
|
+
["L-lesson/L-lesson.md", ["30–40", "estimated_minutes", "time_budget", "ELI5"]],
|
|
96
|
+
["L-lesson/lesson-template.md", ["lesson_id", "source_ids", "反例与边界", "文字等价物"]],
|
|
97
|
+
["H-homework/H-homework.md", ["Submission: ready", "Explain (English)"]],
|
|
98
|
+
["H-homework/homework-template.md", ["Q1", "A1", "Submission: pending"]],
|
|
99
|
+
["common/rules/teaching-policy.md", ["coverage_depth", "不以字符数"]],
|
|
156
100
|
];
|
|
157
|
-
for (const [
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
101
|
+
for (const [file, markers] of contracts) {
|
|
102
|
+
const path = join(root, file);
|
|
103
|
+
if (!isFile(path)) { errors.push(`${file}: required contract is missing`); continue; }
|
|
162
104
|
const text = readFileSync(path, "utf8");
|
|
163
|
-
for (const marker of
|
|
164
|
-
if (!text.includes(marker)) errors.push(`${relative(root, path)}: missing '${marker}'`);
|
|
165
|
-
}
|
|
105
|
+
for (const marker of markers) if (!text.includes(marker)) errors.push(`${file}: missing '${marker}'`);
|
|
166
106
|
}
|
|
167
107
|
}
|
|
168
108
|
|
|
169
|
-
function
|
|
170
|
-
if (!
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
}
|
|
109
|
+
function validateEntry(entry, label, errors, archived) {
|
|
110
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) { errors.push(`${label}: must be an object`); return null; }
|
|
111
|
+
for (const key of ["change_id", "kind", "domain", "topic_id", "locator", "parent_change", "root_change", "current_work", "works_run"]) if (!(key in entry)) errors.push(`${label}: missing ${key}`);
|
|
112
|
+
if (!CHANGE_NAME.test(entry.change_id ?? "") || !["learning", "consolidation"].includes(entry.kind) || !DOMAIN.test(entry.domain ?? "") || !DOMAIN.test(entry.topic_id ?? "")) errors.push(`${label}: invalid identity`);
|
|
113
|
+
if (!LOCATOR.test(entry.locator ?? "") || String(entry.locator).split("/").includes("..")) errors.push(`${label}: invalid locator`);
|
|
114
|
+
if (!(entry.parent_change === null || CHANGE_NAME.test(entry.parent_change ?? "")) || !CHANGE_NAME.test(entry.root_change ?? "")) errors.push(`${label}: invalid parent/root`);
|
|
115
|
+
if (!(entry.current_work === null || WORK_ID.test(entry.current_work ?? ""))) errors.push(`${label}: invalid current_work`);
|
|
116
|
+
if (!Array.isArray(entry.works_run) || entry.works_run.some((item) => !WORK_ID.test(item)) || duplicates(entry.works_run).length) errors.push(`${label}: invalid works_run`);
|
|
117
|
+
if (entry.updated_at !== undefined && !validDate(entry.updated_at)) errors.push(`${label}: invalid updated_at`);
|
|
118
|
+
if (archived && !String(entry.locator).startsWith("archive/")) errors.push(`${label}: archived entry must use archive locator`);
|
|
119
|
+
if (!archived && String(entry.locator).startsWith("archive/")) errors.push(`${label}: active entry cannot use archive locator`);
|
|
120
|
+
return entry;
|
|
196
121
|
}
|
|
197
122
|
|
|
198
|
-
function
|
|
199
|
-
const mastery =
|
|
200
|
-
if (!mastery || typeof mastery !== "object"
|
|
201
|
-
|
|
202
|
-
|
|
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`);
|
|
123
|
+
function validateMastery(value, label, errors) {
|
|
124
|
+
const mastery = value.mastery;
|
|
125
|
+
if (!mastery || typeof mastery !== "object") { errors.push(`${label}: mastery is required`); return; }
|
|
126
|
+
if (!["unverified", "immediate", "retention_verified"].includes(mastery.overall) || !EVIDENCE.has(mastery.immediate) || !["not_scheduled", "due", "passed", "needs_review"].includes(mastery.retention) || !EVIDENCE.has(mastery.critical_objectives) || !EVIDENCE.has(mastery.transfer)) errors.push(`${label}: invalid mastery projection`);
|
|
127
|
+
if (mastery.overall === "retention_verified" && mastery.retention !== "passed") errors.push(`${label}: retention_verified requires retention passed`);
|
|
207
128
|
if (!Array.isArray(mastery.blocking_misconceptions) || !Array.isArray(mastery.evidence)) errors.push(`${label}: mastery arrays are required`);
|
|
208
|
-
if (!(mastery.next_review_at === null ||
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (!
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
errors.push(`${label}:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
errors.push(`${label}:
|
|
129
|
+
if (!(mastery.next_review_at === null || validDate(mastery.next_review_at))) errors.push(`${label}: invalid next_review_at`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function validateChange(value, expectedId, expectedLocator, errors) {
|
|
133
|
+
const label = `${expectedId}/.status.json`;
|
|
134
|
+
if (!value || typeof value !== "object") return;
|
|
135
|
+
if (value.schema_version === 1) { errors.push(`${label}: learning-reset-required; v1 is not auto-migrated`); return; }
|
|
136
|
+
if (value.schema_version !== 2 || value.artifact !== "learning-change-status" || value.change_id !== expectedId || value.locator !== expectedLocator) errors.push(`${label}: invalid identity/schema/locator`);
|
|
137
|
+
if (!CHANGE_NAME.test(value.change_id ?? "") || !["learning", "consolidation"].includes(value.kind) || !DOMAIN.test(value.domain ?? "") || !DOMAIN.test(value.topic_id ?? "")) errors.push(`${label}: invalid domain/kind/topic`);
|
|
138
|
+
if (!LIFECYCLE.has(value.lifecycle) || !PHASE.has(value.phase)) errors.push(`${label}: invalid lifecycle/phase`);
|
|
139
|
+
if (!(value.current_work === null || WORK_ID.test(value.current_work ?? "")) || !Array.isArray(value.works_run) || duplicates(value.works_run).length) errors.push(`${label}: invalid work projection`);
|
|
140
|
+
if (!validDate(value.created_at) || !validDate(value.updated_at)) errors.push(`${label}: invalid timestamps`);
|
|
141
|
+
if (!(value.closed_at === null || validDate(value.closed_at)) || !(value.archived_at === null || validDate(value.archived_at))) errors.push(`${label}: invalid close/archive timestamp`);
|
|
142
|
+
if (value.lifecycle === "archived" && (value.phase !== "archived" || value.current_work !== null || !String(value.locator).startsWith("archive/"))) errors.push(`${label}: archive state does not match locator`);
|
|
143
|
+
if (value.lifecycle !== "archived" && (value.archived_at !== null || value.archive_path !== null || String(value.locator).startsWith("archive/"))) errors.push(`${label}: active state contains archive fields`);
|
|
144
|
+
if (!value.homework || !["none", "pending", "reviewed", "needs_revision"].includes(value.homework.status) || !(value.homework.latest_id === null || typeof value.homework.latest_id === "string")) errors.push(`${label}: invalid homework projection`);
|
|
145
|
+
if (!Array.isArray(value.children) || !Array.isArray(value.blockers)) errors.push(`${label}: children/blockers must be arrays`);
|
|
146
|
+
validateMastery(value, label, errors);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function validateLessons(changeRoot, errors) {
|
|
150
|
+
const lessonRoot = join(changeRoot, "lessons");
|
|
151
|
+
if (!isDir(lessonRoot)) return;
|
|
152
|
+
for (const path of files(lessonRoot).filter((candidate) => /\.md$/.test(candidate) && !candidate.endsWith("INDEX.md"))) {
|
|
153
|
+
const text = readFileSync(path, "utf8");
|
|
154
|
+
const label = relative(changeRoot, path).split(sep).join("/");
|
|
155
|
+
const meta = frontmatter(text);
|
|
156
|
+
if (!/^L-[0-9]{3}(?:-[a-z0-9-]+)?$/.test(String(meta.lesson_id ?? ""))) errors.push(`${label}: lesson_id is required`);
|
|
157
|
+
if (!Number.isInteger(meta.estimated_minutes) || meta.estimated_minutes < 30 || meta.estimated_minutes > 40) errors.push(`${label}: estimated_minutes must be 30–40`);
|
|
158
|
+
if (!Array.isArray(meta.time_budget) && !text.includes("time_budget:")) errors.push(`${label}: time_budget is required`);
|
|
159
|
+
for (const heading of ["宏观", "机制", "精确定义", "English", "正例", "反例", "迁移", "误区", "总结", "来源"]) if (!text.includes(heading)) errors.push(`${label}: missing teaching coverage '${heading}'`);
|
|
160
|
+
if (!/(文字等价物|完整的文字|文字替代)/.test(text)) errors.push(`${label}: visual representation needs a text equivalent`);
|
|
161
|
+
if (!/source_ids:|\| Source ID \|/.test(text)) errors.push(`${label}: source_ids/source table is required`);
|
|
162
|
+
if (/\bverdict\b|\bmastered\b|^##\s*(?:Answers|Review)/mi.test(text)) errors.push(`${label}: lesson must not contain homework review/mastery fields`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function validateHomework(changeRoot, errors) {
|
|
167
|
+
const homeworkRoot = join(changeRoot, "homework");
|
|
168
|
+
if (!isDir(homeworkRoot)) return;
|
|
169
|
+
for (const path of files(homeworkRoot).filter((candidate) => /^HW-.*\.md$/.test(candidate.split(sep).pop() ?? ""))) {
|
|
170
|
+
const text = readFileSync(path, "utf8");
|
|
171
|
+
const label = relative(changeRoot, path).split(sep).join("/");
|
|
172
|
+
const ready = /(^|\n)Submission:\s*ready\s*(?:\n|$)/i.test(text);
|
|
173
|
+
const reviewed = /^##\s+Review\s*$/mi.test(text);
|
|
174
|
+
if (!/(^|\n)Submission:\s*(?:pending|ready)\s*(?:\n|$)/i.test(text)) errors.push(`${label}: Submission marker is required`);
|
|
175
|
+
const questions = [...text.matchAll(/^###\s+Q(\d+)\b/gm)].map((match) => Number(match[1]));
|
|
176
|
+
const answers = [...text.matchAll(/^###\s+A(\d+)\b/gm)].map((match) => Number(match[1]));
|
|
177
|
+
if (!questions.length || questions.length !== answers.length || questions.some((number, index) => number !== index + 1) || answers.some((number, index) => number !== index + 1)) errors.push(`${label}: Q/A numbering must be contiguous and paired`);
|
|
178
|
+
if (reviewed && !ready) errors.push(`${label}: review cannot exist before Submission: ready`);
|
|
179
|
+
if (reviewed && !/correct|partial|incorrect|uncertain/i.test(text) || reviewed && !/Explain \(English\)/.test(text)) errors.push(`${label}: review needs verdict and Explain (English)`);
|
|
242
180
|
}
|
|
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
181
|
}
|
|
252
182
|
|
|
253
183
|
function validateContext(stateRoot, errors) {
|
|
254
|
-
const
|
|
255
|
-
if (!
|
|
256
|
-
for (const required of ["INDEX.md", "REVIEW.md"]) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
}
|
|
184
|
+
const context = join(stateRoot, "context");
|
|
185
|
+
if (!isDir(context)) return;
|
|
186
|
+
for (const required of ["INDEX.md", "REVIEW.md"]) if (!isFile(join(context, required))) errors.push(`context/${required}: missing`);
|
|
187
|
+
const ids = new Set();
|
|
188
|
+
for (const path of files(context).filter((candidate) => candidate.endsWith(".md"))) {
|
|
189
|
+
const text = readFileSync(path, "utf8");
|
|
190
|
+
const id = /^\|\s*topic_id\s*\|\s*([^|]+)\s*\|/mi.exec(text)?.[1]?.trim();
|
|
191
|
+
if (id) { if (ids.has(id)) errors.push(`${relative(context, path)}: duplicate topic_id ${id}`); ids.add(id); }
|
|
192
|
+
if (path.includes(`${sep}topics${sep}`) && !/provenance|source_change_id|claim_id/i.test(text)) errors.push(`${relative(context, path)}: topic view needs provenance`);
|
|
279
193
|
}
|
|
280
194
|
}
|
|
281
195
|
|
|
282
|
-
function validateStateRoot(root,
|
|
196
|
+
function validateStateRoot(root, opts, errors) {
|
|
283
197
|
const statusPath = join(root, "status.json");
|
|
284
|
-
if (!isFile(statusPath)) {
|
|
285
|
-
errors.push("status.json: missing");
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
198
|
+
if (!isFile(statusPath)) { errors.push("status.json: missing"); return; }
|
|
288
199
|
const status = readJson(statusPath, "status.json", errors);
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
errors.push(`${
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
if (
|
|
200
|
+
if (!status) return;
|
|
201
|
+
if (status.schema_version === 1) { errors.push("status.json: learning-reset-required; v1 is not auto-migrated"); return; }
|
|
202
|
+
if (status.schema_version !== 2 || status.workflow !== "learning" || !Array.isArray(status.active) || !Array.isArray(status.archived)) { errors.push("status.json: expected learning schema v2"); return; }
|
|
203
|
+
const active = status.active.map((entry, index) => validateEntry(entry, `status.active[${index}]`, errors, false)).filter(Boolean);
|
|
204
|
+
const archived = status.archived.map((entry, index) => validateEntry(entry, `status.archived[${index}]`, errors, true)).filter(Boolean);
|
|
205
|
+
const all = [...active, ...archived];
|
|
206
|
+
const ids = all.map((entry) => entry.change_id);
|
|
207
|
+
if (duplicates(ids).length) errors.push("status.json: duplicate or overlapping change ids");
|
|
208
|
+
const byId = new Map(all.map((entry) => [entry.change_id, entry]));
|
|
209
|
+
for (const entry of all) {
|
|
210
|
+
const path = join(root, ...String(entry.locator).split("/"), ".status.json");
|
|
211
|
+
if (!isFile(path)) { errors.push(`${entry.change_id}: indexed Change is missing ${path}`); continue; }
|
|
212
|
+
const value = readJson(path, `${entry.change_id}/.status.json`, errors);
|
|
213
|
+
validateChange(value, entry.change_id, entry.locator, errors);
|
|
214
|
+
if (value && (value.domain !== entry.domain || value.topic_id !== entry.topic_id || value.parent_change !== entry.parent_change || value.current_work !== entry.current_work || JSON.stringify(value.works_run) !== JSON.stringify(entry.works_run))) errors.push(`${entry.change_id}: global and change projections differ`);
|
|
215
|
+
const seen = new Set();
|
|
216
|
+
let parent = entry.parent_change;
|
|
217
|
+
while (parent !== null) {
|
|
218
|
+
if (seen.has(parent)) { errors.push(`${entry.change_id}: parent cycle detected`); break; }
|
|
219
|
+
seen.add(parent);
|
|
220
|
+
if (!byId.has(parent)) { errors.push(`${entry.change_id}: parent_change ${parent} is not indexed`); break; }
|
|
221
|
+
parent = byId.get(parent).parent_change;
|
|
222
|
+
}
|
|
223
|
+
const changeRoot = join(root, ...String(entry.locator).split("/"));
|
|
224
|
+
validateLessons(changeRoot, errors);
|
|
225
|
+
validateHomework(changeRoot, errors);
|
|
226
|
+
if (opts.stage === "pre-archive" && (!opts.change || opts.change === entry.change_id) && value?.lifecycle !== "closed") errors.push(`${entry.change_id}: pre-archive requires lifecycle closed`);
|
|
227
|
+
if (opts.stage === "complete" && (!opts.change || opts.change === entry.change_id) && !String(entry.locator).startsWith("archive/")) errors.push(`${entry.change_id}: complete stage requires archived locator`);
|
|
228
|
+
}
|
|
229
|
+
const locationsPath = join(root, "locations.json");
|
|
230
|
+
if (all.length && !isFile(locationsPath)) errors.push("locations.json: required when Changes exist");
|
|
231
|
+
if (isFile(locationsPath)) {
|
|
232
|
+
const locations = readJson(locationsPath, "locations.json", errors);
|
|
233
|
+
if (locations && (locations.schema_version !== 2 || locations.workflow !== "learning" || !Array.isArray(locations.entries))) errors.push("locations.json: expected schema v2");
|
|
234
|
+
else if (locations) {
|
|
235
|
+
const locationIds = locations.entries.map((entry) => entry.change_id);
|
|
236
|
+
if (duplicates(locationIds).length) errors.push("locations.json: duplicate change ids");
|
|
237
|
+
for (const entry of all) if (!locations.entries.some((candidate) => candidate.change_id === entry.change_id && candidate.locator === entry.locator)) errors.push(`${entry.change_id}: locations.json does not match current locator`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (opts.change && !all.some((entry) => entry.change_id === opts.change)) errors.push(`${opts.change}: Change is not indexed`);
|
|
330
241
|
validateContext(root, errors);
|
|
331
242
|
}
|
|
332
243
|
|
|
333
244
|
function main() {
|
|
334
|
-
const
|
|
245
|
+
const opts = options(process.argv.slice(2));
|
|
335
246
|
const errors = [];
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
}
|
|
247
|
+
if (opts.selfCheck) validateWorkflowRoot(resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."), errors);
|
|
248
|
+
if (opts.workflowRoot) validateWorkflowRoot(opts.workflowRoot, errors);
|
|
249
|
+
if (opts.stateRoot) validateStateRoot(opts.stateRoot, opts, errors);
|
|
250
|
+
if (errors.length) { console.error(`Learning validation failed (${errors.length})`); for (const error of errors) console.error(` - ${error}`); process.exitCode = 1; return; }
|
|
348
251
|
console.log("Learning validation: OK");
|
|
349
252
|
}
|
|
350
253
|
|
|
351
|
-
try {
|
|
352
|
-
main();
|
|
353
|
-
} catch (error) {
|
|
354
|
-
console.error(`Learning validation failed: ${error.message}`);
|
|
355
|
-
process.exitCode = 1;
|
|
356
|
-
}
|
|
254
|
+
try { main(); } catch (error) { console.error(`Learning validation failed: ${error.message}`); process.exitCode = 1; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"schema_version":1,"id":"learning","version":"1.0.0","persistent_knowledge":["<Path>{roots.state}/learning/context/INDEX.md</Path>","<Path>{roots.state}/learning/context/REVIEW.md</Path>"],"stages":[{"id":"intake","after":[],"inputs":["goal"],"outputs":["course"],"risk":"read-only","context_budget":12000},{"id":"teach","after":["intake"],"inputs":["course"],"outputs":["lesson"],"risk":"read-only","context_budget":18000},{"id":"practice","after":["teach"],"inputs":["lesson"],"outputs":["homework"],"risk":"read-only","context_budget":12000},{"id":"review","after":["practice"],"inputs":["homework"],"outputs":["mastery"],"risk":"read-only","context_budget":12000},{"id":"consolidate","after":["review"],"inputs":["lesson","mastery"],"outputs":["knowledge"],"risk":"local-reversible","context_budget":14000}]}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
"config": null,
|
|
5
5
|
"structured_state": [
|
|
6
6
|
".speculo/learning/status.json",
|
|
7
|
-
".speculo/learning/
|
|
8
|
-
".speculo/learning/
|
|
7
|
+
".speculo/learning/locations.json",
|
|
8
|
+
".speculo/learning/changes/**/.status.json",
|
|
9
|
+
".speculo/learning/archive/**/.status.json"
|
|
9
10
|
],
|
|
10
11
|
"opaque_default": "preserve-byte-for-byte"
|
|
11
12
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"schema_version":1,"id":"ops","version":"1.0.0","persistent_knowledge":["<Path>{roots.state}/ops/context/</Path>","<Path>{roots.state}/ops/adr/</Path>","<Path>{roots.state}/ops/runbooks/</Path>","<Path>{roots.state}/ops/projects/{project_id}/context/</Path>","<Path>{roots.state}/ops/projects/{project_id}/adr/</Path>","<Path>{roots.state}/ops/projects/{project_id}/runbooks/</Path>"],"stages":[{"id":"intake","after":[],"inputs":["request","target"],"outputs":["dossier"],"risk":"read-only","context_budget":16000},{"id":"plan","after":["intake"],"inputs":["dossier"],"outputs":["plan","approval"],"risk":"external-mutation","context_budget":22000},{"id":"execute","after":["plan"],"inputs":["plan","approval"],"outputs":["attempt"],"risk":"production-critical","context_budget":20000},{"id":"verify","after":["execute"],"inputs":["attempt"],"outputs":["verification"],"risk":"read-only","context_budget":16000},{"id":"learn","after":["verify"],"inputs":["verification"],"outputs":["retrospective"],"risk":"local-reversible","context_budget":12000}]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"schema_version":1,"id":"person","version":"1.0.0","persistent_knowledge":[],"stages":[{"id":"frame","after":[],"inputs":["question"],"outputs":["dossier"],"risk":"read-only","context_budget":14000},{"id":"deliberate","after":["frame"],"inputs":["dossier"],"outputs":["decision"],"risk":"read-only","context_budget":18000},{"id":"act","after":["deliberate"],"inputs":["decision"],"outputs":["actions"],"risk":"local-reversible","context_budget":10000}]}
|