@haaaiawd/loom 0.1.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/README.md +371 -0
- package/cli/bin/loom.js +465 -0
- package/cli/src/activate.js +64 -0
- package/cli/src/auto.js +44 -0
- package/cli/src/diagnostics.js +277 -0
- package/cli/src/guide.js +201 -0
- package/cli/src/help.js +410 -0
- package/cli/src/init.js +125 -0
- package/cli/src/intent-map.js +284 -0
- package/cli/src/philosophy.js +117 -0
- package/cli/src/preview-prompt.md +329 -0
- package/cli/src/preview.js +15 -0
- package/cli/src/verify.js +199 -0
- package/cli/src/version.js +133 -0
- package/dimensions/SEARCH_METHODOLOGY.md +97 -0
- package/meta/BASELINE.md +276 -0
- package/meta/INTENT_LOOP.md +596 -0
- package/meta/PHILOSOPHY_WEAVER.md +289 -0
- package/meta/ROLE_ACTIVATION.md +267 -0
- package/package.json +41 -0
- package/roles/architect.md +99 -0
- package/roles/forge.md +126 -0
- package/roles/keeper.md +196 -0
- package/roles/visionary.md +86 -0
- package/templates/INTENT_MAP_TEMPLATE.json +65 -0
- package/templates/PHILOSOPHY_TEMPLATE.md +75 -0
- package/templates/VISION_TEMPLATE.md +65 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// intent-map.js — Intent Map 的加载、校验、查询
|
|
2
|
+
// 真相源是磁盘上的 04_INTENT_MAP.json,这个库负责按需查询,不返回整个文件。
|
|
3
|
+
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
5
|
+
import { resolve, dirname, join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 从 heading 文本中提取显式锚点(和 philosophy.js 一致的逻辑)。
|
|
9
|
+
*/
|
|
10
|
+
function extractExplicitAnchor(headingText) {
|
|
11
|
+
const match = headingText.match(/\{#([\w-]+)\}\s*$/);
|
|
12
|
+
return match ? match[1] : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* slugify — strip \r、支持显式锚点、ASCII fallback。
|
|
17
|
+
*/
|
|
18
|
+
function slugify(text) {
|
|
19
|
+
return text
|
|
20
|
+
.replace(/\r/g, '')
|
|
21
|
+
.replace(/\{#[\w-]+\}\s*$/, '')
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^\w\s-]/g, '')
|
|
24
|
+
.replace(/\s+/g, '-')
|
|
25
|
+
.replace(/-+/g, '-')
|
|
26
|
+
.replace(/^-|-$/g, '')
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 从 MD 内容中提取指定 section(和 philosophy.js 一致的逻辑)。
|
|
32
|
+
*/
|
|
33
|
+
function extractMdSection(content, sectionSlug) {
|
|
34
|
+
if (!sectionSlug) return content;
|
|
35
|
+
const lines = content.split('\n');
|
|
36
|
+
let capturing = false;
|
|
37
|
+
let targetLevel = 0;
|
|
38
|
+
const captured = [];
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const cleanLine = line.replace(/\r$/, '');
|
|
41
|
+
const headingMatch = cleanLine.match(/^(#{1,6})\s+(.+)$/);
|
|
42
|
+
if (headingMatch) {
|
|
43
|
+
const level = headingMatch[1].length;
|
|
44
|
+
const headingText = headingMatch[2];
|
|
45
|
+
const slug = extractExplicitAnchor(headingText) || slugify(headingText);
|
|
46
|
+
if (capturing && level <= targetLevel) break;
|
|
47
|
+
if (slug === sectionSlug) {
|
|
48
|
+
capturing = true;
|
|
49
|
+
targetLevel = level;
|
|
50
|
+
captured.push(cleanLine);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (capturing) captured.push(cleanLine);
|
|
55
|
+
}
|
|
56
|
+
if (captured.length === 0) {
|
|
57
|
+
throw new Error(`意图叙事章节未找到: #${sectionSlug}`);
|
|
58
|
+
}
|
|
59
|
+
return captured.join('\n').trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 必填字段(INTENT_LOOP.md 底线) */
|
|
63
|
+
const REQUIRED_FIELDS = ['id', 'narrative_ref', 'depends_on', 'acceptance', 'philosophy_anchors', 'status'];
|
|
64
|
+
|
|
65
|
+
/** 合法 status 值 */
|
|
66
|
+
const VALID_STATUS = ['pending', 'in_progress', 'completed', 'blocked', 'needs_review'];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 加载 Intent Map 文件。
|
|
70
|
+
* @param {string} loomDir — .loom/v{N}/ 目录的绝对路径
|
|
71
|
+
* @returns {{ _meta: object, intents: Record<string, object>, topo_order: string[] }}
|
|
72
|
+
*/
|
|
73
|
+
export function loadIntentMap(loomDir) {
|
|
74
|
+
const filePath = join(loomDir, '04_INTENT_MAP.json');
|
|
75
|
+
if (!existsSync(filePath)) {
|
|
76
|
+
throw new Error(`Intent Map 不存在: ${filePath}`);
|
|
77
|
+
}
|
|
78
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
79
|
+
let data;
|
|
80
|
+
try {
|
|
81
|
+
data = JSON.parse(raw);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
throw new Error(`Intent Map JSON 解析失败: ${e.message}`);
|
|
84
|
+
}
|
|
85
|
+
validateIntentMap(data);
|
|
86
|
+
return data;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 校验 Intent Map 结构合规性(INTENT_LOOP.md I-1, I-2 底线)。
|
|
91
|
+
* 抛出错误列表,不静默修复。
|
|
92
|
+
*/
|
|
93
|
+
export function validateIntentMap(data) {
|
|
94
|
+
const errors = [];
|
|
95
|
+
|
|
96
|
+
if (!data.intents || typeof data.intents !== 'object') {
|
|
97
|
+
errors.push('缺少 intents 对象');
|
|
98
|
+
throw new Error(`Intent Map 校验失败:\n - ${errors.join('\n - ')}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!Array.isArray(data.topo_order)) {
|
|
102
|
+
errors.push('缺少 topo_order 数组');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const [id, intent] of Object.entries(data.intents)) {
|
|
106
|
+
if (intent.id !== id) {
|
|
107
|
+
errors.push(`intents["${id}"].id 与 key 不一致 (实际: "${intent.id}")`);
|
|
108
|
+
}
|
|
109
|
+
for (const field of REQUIRED_FIELDS) {
|
|
110
|
+
if (!(field in intent)) {
|
|
111
|
+
errors.push(`intents["${id}"] 缺少必填字段: ${field}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (intent.status && !VALID_STATUS.includes(intent.status)) {
|
|
115
|
+
errors.push(`intents["${id}"].status 非法: "${intent.status}" (合法: ${VALID_STATUS.join('|')})`);
|
|
116
|
+
}
|
|
117
|
+
if (intent.depends_on) {
|
|
118
|
+
for (const dep of intent.depends_on) {
|
|
119
|
+
if (!(dep in data.intents)) {
|
|
120
|
+
errors.push(`intents["${id}"].depends_on 引用了不存在的 Intent: ${dep}`);
|
|
121
|
+
}
|
|
122
|
+
// 依赖状态一致性:completed 的 Intent 不能依赖 blocked 的 Intent
|
|
123
|
+
const depIntent = data.intents[dep];
|
|
124
|
+
if (depIntent && intent.status === 'completed' && depIntent.status === 'blocked') {
|
|
125
|
+
errors.push(`intents["${id}"] 状态为 completed 但依赖 blocked 的 ${dep}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// topo_order 必须覆盖所有 Intent
|
|
132
|
+
if (Array.isArray(data.topo_order)) {
|
|
133
|
+
const topoSet = new Set(data.topo_order);
|
|
134
|
+
for (const id of Object.keys(data.intents)) {
|
|
135
|
+
if (!topoSet.has(id)) {
|
|
136
|
+
errors.push(`topo_order 缺少 Intent: ${id}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (errors.length > 0) {
|
|
142
|
+
throw new Error(`Intent Map 校验失败:\n - ${errors.join('\n - ')}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 返回下一个可执行 Intent:
|
|
148
|
+
* status=pending 且 depends_on 全部 completed,按 topo_order 取第一个。
|
|
149
|
+
* @returns {object|null} Intent 对象,或 null(没有可执行的)
|
|
150
|
+
*/
|
|
151
|
+
export function getNextIntent(loomDir) {
|
|
152
|
+
const { intents, topo_order } = loadIntentMap(loomDir);
|
|
153
|
+
for (const id of topo_order) {
|
|
154
|
+
const intent = intents[id];
|
|
155
|
+
if (intent.status !== 'pending') continue;
|
|
156
|
+
const depsReady = intent.depends_on.every(
|
|
157
|
+
(dep) => intents[dep]?.status === 'completed'
|
|
158
|
+
);
|
|
159
|
+
if (depsReady) return intent;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 返回进度概览:各状态的 Intent 数量 + ID 列表。
|
|
166
|
+
*/
|
|
167
|
+
export function getStatus(loomDir) {
|
|
168
|
+
const { intents } = loadIntentMap(loomDir);
|
|
169
|
+
const summary = { pending: [], in_progress: [], completed: [], blocked: [] };
|
|
170
|
+
for (const [id, intent] of Object.entries(intents)) {
|
|
171
|
+
const s = intent.status;
|
|
172
|
+
if (summary[s]) summary[s].push(id);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
counts: {
|
|
176
|
+
pending: summary.pending.length,
|
|
177
|
+
in_progress: summary.in_progress.length,
|
|
178
|
+
completed: summary.completed.length,
|
|
179
|
+
blocked: summary.blocked.length,
|
|
180
|
+
total: Object.keys(intents).length,
|
|
181
|
+
},
|
|
182
|
+
ids: summary,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 输出 Mermaid 依赖图。
|
|
188
|
+
*/
|
|
189
|
+
export function getDependencyGraph(loomDir) {
|
|
190
|
+
const { intents, topo_order } = loadIntentMap(loomDir);
|
|
191
|
+
const lines = ['```mermaid', 'graph TD'];
|
|
192
|
+
for (const id of topo_order) {
|
|
193
|
+
const intent = intents[id];
|
|
194
|
+
const shape = intent.status === 'completed' ? ':::done'
|
|
195
|
+
: intent.status === 'blocked' ? ':::blocked'
|
|
196
|
+
: intent.status === 'in_progress' ? ':::active'
|
|
197
|
+
: '';
|
|
198
|
+
lines.push(` ${id}${shape}`);
|
|
199
|
+
if (intent.depends_on && intent.depends_on.length > 0) {
|
|
200
|
+
for (const dep of intent.depends_on) {
|
|
201
|
+
lines.push(` ${dep} --> ${id}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
lines.push('```');
|
|
206
|
+
return lines.join('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 按 ID 返回单个 Intent 的完整信息。
|
|
211
|
+
*/
|
|
212
|
+
export function getIntent(loomDir, intentId) {
|
|
213
|
+
const { intents } = loadIntentMap(loomDir);
|
|
214
|
+
if (!(intentId in intents)) {
|
|
215
|
+
throw new Error(`Intent 不存在: ${intentId}`);
|
|
216
|
+
}
|
|
217
|
+
return intents[intentId];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 更新 Intent 的运行时 status。
|
|
222
|
+
* 只允许合法的状态转换,防止跳变(如 completed→pending)。
|
|
223
|
+
* @param {string} loomDir — .loom/v{N}/ 目录
|
|
224
|
+
* @param {string} intentId — Intent ID
|
|
225
|
+
* @param {string} newStatus — pending | in_progress | completed | blocked
|
|
226
|
+
* @returns {object} 更新后的 Intent
|
|
227
|
+
*/
|
|
228
|
+
export function updateIntentStatus(loomDir, intentId, newStatus) {
|
|
229
|
+
if (!VALID_STATUS.includes(newStatus)) {
|
|
230
|
+
throw new Error(`非法 status: "${newStatus}" (合法: ${VALID_STATUS.join('|')})`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const filePath = join(loomDir, '04_INTENT_MAP.json');
|
|
234
|
+
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
235
|
+
|
|
236
|
+
if (!(intentId in data.intents)) {
|
|
237
|
+
throw new Error(`Intent 不存在: ${intentId}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const oldStatus = data.intents[intentId].status;
|
|
241
|
+
const validTransitions = {
|
|
242
|
+
pending: ['in_progress', 'blocked'],
|
|
243
|
+
in_progress: ['completed', 'blocked'],
|
|
244
|
+
completed: ['needs_review'], // 变更回流时可标记需重新验证
|
|
245
|
+
blocked: ['pending'], // 阻塞解除后回到 pending
|
|
246
|
+
needs_review: ['pending', 'completed'], // 重新验证后回到 pending 或直接 completed
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (!validTransitions[oldStatus]?.includes(newStatus)) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`非法状态转换: ${oldStatus} → ${newStatus}` +
|
|
252
|
+
`\n合法转换: ${oldStatus} → [${validTransitions[oldStatus]?.join(', ') || '无(终态)'}]`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
data.intents[intentId].status = newStatus;
|
|
257
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
258
|
+
return data.intents[intentId];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 获取某 Intent 的意图叙事(解析 narrative_ref,读愿景文档对应章节)。
|
|
263
|
+
* narrative_ref 格式: "01_VISION.md#int-001" 或 "01_VISION.md#int-001"
|
|
264
|
+
* @returns {string} 意图叙事内容
|
|
265
|
+
*/
|
|
266
|
+
export function getNarrative(loomDir, intentId) {
|
|
267
|
+
const { intents } = loadIntentMap(loomDir);
|
|
268
|
+
if (!(intentId in intents)) {
|
|
269
|
+
throw new Error(`Intent 不存在: ${intentId}`);
|
|
270
|
+
}
|
|
271
|
+
const ref = intents[intentId].narrative_ref;
|
|
272
|
+
if (!ref) {
|
|
273
|
+
throw new Error(`Intent ${intentId} 没有 narrative_ref`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 解析 "FILE.md#section" 格式
|
|
277
|
+
const [file, section] = ref.split('#');
|
|
278
|
+
const filePath = join(loomDir, file.trim());
|
|
279
|
+
if (!existsSync(filePath)) {
|
|
280
|
+
throw new Error(`愿景文档不存在: ${filePath}`);
|
|
281
|
+
}
|
|
282
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
283
|
+
return extractMdSection(content, section ? section.trim() : null);
|
|
284
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// philosophy.js — 按锚点加载哲学文档的特定章节
|
|
2
|
+
// 哲学文档是 MD,锚点格式: "PRODUCT_PHILOSOPHY.md#core-belief"
|
|
3
|
+
// 这个库按锚点提取对应章节,不返回整个文件。
|
|
4
|
+
|
|
5
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 解析锚点字符串。
|
|
10
|
+
* @param {string} anchor — "FILE.md#section" 或 "FILE.md"
|
|
11
|
+
* @returns {{ file: string, section: string|null }}
|
|
12
|
+
*/
|
|
13
|
+
export function parseAnchor(anchor) {
|
|
14
|
+
const [file, section] = anchor.split('#');
|
|
15
|
+
return { file: file.trim(), section: section ? section.trim() : null };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 从 heading 文本中提取显式锚点。
|
|
20
|
+
* 支持 Pandoc/MDX 风格语法: "## 核心信念 {#core-belief}"
|
|
21
|
+
* @returns {string|null} 显式锚点 slug,或 null(无显式锚点时)
|
|
22
|
+
*/
|
|
23
|
+
function extractExplicitAnchor(headingText) {
|
|
24
|
+
const match = headingText.match(/\{#([\w-]+)\}\s*$/);
|
|
25
|
+
return match ? match[1] : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 从 heading 文本生成 slug(fallback,无显式锚点时用)。
|
|
30
|
+
* 规则:小写、去除 \r、空格转连字符、去除非 [a-z0-9_-] 字符。
|
|
31
|
+
* 注意:\w 不匹配中文,所以中文标题的 slug 会是空的——
|
|
32
|
+
* 这就是为什么哲学文档应该用显式锚点 {#anchor} 标注中文标题。
|
|
33
|
+
*/
|
|
34
|
+
function slugify(text) {
|
|
35
|
+
return text
|
|
36
|
+
.replace(/\r/g, '') // strip CRLF 的 \r
|
|
37
|
+
.replace(/\{#[\w-]+\}\s*$/, '') // 去掉显式锚点标记
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/[^\w\s-]/g, '') // \w = [a-zA-Z0-9_]
|
|
40
|
+
.replace(/\s+/g, '-')
|
|
41
|
+
.replace(/-+/g, '-')
|
|
42
|
+
.replace(/^-|-$/g, '')
|
|
43
|
+
.trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 从 MD 内容中提取指定 section 的内容(到下一个同级或更高级 heading 为止)。
|
|
48
|
+
* 如果 section 为 null,返回整个文件。
|
|
49
|
+
* 支持显式锚点 {#slug} 和自动 slugify 两种方式匹配。
|
|
50
|
+
*/
|
|
51
|
+
function extractSection(content, sectionSlug) {
|
|
52
|
+
if (!sectionSlug) return content;
|
|
53
|
+
|
|
54
|
+
const lines = content.split('\n');
|
|
55
|
+
let capturing = false;
|
|
56
|
+
let targetLevel = 0;
|
|
57
|
+
const captured = [];
|
|
58
|
+
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
// strip \r 以兼容 Windows CRLF
|
|
61
|
+
const cleanLine = line.replace(/\r$/, '');
|
|
62
|
+
const headingMatch = cleanLine.match(/^(#{1,6})\s+(.+)$/);
|
|
63
|
+
if (headingMatch) {
|
|
64
|
+
const level = headingMatch[1].length;
|
|
65
|
+
const headingText = headingMatch[2];
|
|
66
|
+
// 优先用显式锚点,没有再 fallback 到 slugify
|
|
67
|
+
const slug = extractExplicitAnchor(headingText) || slugify(headingText);
|
|
68
|
+
|
|
69
|
+
if (capturing && level <= targetLevel) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (slug === sectionSlug) {
|
|
73
|
+
capturing = true;
|
|
74
|
+
targetLevel = level;
|
|
75
|
+
captured.push(cleanLine);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (capturing) {
|
|
80
|
+
captured.push(cleanLine);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (captured.length === 0) {
|
|
85
|
+
throw new Error(`章节未找到: #${sectionSlug}`);
|
|
86
|
+
}
|
|
87
|
+
return captured.join('\n').trim();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 按锚点加载哲学文档内容。
|
|
92
|
+
* @param {string} philosophyDir — 00_PHILOSOPHY/ 目录路径
|
|
93
|
+
* @param {string} anchor — "PRODUCT_PHILOSOPHY.md#core-belief"
|
|
94
|
+
* @returns {string} MD 文本(章节或整个文件)
|
|
95
|
+
*/
|
|
96
|
+
export function getPhilosophy(philosophyDir, anchor) {
|
|
97
|
+
const { file, section } = parseAnchor(anchor);
|
|
98
|
+
const filePath = join(philosophyDir, file);
|
|
99
|
+
|
|
100
|
+
if (!existsSync(filePath)) {
|
|
101
|
+
throw new Error(`哲学文档不存在: ${filePath}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
105
|
+
return extractSection(content, section);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 列出哲学目录下所有 .md 文件名。
|
|
110
|
+
*/
|
|
111
|
+
export function listPhilosophyFiles(philosophyDir) {
|
|
112
|
+
if (!existsSync(philosophyDir)) {
|
|
113
|
+
throw new Error(`哲学目录不存在: ${philosophyDir}`);
|
|
114
|
+
}
|
|
115
|
+
const dir = readdirSync(philosophyDir);
|
|
116
|
+
return dir.filter((f) => f.endsWith('.md'));
|
|
117
|
+
}
|