@haaaiawd/loom 1.2.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -58
- package/CONTRIBUTING.md +37 -0
- package/EVIL_EVAL.md +112 -0
- package/README.md +193 -446
- package/README.zh-CN.md +174 -0
- package/SECURITY.md +11 -0
- package/cli/bin/loom.js +170 -995
- package/cli/src/protocol.js +367 -0
- package/cli/src/store.js +626 -0
- package/design.md +194 -0
- package/docs/PROMPT_CATALOG.md +99 -0
- package/docs/RELEASE_CHECKLIST.md +53 -0
- package/docs/UX_FLOW.md +171 -0
- package/docs/brand/loom-mark.svg +18 -0
- package/docs/brand/loom-readme-header.svg +34 -0
- package/docs/brand/loom-readme-header.zh-CN.svg +29 -0
- package/docs/loom-eval-loop.drawio +21 -0
- package/docs/loom-eval-loop.svg +56 -0
- package/docs/loom-production-loop.drawio +41 -0
- package/docs/loom-production-loop.svg +92 -0
- package/package.json +43 -40
- package/EXTERNAL_ACQUISITION_DESIGN.md +0 -143
- package/cli/help/asset.md +0 -36
- package/cli/help/atelier.md +0 -37
- package/cli/help/capability.md +0 -73
- package/cli/help/concepts.md +0 -103
- package/cli/help/doctor.md +0 -73
- package/cli/help/expertise.md +0 -51
- package/cli/help/loop.md +0 -134
- package/cli/help/patch.md +0 -33
- package/cli/help/preview.md +0 -60
- package/cli/help/proposals.md +0 -21
- package/cli/help/version.md +0 -136
- package/cli/help/workflow.md +0 -114
- package/cli/src/activate.js +0 -473
- package/cli/src/asset-library.js +0 -384
- package/cli/src/atelier.js +0 -331
- package/cli/src/auto.js +0 -116
- package/cli/src/capability-graph.js +0 -430
- package/cli/src/capability-proposals.js +0 -225
- package/cli/src/diagnostics.js +0 -766
- package/cli/src/expertise-pack.js +0 -336
- package/cli/src/guide.js +0 -495
- package/cli/src/help.js +0 -41
- package/cli/src/init.js +0 -187
- package/cli/src/intent-draft.js +0 -303
- package/cli/src/intent-map.js +0 -747
- package/cli/src/patch.js +0 -214
- package/cli/src/philosophy.js +0 -331
- package/cli/src/preview-prompt.md +0 -337
- package/cli/src/preview.js +0 -73
- package/cli/src/shared/intent-ref.js +0 -38
- package/cli/src/shared/md-utils.js +0 -125
- package/cli/src/shared/paths.js +0 -73
- package/cli/src/shared/proof-reference.js +0 -19
- package/cli/src/shared/verification-method.js +0 -32
- package/cli/src/verify.js +0 -394
- package/cli/src/version.js +0 -134
- package/dimensions/AUTHORSHIP.md +0 -45
- package/dimensions/PART_DECOMPOSITION.md +0 -42
- package/dimensions/SEARCH_METHODOLOGY.md +0 -101
- package/dimensions/examples/AGENT_SYSTEM/README.md +0 -219
- package/dimensions/examples/CLI_TOOL/README.md +0 -163
- package/dimensions/universal/COLLABORATION_PHILOSOPHY.md +0 -28
- package/dimensions/universal/ENGINEERING_CREED.md +0 -30
- package/dimensions/universal/PRODUCT_PHILOSOPHY.md +0 -32
- package/meta/BASELINE.md +0 -91
- package/meta/INTENT_LOOP.md +0 -296
- package/meta/PHILOSOPHY_WEAVER.md +0 -110
- package/meta/ROLE_ACTIVATION.md +0 -114
- package/roles/architect.md +0 -86
- package/roles/forge.md +0 -112
- package/roles/keeper.md +0 -113
- package/roles/visionary.md +0 -57
- package/templates/ASSET_LIBRARY_MANIFEST_TEMPLATE.json +0 -10
- package/templates/ATELIER_RECORD_TEMPLATE.json +0 -48
- package/templates/CAPABILITY_BRIEF_TEMPLATE.md +0 -35
- package/templates/CAPABILITY_GRAPH_TEMPLATE.json +0 -11
- package/templates/EXPERTISE_PACK_TEMPLATE.json +0 -22
- package/templates/INTENT_MAP_TEMPLATE.json +0 -85
- package/templates/PHILOSOPHY_TEMPLATE.md +0 -44
- package/templates/VISION_TEMPLATE.md +0 -44
package/cli/src/init.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
// init — 初始化 LOOM 项目目录结构
|
|
2
|
-
// 创建 .loom/v1/ 骨架 + 复制模板文件
|
|
3
|
-
|
|
4
|
-
import { mkdirSync, existsSync, copyFileSync, readdirSync, statSync, writeFileSync, readFileSync } from 'node:fs';
|
|
5
|
-
import { join, dirname, resolve } from 'node:path';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { getLoomRoot } from './shared/paths.js';
|
|
8
|
-
import { scaffoldChangelog } from './patch.js';
|
|
9
|
-
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 递归复制目录
|
|
14
|
-
*/
|
|
15
|
-
function copyDir(src, dst) {
|
|
16
|
-
if (!existsSync(src)) return;
|
|
17
|
-
mkdirSync(dst, { recursive: true });
|
|
18
|
-
for (const entry of readdirSync(src)) {
|
|
19
|
-
const srcPath = join(src, entry);
|
|
20
|
-
const dstPath = join(dst, entry);
|
|
21
|
-
if (statSync(srcPath).isDirectory()) {
|
|
22
|
-
copyDir(srcPath, dstPath);
|
|
23
|
-
} else {
|
|
24
|
-
copyFileSync(srcPath, dstPath);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 创建某个版本的目录结构 + 复制模板。
|
|
31
|
-
* 共享给 init(v1)和 version new(v{N+1})。
|
|
32
|
-
* @param {string} projectDir — 项目根目录
|
|
33
|
-
* @param {string|number} version — 版本号,如 'v1' 或 'v2' 或 1
|
|
34
|
-
* @param {string|null} parentVersion — lineage parent; null for v1
|
|
35
|
-
* @returns {{ created: string[], skipped: string[] }}
|
|
36
|
-
*/
|
|
37
|
-
export function createVersionStructure(projectDir, version, parentVersion = null) {
|
|
38
|
-
const v = typeof version === 'number' ? `v${version}` : version;
|
|
39
|
-
const cwd = projectDir || process.cwd();
|
|
40
|
-
const loomRoot = getLoomRoot();
|
|
41
|
-
|
|
42
|
-
const created = [];
|
|
43
|
-
const skipped = [];
|
|
44
|
-
|
|
45
|
-
const dirs = [
|
|
46
|
-
'.loom',
|
|
47
|
-
`.loom/${v}`,
|
|
48
|
-
`.loom/${v}/00_PHILOSOPHY`,
|
|
49
|
-
`.loom/${v}/verifications`,
|
|
50
|
-
`.loom/${v}/03_DECISIONS`,
|
|
51
|
-
`.loom/${v}/07_CAPABILITY_BRIEFS`,
|
|
52
|
-
`.loom/${v}/07_GRAPH_PROPOSALS`,
|
|
53
|
-
`.loom/${v}/08_ASSET_LIBRARY/files`,
|
|
54
|
-
`.loom/${v}/10_EXPERTISE_PACKS`,
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
for (const d of dirs) {
|
|
58
|
-
const path = join(cwd, d);
|
|
59
|
-
if (existsSync(path)) {
|
|
60
|
-
skipped.push(d);
|
|
61
|
-
} else {
|
|
62
|
-
mkdirSync(path, { recursive: true });
|
|
63
|
-
created.push(d);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const templates = [
|
|
68
|
-
['templates/INTENT_MAP_TEMPLATE.json', `.loom/${v}/04_INTENT_MAP.json`],
|
|
69
|
-
['templates/PHILOSOPHY_TEMPLATE.md', `.loom/${v}/00_PHILOSOPHY/PRODUCT_PHILOSOPHY.md`],
|
|
70
|
-
['templates/VISION_TEMPLATE.md', `.loom/${v}/01_VISION.md`],
|
|
71
|
-
['templates/CAPABILITY_GRAPH_TEMPLATE.json', `.loom/${v}/07_CAPABILITY_GRAPH.json`],
|
|
72
|
-
['templates/ASSET_LIBRARY_MANIFEST_TEMPLATE.json', `.loom/${v}/08_ASSET_LIBRARY/manifest.json`],
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
// 02_ARCHITECTURE.md 和 05_VERIFICATION.md 没有"填空模板"——
|
|
76
|
-
// 它们由 Architect 自由设计。但 init 时 scaffold 空文件 + LOOM_TEMPLATE 标记,
|
|
77
|
-
// 让 guide 能检测到"还没设计",且 Agent 知道该往哪写。
|
|
78
|
-
const scaffoldFiles = [
|
|
79
|
-
[`.loom/${v}/02_ARCHITECTURE.md`, [
|
|
80
|
-
'<!-- LOOM_TEMPLATE -->',
|
|
81
|
-
`# 02_ARCHITECTURE.md — 系统边界与责任`,
|
|
82
|
-
'',
|
|
83
|
-
`> 版本: ${v}`,
|
|
84
|
-
'> 产出自: Architect 角色激活',
|
|
85
|
-
'',
|
|
86
|
-
'Architect 设计系统边界后替换本文件。',
|
|
87
|
-
'内容要求:责任边界、公开契约、数据与依赖方向、失败与演进边界;目录只在影响理解时记录。',
|
|
88
|
-
'设计完成后删除顶部的 `<!-- LOOM_TEMPLATE -->` 标记。',
|
|
89
|
-
'',
|
|
90
|
-
].join('\n')],
|
|
91
|
-
[`.loom/${v}/05_VERIFICATION.md`, [
|
|
92
|
-
'<!-- LOOM_TEMPLATE -->',
|
|
93
|
-
`# 05_VERIFICATION.md — 完成与质量契约`,
|
|
94
|
-
'',
|
|
95
|
-
`> 版本: ${v}`,
|
|
96
|
-
'> 产出自: Architect 角色激活',
|
|
97
|
-
'',
|
|
98
|
-
'Architect 设计成功契约后替换本文件。',
|
|
99
|
-
'内容要求:每个 Intent 的 Reliability Floor(结果、失败边界;若改动既有状态,还要写保留项与旧状态 → 操作 → 新状态的验证序列);需要高于功能正确性时,再定义 Distinctive Ceiling、基线与证据方式。',
|
|
100
|
-
'Intent Map 的 acceptance 与 quality_contract 可分别引用本文件章节。',
|
|
101
|
-
'设计完成后删除顶部的 `<!-- LOOM_TEMPLATE -->` 标记。',
|
|
102
|
-
'',
|
|
103
|
-
].join('\n')],
|
|
104
|
-
];
|
|
105
|
-
|
|
106
|
-
for (const [dst, content] of scaffoldFiles) {
|
|
107
|
-
const dstPath = join(cwd, dst);
|
|
108
|
-
if (existsSync(dstPath)) {
|
|
109
|
-
skipped.push(dst);
|
|
110
|
-
} else {
|
|
111
|
-
writeFileSync(dstPath, content, 'utf-8');
|
|
112
|
-
created.push(dst);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
for (const [src, dst] of templates) {
|
|
117
|
-
const srcPath = join(loomRoot, src);
|
|
118
|
-
const dstPath = join(cwd, dst);
|
|
119
|
-
if (existsSync(dstPath)) {
|
|
120
|
-
skipped.push(dst);
|
|
121
|
-
} else if (existsSync(srcPath)) {
|
|
122
|
-
copyFileSync(srcPath, dstPath);
|
|
123
|
-
if (dst.endsWith('04_INTENT_MAP.json') || dst.endsWith('07_CAPABILITY_GRAPH.json') || dst.endsWith('08_ASSET_LIBRARY/manifest.json')) {
|
|
124
|
-
const map = JSON.parse(readFileSync(dstPath, 'utf-8'));
|
|
125
|
-
map._meta._loom_version = v;
|
|
126
|
-
map._meta._parent_version = parentVersion;
|
|
127
|
-
writeFileSync(dstPath, JSON.stringify(map, null, 2), 'utf-8');
|
|
128
|
-
}
|
|
129
|
-
created.push(dst);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const changelogFiles = [`.loom/${v}/06_CHANGELOG.json`, `.loom/${v}/06_CHANGELOG.md`];
|
|
134
|
-
const changelogExisted = changelogFiles.map((file) => existsSync(join(cwd, file)));
|
|
135
|
-
scaffoldChangelog(join(cwd, '.loom', v));
|
|
136
|
-
changelogFiles.forEach((file, index) => (changelogExisted[index] ? skipped : created).push(file));
|
|
137
|
-
|
|
138
|
-
return { created, skipped };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* 初始化项目目录(创建 v1 + 写入 current 指针)。
|
|
143
|
-
* @param {string} projectDir — 项目根目录(默认 cwd)
|
|
144
|
-
* @returns {{ created: string[], skipped: string[] }}
|
|
145
|
-
*/
|
|
146
|
-
export function initProject(projectDir) {
|
|
147
|
-
const cwd = projectDir || process.cwd();
|
|
148
|
-
const result = createVersionStructure(cwd, 'v1', null);
|
|
149
|
-
// 写入 current 指针
|
|
150
|
-
const currentPath = join(cwd, '.loom', 'current');
|
|
151
|
-
if (!existsSync(currentPath)) {
|
|
152
|
-
writeFileSync(currentPath, 'v1', 'utf-8');
|
|
153
|
-
result.created.push('.loom/current');
|
|
154
|
-
} else {
|
|
155
|
-
result.skipped.push('.loom/current');
|
|
156
|
-
}
|
|
157
|
-
// 生成极简 AGENTS.md(项目级锚点,让 agent 发现 LOOM)
|
|
158
|
-
const agentsMdPath = join(cwd, 'AGENTS.md');
|
|
159
|
-
if (!existsSync(agentsMdPath)) {
|
|
160
|
-
writeFileSync(agentsMdPath, [
|
|
161
|
-
'# AGENTS.md',
|
|
162
|
-
'',
|
|
163
|
-
'本项目使用 LOOM:按 Doctrine、Intent 与 Contract 对齐方向,再通过',
|
|
164
|
-
'Expertise Compiler、Quality Arena 与 Quality Proof 把任务专业地完成并证明确实成立。',
|
|
165
|
-
'',
|
|
166
|
-
'进入项目后:',
|
|
167
|
-
'1. 运行 `loom guide --dry-run`,确认当前阶段和唯一下一步。',
|
|
168
|
-
'2. 用 `loom activate <role>` 获取当前角色上下文;实现或验证单个 Intent 时必须加 `--intent <id>`。',
|
|
169
|
-
'3. 只在当前角色的权限内行动;发现目标、契约或架构需要改变时,按 LOOM 回流,不要静默扩展范围。',
|
|
170
|
-
'4. 完成前运行当前 Intent 的验证方法与 `loom doctor`;声称质量提升时必须提供基线相对 Quality Proof,以磁盘证据而非会话记忆判断状态。',
|
|
171
|
-
'5. Keeper 验证必须运行在新的 Agent thread 中;同一会话切换角色不构成独立验证。',
|
|
172
|
-
'6. 协作节奏默认是手动;需要让 Agent 在已允许阶段连续推进时,显式运行 `loom auto on`。AUTO 不会跳过契约、证据或 Keeper 门禁。',
|
|
173
|
-
'',
|
|
174
|
-
'常用入口:',
|
|
175
|
-
'- `loom --help`',
|
|
176
|
-
'- `loom help workflow`',
|
|
177
|
-
'- `loom help concepts`',
|
|
178
|
-
'- `loom context`',
|
|
179
|
-
'- `loom doctor`',
|
|
180
|
-
'',
|
|
181
|
-
].join('\n'), 'utf-8');
|
|
182
|
-
result.created.push('AGENTS.md');
|
|
183
|
-
} else {
|
|
184
|
-
result.skipped.push('AGENTS.md');
|
|
185
|
-
}
|
|
186
|
-
return result;
|
|
187
|
-
}
|
package/cli/src/intent-draft.js
DELETED
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
// intent-draft.js — current-version Minor Intent drafts and atomic finalization.
|
|
2
|
-
// Drafts isolate semantic edits from the official map until their references and DAG validate.
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
existsSync,
|
|
6
|
-
mkdirSync,
|
|
7
|
-
readdirSync,
|
|
8
|
-
readFileSync,
|
|
9
|
-
renameSync,
|
|
10
|
-
unlinkSync,
|
|
11
|
-
writeFileSync,
|
|
12
|
-
} from 'node:fs';
|
|
13
|
-
import { basename, join } from 'node:path';
|
|
14
|
-
import { extractExplicitAnchor, extractMdSection, readJsonFile, slugify } from './shared/md-utils.js';
|
|
15
|
-
import { applyImpactReview, computeTopoOrder, dependentClosure, VALID_STATUS, validateImpactPartition, validateIntentMap } from './intent-map.js';
|
|
16
|
-
import { getPhilosophy } from './philosophy.js';
|
|
17
|
-
|
|
18
|
-
const ID_RE = /^INT-(\d{3,})$/;
|
|
19
|
-
const REQUIRED_FIELDS = [
|
|
20
|
-
'id',
|
|
21
|
-
'revision',
|
|
22
|
-
'title',
|
|
23
|
-
'narrative_ref',
|
|
24
|
-
'depends_on',
|
|
25
|
-
'acceptance',
|
|
26
|
-
'philosophy_anchors',
|
|
27
|
-
'status',
|
|
28
|
-
];
|
|
29
|
-
const PLACEHOLDER_RE = /(?:\.{3}|…|\b(?:todo|tbd|placeholder)\b|待填|待写|请填写|在此填写|\[必须\]|replace this)/i;
|
|
30
|
-
|
|
31
|
-
function assertIntentId(id) {
|
|
32
|
-
if (!ID_RE.test(id || '')) throw new Error(`Intent ID 非法: ${id || '(empty)'}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function draftDir(versionDir) {
|
|
36
|
-
return join(versionDir, 'drafts');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function draftPath(versionDir, id) {
|
|
40
|
-
assertIntentId(id);
|
|
41
|
-
return join(draftDir(versionDir), `${id}.json`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function atomicWrite(filePath, content) {
|
|
45
|
-
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
46
|
-
writeFileSync(tempPath, content, 'utf-8');
|
|
47
|
-
try {
|
|
48
|
-
renameSync(tempPath, filePath);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
try { unlinkSync(tempPath); } catch {}
|
|
51
|
-
throw error;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function readOfficialMap(versionDir) {
|
|
56
|
-
return readJsonFile(join(versionDir, '04_INTENT_MAP.json'), 'Intent Map');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function getSection(versionDir, ref, expectedFile, label) {
|
|
60
|
-
if (typeof ref !== 'string') throw new Error(`${label}引用必须是字符串`);
|
|
61
|
-
const match = ref.trim().match(/^(?:see\s+)?([^#]+)#([\w-]+)$/i);
|
|
62
|
-
if (!match || basename(match[1]) !== match[1] || match[1] !== expectedFile) {
|
|
63
|
-
throw new Error(`${label}引用必须是 ${expectedFile}#<section>`);
|
|
64
|
-
}
|
|
65
|
-
const filePath = join(versionDir, expectedFile);
|
|
66
|
-
if (!existsSync(filePath)) throw new Error(`${label}文件不存在: ${filePath}`);
|
|
67
|
-
return extractMdSection(readFileSync(filePath, 'utf-8'), match[2], label);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function meaningfulBody(section) {
|
|
71
|
-
return section
|
|
72
|
-
.split(/\r?\n/)
|
|
73
|
-
.filter((line) => !/^\s*(?:#|<!--|-->|>\s*DRAFT)/i.test(line))
|
|
74
|
-
.join(' ')
|
|
75
|
-
.replace(/\s+/g, ' ')
|
|
76
|
-
.trim();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function validateProse(section, label, minimumLength) {
|
|
80
|
-
const body = meaningfulBody(section);
|
|
81
|
-
if (body.length < minimumLength || PLACEHOLDER_RE.test(body)) {
|
|
82
|
-
throw new Error(`${label}必须包含真实、非占位内容(当前有效内容 ${body.length} 字符)`);
|
|
83
|
-
}
|
|
84
|
-
return section;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function resolveAcceptance(versionDir, acceptance) {
|
|
88
|
-
const isReference = typeof acceptance === 'string' && /^(?:see\s+)?[^#]+#[\w-]+$/i.test(acceptance.trim());
|
|
89
|
-
return isReference
|
|
90
|
-
? getSection(versionDir, acceptance, '05_VERIFICATION.md', '验证契约')
|
|
91
|
-
: String(acceptance || '');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function hasSection(content, sectionId) {
|
|
95
|
-
return content.split(/\r?\n/).some((line) => {
|
|
96
|
-
const match = line.match(/^#{1,6}\s+(.+)$/);
|
|
97
|
-
if (!match) return false;
|
|
98
|
-
return (extractExplicitAnchor(match[1]) || slugify(match[1])) === sectionId;
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function appendDraftSection(filePath, id, title, kind) {
|
|
103
|
-
const anchor = id.toLowerCase();
|
|
104
|
-
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
|
|
105
|
-
if (hasSection(existing, anchor)) throw new Error(`${basename(filePath)} 已存在 ${id} 章节,拒绝重复追加`);
|
|
106
|
-
|
|
107
|
-
const prompt = kind === 'narrative'
|
|
108
|
-
? '[TODO: replace this with the intent narrative: why this capability must exist and what user outcome it protects.]'
|
|
109
|
-
: '[TODO: replace this with concrete, observable acceptance criteria: result, failure and boundary behavior; if existing user or system state is changed, also name what must be preserved and one old-state → operation → new-state verification sequence.]';
|
|
110
|
-
const heading = kind === 'narrative' ? `${id}: ${title}` : `${id}: Acceptance`;
|
|
111
|
-
const section = `\n\n## [DRAFT] ${heading} {#${anchor}}\n\n<!-- LOOM INTENT DRAFT: ${id} -->\n${prompt}\n`;
|
|
112
|
-
atomicWrite(filePath, existing.replace(/\s*$/, '') + section);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function allocateId(map, versionDir) {
|
|
116
|
-
const numbers = Object.keys(map.intents).map((id) => Number(id.match(ID_RE)?.[1] || 0));
|
|
117
|
-
if (existsSync(draftDir(versionDir))) {
|
|
118
|
-
// Draft IDs are discovered without trusting arbitrary filenames as paths.
|
|
119
|
-
for (const file of readdirSync(draftDir(versionDir))) {
|
|
120
|
-
const match = file.match(/^INT-(\d{3,})\.json$/);
|
|
121
|
-
if (match) numbers.push(Number(match[1]));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return `INT-${String(Math.max(0, ...numbers) + 1).padStart(3, '0')}`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function addIntentDraft(versionDir, title, dependencies = []) {
|
|
128
|
-
if (!title || typeof title !== 'string' || PLACEHOLDER_RE.test(title) || title.trim().length < 2) {
|
|
129
|
-
throw new Error('--title 必须是非占位标题');
|
|
130
|
-
}
|
|
131
|
-
const map = readOfficialMap(versionDir);
|
|
132
|
-
validateIntentMap(map);
|
|
133
|
-
const dependsOn = [...new Set(dependencies.filter(Boolean))];
|
|
134
|
-
for (const dependency of dependsOn) {
|
|
135
|
-
assertIntentId(dependency);
|
|
136
|
-
if (!(dependency in map.intents)) throw new Error(`依赖不存在于官方 Intent Map: ${dependency}`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const id = allocateId(map, versionDir);
|
|
140
|
-
const path = draftPath(versionDir, id);
|
|
141
|
-
if (existsSync(path) || id in map.intents) throw new Error(`Intent 已存在: ${id}`);
|
|
142
|
-
mkdirSync(draftDir(versionDir), { recursive: true });
|
|
143
|
-
|
|
144
|
-
const draft = {
|
|
145
|
-
_draft: { operation: 'add', created_at: new Date().toISOString() },
|
|
146
|
-
id,
|
|
147
|
-
revision: 1,
|
|
148
|
-
title: title.trim(),
|
|
149
|
-
narrative_ref: `01_VISION.md#${id.toLowerCase()}`,
|
|
150
|
-
depends_on: dependsOn,
|
|
151
|
-
acceptance: `see 05_VERIFICATION.md#${id.toLowerCase()}`,
|
|
152
|
-
philosophy_anchors: [],
|
|
153
|
-
status: 'pending',
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
// Check both documents before touching either, then append deterministic draft sections.
|
|
157
|
-
for (const file of ['01_VISION.md', '05_VERIFICATION.md']) {
|
|
158
|
-
const content = existsSync(join(versionDir, file)) ? readFileSync(join(versionDir, file), 'utf-8') : '';
|
|
159
|
-
if (hasSection(content, id.toLowerCase())) {
|
|
160
|
-
throw new Error(`${file} 已存在 ${id} 章节,拒绝重复追加`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
appendDraftSection(join(versionDir, '01_VISION.md'), id, draft.title, 'narrative');
|
|
164
|
-
appendDraftSection(join(versionDir, '05_VERIFICATION.md'), id, draft.title, 'acceptance');
|
|
165
|
-
atomicWrite(path, `${JSON.stringify(draft, null, 2)}\n`);
|
|
166
|
-
return draft;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function getIntentDraft(versionDir, id) {
|
|
170
|
-
const path = draftPath(versionDir, id);
|
|
171
|
-
if (!existsSync(path)) throw new Error(`Intent draft 不存在: ${id}`);
|
|
172
|
-
return readJsonFile(path, 'Intent draft');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function reviseIntentDraft(versionDir, id, reason) {
|
|
176
|
-
assertIntentId(id);
|
|
177
|
-
if (!reason || typeof reason !== 'string' || PLACEHOLDER_RE.test(reason) || reason.trim().length < 3) {
|
|
178
|
-
throw new Error('--reason 必须说明修订原因');
|
|
179
|
-
}
|
|
180
|
-
const path = draftPath(versionDir, id);
|
|
181
|
-
if (existsSync(path)) throw new Error(`${id} 已存在 draft;先完成或删除现有 draft`);
|
|
182
|
-
const map = readOfficialMap(versionDir);
|
|
183
|
-
validateIntentMap(map);
|
|
184
|
-
const current = map.intents[id];
|
|
185
|
-
if (!current) throw new Error(`Intent 不存在: ${id}`);
|
|
186
|
-
|
|
187
|
-
const draft = {
|
|
188
|
-
...current,
|
|
189
|
-
_draft: { operation: 'revise', created_at: new Date().toISOString() },
|
|
190
|
-
revision: (current.revision ?? 1) + 1,
|
|
191
|
-
revision_reason: reason.trim(),
|
|
192
|
-
};
|
|
193
|
-
mkdirSync(draftDir(versionDir), { recursive: true });
|
|
194
|
-
atomicWrite(path, `${JSON.stringify(draft, null, 2)}\n`);
|
|
195
|
-
|
|
196
|
-
const direct = Object.values(map.intents)
|
|
197
|
-
.filter((intent) => intent.depends_on.includes(id))
|
|
198
|
-
.map((intent) => intent.id);
|
|
199
|
-
const seen = new Set();
|
|
200
|
-
const queue = [...direct];
|
|
201
|
-
while (queue.length) {
|
|
202
|
-
const dependency = queue.shift();
|
|
203
|
-
if (seen.has(dependency)) continue;
|
|
204
|
-
seen.add(dependency);
|
|
205
|
-
for (const intent of Object.values(map.intents)) {
|
|
206
|
-
if (intent.depends_on.includes(dependency)) queue.push(intent.id);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return { draft, reverse_dependencies: { direct, transitive: [...seen] } };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function validateDraft(versionDir, draft, map) {
|
|
213
|
-
for (const field of REQUIRED_FIELDS) {
|
|
214
|
-
if (!(field in draft)) throw new Error(`Intent draft 缺少必填字段: ${field}`);
|
|
215
|
-
}
|
|
216
|
-
assertIntentId(draft.id);
|
|
217
|
-
if (!draft._draft || !['add', 'revise'].includes(draft._draft.operation)) throw new Error('Intent draft 缺少合法 _draft.operation');
|
|
218
|
-
if (!Number.isInteger(draft.revision) || draft.revision < 1) throw new Error('Intent draft revision 必须是正整数');
|
|
219
|
-
if (!VALID_STATUS.includes(draft.status)) throw new Error(`Intent draft status 非法: ${draft.status}`);
|
|
220
|
-
if (typeof draft.title !== 'string' || draft.title.trim().length < 2 || PLACEHOLDER_RE.test(draft.title)) throw new Error('Intent draft title 非法');
|
|
221
|
-
if (!Array.isArray(draft.depends_on) || new Set(draft.depends_on).size !== draft.depends_on.length) throw new Error('Intent draft depends_on 必须是不重复数组');
|
|
222
|
-
if (!Array.isArray(draft.philosophy_anchors) || draft.philosophy_anchors.length === 0) throw new Error('Intent draft philosophy_anchors 必须非空');
|
|
223
|
-
if (draft.depends_on.includes(draft.id)) throw new Error(`${draft.id} 不能依赖自身`);
|
|
224
|
-
for (const dependency of draft.depends_on) {
|
|
225
|
-
assertIntentId(dependency);
|
|
226
|
-
if (!(dependency in map.intents)) throw new Error(`依赖不存在于官方 Intent Map: ${dependency}`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
validateProse(getSection(versionDir, draft.narrative_ref, '01_VISION.md', '意图叙事'), '意图叙事', 30);
|
|
230
|
-
validateProse(resolveAcceptance(versionDir, draft.acceptance), '验证契约', 30);
|
|
231
|
-
for (const anchor of draft.philosophy_anchors) {
|
|
232
|
-
if (typeof anchor !== 'string' || basename(anchor.split('#')[0]) !== anchor.split('#')[0]) throw new Error(`哲学锚点非法: ${anchor}`);
|
|
233
|
-
validateProse(getPhilosophy(join(versionDir, '00_PHILOSOPHY'), anchor), `哲学锚点 ${anchor}`, 10);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const current = map.intents[draft.id];
|
|
237
|
-
if (draft._draft.operation === 'add') {
|
|
238
|
-
if (current) throw new Error(`官方 Intent 已存在,不能 add: ${draft.id}`);
|
|
239
|
-
if (draft.revision !== 1 || draft.status !== 'pending') throw new Error('新增 Intent 必须 revision=1 且 status=pending');
|
|
240
|
-
} else {
|
|
241
|
-
if (!current) throw new Error(`待修订的官方 Intent 不存在: ${draft.id}`);
|
|
242
|
-
if (draft.revision !== (current.revision ?? 1) + 1) throw new Error('修订 revision 必须恰好递增 1');
|
|
243
|
-
if (!draft.revision_reason || PLACEHOLDER_RE.test(draft.revision_reason)) throw new Error('修订 draft 必须有真实 revision_reason');
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export function finalizeIntentDraft(versionDir, id, options = {}) {
|
|
248
|
-
const path = draftPath(versionDir, id);
|
|
249
|
-
const draft = getIntentDraft(versionDir, id);
|
|
250
|
-
const map = readOfficialMap(versionDir);
|
|
251
|
-
validateIntentMap(map);
|
|
252
|
-
validateDraft(versionDir, draft, map);
|
|
253
|
-
|
|
254
|
-
const operation = draft._draft.operation;
|
|
255
|
-
const current = map.intents[id];
|
|
256
|
-
const review = options.review || [];
|
|
257
|
-
const unaffected = options.unaffected || [];
|
|
258
|
-
const dependents = operation === 'revise' ? dependentClosure(map.intents, id) : { all: [] };
|
|
259
|
-
if (operation === 'revise') validateImpactPartition(dependents.all, review, unaffected);
|
|
260
|
-
if (operation === 'add' && (review.length || unaffected.length)) {
|
|
261
|
-
throw new Error('新增 Intent 没有既有下游依赖,不接受 --review 或 --unaffected');
|
|
262
|
-
}
|
|
263
|
-
const { _draft, ...intent } = draft;
|
|
264
|
-
if (operation === 'add') {
|
|
265
|
-
intent.status = 'pending';
|
|
266
|
-
} else {
|
|
267
|
-
intent.status = current.status;
|
|
268
|
-
}
|
|
269
|
-
map.intents[id] = intent;
|
|
270
|
-
let reviewed = [];
|
|
271
|
-
if (operation === 'revise') {
|
|
272
|
-
const targets = current.status === 'completed' ? [id, ...review] : review;
|
|
273
|
-
const result = applyImpactReview(map, targets, { incrementPassOnce: true });
|
|
274
|
-
reviewed = result.reviewed;
|
|
275
|
-
intent.status = map.intents[id].status;
|
|
276
|
-
}
|
|
277
|
-
map.topo_order = computeTopoOrder(map.intents, map.topo_order);
|
|
278
|
-
validateIntentMap(map);
|
|
279
|
-
|
|
280
|
-
if (operation === 'add') {
|
|
281
|
-
for (const file of ['01_VISION.md', '05_VERIFICATION.md']) {
|
|
282
|
-
const content = readFileSync(join(versionDir, file), 'utf-8');
|
|
283
|
-
if (!new RegExp(`^#{1,6}\\s+\\[DRAFT\\]\\s+${id}(?::|\\s)`, 'mi').test(content)) {
|
|
284
|
-
throw new Error(`${file} 缺少 ${id} 的 draft 标记,拒绝 finalize`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
atomicWrite(join(versionDir, '04_INTENT_MAP.json'), `${JSON.stringify(map, null, 2)}\n`);
|
|
290
|
-
if (operation === 'add') {
|
|
291
|
-
for (const file of ['01_VISION.md', '05_VERIFICATION.md']) {
|
|
292
|
-
const filePath = join(versionDir, file);
|
|
293
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
294
|
-
const promoted = content
|
|
295
|
-
.replace(new RegExp(`^(#{1,6})\\s+\\[DRAFT\\]\\s+(${id}(?::|\\s))`, 'mi'), '$1 $2')
|
|
296
|
-
.replace(new RegExp(`^<!-- LOOM INTENT DRAFT: ${id} -->\\r?\\n?`, 'mi'), '');
|
|
297
|
-
if (promoted === content) throw new Error(`${file} 未找到可提升的 ${id} draft 标记;官方 Map 已安全写入,draft 保留以便人工恢复`);
|
|
298
|
-
atomicWrite(filePath, promoted);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
unlinkSync(path);
|
|
302
|
-
return { operation, intent, topo_order: map.topo_order, reviewed, unaffected };
|
|
303
|
-
}
|