@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,41 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "urn:speculo:learning:change-status:
|
|
4
|
-
"title": "Learning Change Status",
|
|
3
|
+
"$id": "urn:speculo:learning:change-status:v2",
|
|
4
|
+
"title": "Learning Change Status v2",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"required": ["schema_version", "artifact", "
|
|
6
|
+
"required": ["schema_version", "artifact", "change_id", "kind", "domain", "domain_type", "topic_id", "parent_change", "root_change", "locator", "lifecycle", "phase", "current_work", "works_run", "created_at", "updated_at", "closed_at", "archived_at", "closure_reason", "archive_path", "homework", "mastery", "children", "blockers"],
|
|
7
7
|
"properties": {
|
|
8
|
-
"schema_version": {"const":
|
|
8
|
+
"schema_version": {"const": 2},
|
|
9
9
|
"artifact": {"const": "learning-change-status"},
|
|
10
|
-
"
|
|
10
|
+
"change_id": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[a-z0-9]+(?:-[a-z0-9]+)*$"},
|
|
11
|
+
"kind": {"enum": ["learning", "consolidation"]},
|
|
11
12
|
"domain": {"type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"},
|
|
12
13
|
"domain_type": {"enum": ["project", "product", "subject", "language", "skill"]},
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
14
|
+
"topic_id": {"type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"},
|
|
15
|
+
"parent_change": {"type": ["string", "null"]},
|
|
16
|
+
"root_change": {"type": "string"},
|
|
17
|
+
"locator": {"type": "string", "pattern": "^(?:changes/.+|archive/[0-9]{4}-[0-9]{2}/.+)$"},
|
|
18
|
+
"lifecycle": {"enum": ["active", "blocked", "closed", "archived"]},
|
|
19
|
+
"phase": {"enum": ["planning", "teaching", "homework", "review", "consolidating", "closed", "archived"]},
|
|
16
20
|
"current_work": {"type": ["string", "null"], "pattern": "^learning/"},
|
|
17
21
|
"works_run": {"type": "array", "items": {"type": "string", "pattern": "^learning/"}, "uniqueItems": true},
|
|
18
22
|
"created_at": {"type": "string", "format": "date-time"},
|
|
19
23
|
"updated_at": {"type": "string", "format": "date-time"},
|
|
20
|
-
"
|
|
24
|
+
"closed_at": {"type": ["string", "null"], "format": "date-time"},
|
|
21
25
|
"archived_at": {"type": ["string", "null"], "format": "date-time"},
|
|
22
|
-
"
|
|
26
|
+
"closure_reason": {"type": ["string", "null"]},
|
|
27
|
+
"archive_path": {"type": ["string", "null"]},
|
|
28
|
+
"homework": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"required": ["status", "latest_id", "submitted_at"],
|
|
31
|
+
"properties": {
|
|
32
|
+
"status": {"enum": ["none", "pending", "reviewed", "needs_revision"]},
|
|
33
|
+
"latest_id": {"type": ["string", "null"]},
|
|
34
|
+
"submitted_at": {"type": ["string", "null"], "format": "date-time"}
|
|
35
|
+
},
|
|
36
|
+
"additionalProperties": false
|
|
37
|
+
},
|
|
23
38
|
"mastery": {
|
|
24
39
|
"type": "object",
|
|
25
|
-
"required": ["
|
|
40
|
+
"required": ["overall", "immediate", "retention", "critical_objectives", "transfer", "blocking_misconceptions", "evidence", "next_review_at"],
|
|
26
41
|
"properties": {
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
42
|
+
"overall": {"enum": ["unverified", "immediate", "retention_verified"]},
|
|
43
|
+
"immediate": {"enum": ["not_attempted", "passed", "needs_review"]},
|
|
44
|
+
"retention": {"enum": ["not_scheduled", "due", "passed", "needs_review"]},
|
|
45
|
+
"critical_objectives": {"enum": ["not_attempted", "passed", "needs_review"]},
|
|
46
|
+
"transfer": {"enum": ["not_attempted", "passed", "needs_review"]},
|
|
32
47
|
"blocking_misconceptions": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
|
33
|
-
"evidence": {"type": "array", "items": {"type": "string"
|
|
48
|
+
"evidence": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
|
34
49
|
"next_review_at": {"type": ["string", "null"], "format": "date-time"}
|
|
35
50
|
},
|
|
36
51
|
"additionalProperties": false
|
|
37
52
|
},
|
|
38
|
-
"
|
|
53
|
+
"children": {"type": "array", "items": {"$ref": "#/$defs/child"}, "uniqueItems": true},
|
|
54
|
+
"blockers": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
|
55
|
+
"child": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"required": ["change_id", "locator", "content_hash", "effective_at", "lifecycle"],
|
|
58
|
+
"properties": {
|
|
59
|
+
"change_id": {"type": "string"},
|
|
60
|
+
"locator": {"type": "string"},
|
|
61
|
+
"content_hash": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
62
|
+
"effective_at": {"type": "string", "format": "date-time"},
|
|
63
|
+
"lifecycle": {"type": "string"}
|
|
64
|
+
},
|
|
65
|
+
"additionalProperties": false
|
|
66
|
+
}
|
|
39
67
|
},
|
|
40
68
|
"additionalProperties": false
|
|
41
69
|
}
|
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "urn:speculo:learning:status:
|
|
4
|
-
"title": "Learning Global Status",
|
|
3
|
+
"$id": "urn:speculo:learning:status:v2",
|
|
4
|
+
"title": "Learning Global Status v2",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": ["schema_version", "workflow", "active", "archived"],
|
|
7
7
|
"properties": {
|
|
8
|
-
"schema_version": {"const":
|
|
8
|
+
"schema_version": {"const": 2},
|
|
9
9
|
"workflow": {"const": "learning"},
|
|
10
|
-
"active": {
|
|
11
|
-
|
|
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
|
-
}
|
|
10
|
+
"active": {"type": "array", "items": {"$ref": "#/$defs/entry"}},
|
|
11
|
+
"archived": {"type": "array", "items": {"$ref": "#/$defs/entry"}}
|
|
30
12
|
},
|
|
31
|
-
"additionalProperties": false
|
|
13
|
+
"additionalProperties": false,
|
|
14
|
+
"$defs": {
|
|
15
|
+
"entry": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"required": ["change_id", "kind", "domain", "topic_id", "locator", "parent_change", "root_change", "current_work", "works_run"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"change_id": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(?:-[a-z0-9]+)*$"},
|
|
20
|
+
"kind": {"enum": ["learning", "consolidation"]},
|
|
21
|
+
"domain": {"type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"},
|
|
22
|
+
"topic_id": {"type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"},
|
|
23
|
+
"locator": {"type": "string", "pattern": "^(?:changes/.+|archive/[0-9]{4}-[0-9]{2}/.+)$"},
|
|
24
|
+
"parent_change": {"type": ["string", "null"]},
|
|
25
|
+
"root_change": {"type": "string"},
|
|
26
|
+
"current_work": {"type": ["string", "null"], "pattern": "^learning/"},
|
|
27
|
+
"works_run": {"type": "array", "items": {"type": "string", "pattern": "^learning/"}, "uniqueItems": true},
|
|
28
|
+
"updated_at": {"type": "string", "format": "date-time"}
|
|
29
|
+
},
|
|
30
|
+
"additionalProperties": false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
32
33
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: topic-synthesis
|
|
3
|
+
description: 将 Learning Change 作为不可变原料,生成带 claim 级 provenance 的主题综合并显式发布。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Topic Synthesis
|
|
7
|
+
|
|
8
|
+
1. 只接收 C-consolidate 已确认的 source manifest;按稳定 Change ID 读取当前 locator,禁止依赖旧路径猜测。
|
|
9
|
+
2. 对每个 claim 记录 source Change、Lesson/Homework/Review anchor、外部 source id、证据状态、冲突、空白和验证时间;不要把类比或索引摘要当成事实来源。
|
|
10
|
+
3. 生成新的 `synthesis/revisions/<version>.md`,旧版本只读保留。综合可以包含未掌握证据,但不得写 `mastered`,除非 R evidence 已存在。
|
|
11
|
+
4. 发布 context 前必须再次获得用户确认;发布只更新 topic INDEX/current view 和 provenance,不删除或改写任何 child 原料。
|
|
12
|
+
|
|
13
|
+
## 引用模板
|
|
14
|
+
|
|
15
|
+
| claim_id | 陈述 | evidence_status | source_change_id | artifact anchor | external source | verified_at |
|
|
16
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
17
|
+
| CLM-001 | `<claim>` | supported | `<change-id>` | `L-001#heading` | `S-001` | `<ISO-8601>` |
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Claim <CLM-NNN>
|
|
2
|
+
|
|
3
|
+
## 陈述
|
|
4
|
+
|
|
5
|
+
## Evidence status
|
|
6
|
+
|
|
7
|
+
`draft | supported | contested | unresolved`
|
|
8
|
+
|
|
9
|
+
## 来源与定位
|
|
10
|
+
|
|
11
|
+
- Change ID / current locator:
|
|
12
|
+
- Lesson/Homework/Review anchor:
|
|
13
|
+
- External source ID / URL / access date:
|
|
14
|
+
|
|
15
|
+
## 冲突、空白与下一次验证
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
|
|
8
|
+
const CHANGE_ID = /^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
9
|
+
|
|
10
|
+
function args(argv) {
|
|
11
|
+
const result = { stateRoot: null, parent: null, sources: [], apply: false, rollback: null };
|
|
12
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
13
|
+
const arg = argv[index];
|
|
14
|
+
if (arg === "--state-root") result.stateRoot = resolve(argv[++index] ?? "");
|
|
15
|
+
else if (arg === "--parent") result.parent = argv[++index] ?? null;
|
|
16
|
+
else if (arg === "--source") result.sources.push(argv[++index] ?? "");
|
|
17
|
+
else if (arg === "--apply") result.apply = true;
|
|
18
|
+
else if (arg === "--rollback") result.rollback = resolve(argv[++index] ?? "");
|
|
19
|
+
else throw new Error(`unknown option: ${arg}`);
|
|
20
|
+
}
|
|
21
|
+
if (!result.stateRoot) throw new Error("--state-root is required");
|
|
22
|
+
if (result.rollback) return result;
|
|
23
|
+
if (!result.parent || !CHANGE_ID.test(result.parent) || result.sources.length === 0) {
|
|
24
|
+
throw new Error("--parent and at least one --source are required");
|
|
25
|
+
}
|
|
26
|
+
if (result.sources.some((source) => !CHANGE_ID.test(source)) || new Set(result.sources).size !== result.sources.length) {
|
|
27
|
+
throw new Error("source ids must be unique valid Change IDs");
|
|
28
|
+
}
|
|
29
|
+
if (result.sources.includes(result.parent)) throw new Error("a parent cannot be one of its own sources");
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function json(path, fallback) {
|
|
34
|
+
if (!existsSync(path)) return fallback;
|
|
35
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function files(root) {
|
|
39
|
+
const result = [];
|
|
40
|
+
async function visit(current) {
|
|
41
|
+
const entries = await (await import("node:fs/promises")).readdir(current, { withFileTypes: true });
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const path = join(current, entry.name);
|
|
44
|
+
if (entry.isDirectory()) await visit(path);
|
|
45
|
+
else if (entry.isFile()) result.push(path);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
await visit(root);
|
|
49
|
+
return result.sort();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function contentHash(root) {
|
|
53
|
+
const hash = createHash("sha256");
|
|
54
|
+
for (const path of await files(root)) {
|
|
55
|
+
const rel = relative(root, path).split(sep).join("/");
|
|
56
|
+
if (rel === ".status.json") continue;
|
|
57
|
+
hash.update(rel);
|
|
58
|
+
hash.update(await readFile(path));
|
|
59
|
+
}
|
|
60
|
+
return hash.digest("hex");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function loadLocations(stateRoot) {
|
|
64
|
+
const path = join(stateRoot, "locations.json");
|
|
65
|
+
const value = await json(path, { schema_version: 2, workflow: "learning", entries: [] });
|
|
66
|
+
if (value.schema_version !== 2 || value.workflow !== "learning" || !Array.isArray(value.entries)) {
|
|
67
|
+
throw new Error("locations.json must use Learning schema v2");
|
|
68
|
+
}
|
|
69
|
+
return { path, value };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function locatorFor(entry, stateRoot) {
|
|
73
|
+
const locator = entry?.locator;
|
|
74
|
+
if (typeof locator === "string" && (locator.startsWith("changes/") || locator.startsWith("archive/"))) return locator;
|
|
75
|
+
return `changes/${entry.change_id}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function updateLocation(value, id, from, to, hash, now, parent) {
|
|
79
|
+
const existing = value.entries.find((entry) => entry.change_id === id);
|
|
80
|
+
if (existing) {
|
|
81
|
+
existing.previous = Array.isArray(existing.previous) ? existing.previous : [];
|
|
82
|
+
existing.previous.push({ locator: from, moved_at: now, reason: "consolidate", content_hash: hash });
|
|
83
|
+
existing.locator = to;
|
|
84
|
+
existing.parent_change = parent;
|
|
85
|
+
existing.root_change = value.entries.find((entry) => entry.change_id === parent)?.root_change ?? parent;
|
|
86
|
+
existing.content_hash = hash;
|
|
87
|
+
existing.updated_at = now;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
value.entries.push({
|
|
91
|
+
change_id: id,
|
|
92
|
+
locator: to,
|
|
93
|
+
parent_change: parent,
|
|
94
|
+
root_change: parent,
|
|
95
|
+
content_hash: hash,
|
|
96
|
+
previous: [{ locator: from, moved_at: now, reason: "consolidate", content_hash: hash }],
|
|
97
|
+
updated_at: now,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function updateStatus(path, id, to, parent, now) {
|
|
102
|
+
const statusPath = join(path, ".status.json");
|
|
103
|
+
if (!existsSync(statusPath)) return;
|
|
104
|
+
const value = await json(statusPath, null);
|
|
105
|
+
if (!value || value.schema_version !== 2 || value.change_id !== id) return;
|
|
106
|
+
value.parent_change = parent;
|
|
107
|
+
value.root_change = value.root_change === id ? parent : value.root_change;
|
|
108
|
+
value.locator = to;
|
|
109
|
+
value.updated_at = now;
|
|
110
|
+
await writeFile(statusPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function updateGlobalStatus(stateRoot, ids, parent, now) {
|
|
114
|
+
const path = join(stateRoot, "status.json");
|
|
115
|
+
const value = await json(path, null);
|
|
116
|
+
if (!value || value.schema_version !== 2) return;
|
|
117
|
+
const root = value.active.find((entry) => entry.change_id === parent)?.root_change ?? parent;
|
|
118
|
+
for (const entry of [...value.active, ...value.archived]) {
|
|
119
|
+
if (!ids.includes(entry.change_id)) continue;
|
|
120
|
+
entry.parent_change = parent;
|
|
121
|
+
entry.root_change = root;
|
|
122
|
+
entry.locator = entry.locator;
|
|
123
|
+
entry.updated_at = now;
|
|
124
|
+
}
|
|
125
|
+
await writeFile(path, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function rollbackManifest(manifestPath) {
|
|
129
|
+
const manifest = await json(manifestPath, null);
|
|
130
|
+
if (!manifest || !Array.isArray(manifest.moves)) throw new Error("invalid relocation manifest");
|
|
131
|
+
for (const move of [...manifest.moves].reverse()) {
|
|
132
|
+
if (existsSync(move.to) && !existsSync(move.from)) await rename(move.to, move.from);
|
|
133
|
+
}
|
|
134
|
+
await writeFile(manifestPath, JSON.stringify({ ...manifest, rolled_back_at: new Date().toISOString() }, null, 2) + "\n", "utf8");
|
|
135
|
+
console.log(JSON.stringify({ status: "rolled-back", manifest: manifestPath }));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function main() {
|
|
139
|
+
const options = args(process.argv.slice(2));
|
|
140
|
+
if (options.rollback) {
|
|
141
|
+
await rollbackManifest(options.rollback);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const changesRoot = join(options.stateRoot, "changes");
|
|
145
|
+
const parentPath = join(changesRoot, options.parent);
|
|
146
|
+
if (!existsSync(parentPath)) throw new Error(`parent Change does not exist: ${options.parent}`);
|
|
147
|
+
const { path: locationsPath, value: locations } = await loadLocations(options.stateRoot);
|
|
148
|
+
const moves = [];
|
|
149
|
+
for (const id of options.sources) {
|
|
150
|
+
const entry = locations.entries.find((candidate) => candidate.change_id === id);
|
|
151
|
+
const fromLocator = locatorFor(entry, options.stateRoot);
|
|
152
|
+
if (!fromLocator.startsWith("changes/")) throw new Error(`source is not an active/closed Change: ${id}`);
|
|
153
|
+
const from = join(options.stateRoot, ...fromLocator.split("/"));
|
|
154
|
+
if (!existsSync(from)) throw new Error(`source Change is missing: ${fromLocator}`);
|
|
155
|
+
const relativeFrom = fromLocator.split("/");
|
|
156
|
+
if (relativeFrom.slice(0, 2).join("/") === `changes/${options.parent}` || from === parentPath || parentPath.startsWith(from + sep)) {
|
|
157
|
+
throw new Error(`source/parent cycle detected for ${id}`);
|
|
158
|
+
}
|
|
159
|
+
const to = join(parentPath, "children", id);
|
|
160
|
+
if (existsSync(to)) throw new Error(`target child already exists: ${id}`);
|
|
161
|
+
moves.push({ id, from, to, from_locator: fromLocator, to_locator: `changes/${options.parent}/children/${id}` });
|
|
162
|
+
}
|
|
163
|
+
const hashes = [];
|
|
164
|
+
for (const move of moves) hashes.push({ id: move.id, content_hash: await contentHash(move.from) });
|
|
165
|
+
const now = new Date().toISOString();
|
|
166
|
+
const manifest = {
|
|
167
|
+
schema_version: 1,
|
|
168
|
+
artifact: "learning-relocation-manifest",
|
|
169
|
+
parent_change: options.parent,
|
|
170
|
+
created_at: now,
|
|
171
|
+
moves: moves.map((move) => ({ ...move, content_hash: hashes.find((item) => item.id === move.id).content_hash })),
|
|
172
|
+
};
|
|
173
|
+
if (!options.apply) {
|
|
174
|
+
console.log(JSON.stringify({ status: "dry-run", ...manifest }, null, 2));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await mkdir(join(parentPath, "children"), { recursive: true });
|
|
179
|
+
const lockPath = join(options.stateRoot, ".consolidate.lock");
|
|
180
|
+
let lock;
|
|
181
|
+
try {
|
|
182
|
+
lock = await open(lockPath, "wx");
|
|
183
|
+
for (const move of moves) await rename(move.from, move.to);
|
|
184
|
+
for (const move of manifest.moves) {
|
|
185
|
+
updateLocation(locations, move.id, move.from_locator, move.to_locator, move.content_hash, now, options.parent);
|
|
186
|
+
await updateStatus(join(options.stateRoot, ...move.to_locator.split("/")), move.id, move.to_locator, options.parent, now);
|
|
187
|
+
}
|
|
188
|
+
await writeFile(locationsPath, JSON.stringify(locations, null, 2) + "\n", "utf8");
|
|
189
|
+
await updateGlobalStatus(options.stateRoot, options.sources, options.parent, now);
|
|
190
|
+
const manifestPath = join(parentPath, "synthesis", `relocation-${now.replaceAll(/[^0-9]/g, "").slice(0, 14)}.json`);
|
|
191
|
+
await mkdir(dirname(manifestPath), { recursive: true });
|
|
192
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
193
|
+
console.log(JSON.stringify({ status: "applied", manifest: manifestPath, moves: manifest.moves }, null, 2));
|
|
194
|
+
} catch (error) {
|
|
195
|
+
for (const move of [...moves].reverse()) {
|
|
196
|
+
if (existsSync(move.to) && !existsSync(move.from)) await rename(move.to, move.from);
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
} finally {
|
|
200
|
+
if (lock) await lock.close();
|
|
201
|
+
await rm(lockPath, { force: true });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
main().catch((error) => {
|
|
206
|
+
console.error(`Learning relocation failed: ${error.message}`);
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
});
|