@haaaiawd/loom 0.10.0 → 1.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -52
  3. package/cli/bin/loom.js +285 -99
  4. package/cli/help/concepts.md +93 -72
  5. package/cli/help/doctor.md +71 -121
  6. package/cli/help/loop.md +120 -135
  7. package/cli/help/patch.md +33 -0
  8. package/cli/help/preview.md +2 -1
  9. package/cli/help/version.md +92 -16
  10. package/cli/help/workflow.md +89 -100
  11. package/cli/src/activate.js +302 -73
  12. package/cli/src/diagnostics.js +138 -41
  13. package/cli/src/guide.js +41 -19
  14. package/cli/src/init.js +50 -29
  15. package/cli/src/intent-draft.js +303 -0
  16. package/cli/src/intent-map.js +540 -54
  17. package/cli/src/patch.js +214 -0
  18. package/cli/src/philosophy.js +177 -154
  19. package/cli/src/preview-prompt.md +13 -6
  20. package/cli/src/preview.js +1 -0
  21. package/cli/src/shared/intent-ref.js +38 -0
  22. package/cli/src/shared/proof-reference.js +19 -0
  23. package/cli/src/shared/verification-method.js +32 -0
  24. package/cli/src/verify.js +184 -61
  25. package/cli/src/version.js +5 -4
  26. package/dimensions/PART_DECOMPOSITION.md +42 -203
  27. package/dimensions/SEARCH_METHODOLOGY.md +101 -97
  28. package/dimensions/examples/AGENT_SYSTEM/README.md +1 -1
  29. package/dimensions/examples/CLI_TOOL/README.md +1 -1
  30. package/dimensions/universal/COLLABORATION_PHILOSOPHY.md +28 -77
  31. package/dimensions/universal/ENGINEERING_CREED.md +30 -74
  32. package/dimensions/universal/PRODUCT_PHILOSOPHY.md +32 -70
  33. package/meta/BASELINE.md +91 -276
  34. package/meta/INTENT_LOOP.md +242 -737
  35. package/meta/PHILOSOPHY_WEAVER.md +110 -343
  36. package/meta/ROLE_ACTIVATION.md +103 -267
  37. package/package.json +4 -3
  38. package/roles/architect.md +71 -111
  39. package/roles/forge.md +87 -126
  40. package/roles/keeper.md +99 -223
  41. package/roles/visionary.md +57 -86
  42. package/templates/INTENT_MAP_TEMPLATE.json +24 -10
  43. package/templates/PHILOSOPHY_TEMPLATE.md +44 -75
  44. package/templates/VISION_TEMPLATE.md +44 -67
@@ -0,0 +1,38 @@
1
+ // intent-ref.js — Resolve current and version-qualified Intent read references.
2
+
3
+ import { existsSync } from 'node:fs';
4
+ import { basename, dirname, join } from 'node:path';
5
+
6
+ export const VERSION_PATTERN = /^v\d+$/;
7
+ export const INTENT_ID_PATTERN = /^INT-\d+$/;
8
+
9
+ export function formatIntentRef(version, intentId) {
10
+ return `${version}:${intentId}`;
11
+ }
12
+
13
+ /** Resolve INT-003 against the current version or v1:INT-003 against .loom/v1. */
14
+ export function resolveIntentRef(currentVersionDir, input) {
15
+ const currentVersion = basename(currentVersionDir);
16
+ if (!VERSION_PATTERN.test(currentVersion)) {
17
+ throw new Error(`无法从目录确定当前 LOOM 版本: ${currentVersionDir}`);
18
+ }
19
+
20
+ const parts = String(input || '').split(':');
21
+ if (parts.length > 2) throw new Error(`Intent 引用格式非法: ${input}`);
22
+ const qualified = parts.length === 2;
23
+ const version = qualified ? parts[0] : currentVersion;
24
+ const intentId = qualified ? parts[1] : parts[0];
25
+ if (!VERSION_PATTERN.test(version) || !INTENT_ID_PATTERN.test(intentId)) {
26
+ throw new Error(`Intent 引用格式非法: ${input}(应为 INT-003 或 v1:INT-003)`);
27
+ }
28
+
29
+ const versionDir = qualified ? join(dirname(currentVersionDir), version) : currentVersionDir;
30
+ if (!existsSync(versionDir)) throw new Error(`版本不存在: ${version}`);
31
+ return {
32
+ ref: formatIntentRef(version, intentId),
33
+ version,
34
+ intentId,
35
+ versionDir,
36
+ historical: version !== currentVersion,
37
+ };
38
+ }
@@ -0,0 +1,19 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { isAbsolute, relative, resolve } from 'node:path';
3
+ import { extractMdSection } from './md-utils.js';
4
+
5
+ /** Resolve a root-relative Markdown proof reference without allowing path escape. */
6
+ export function resolveQualityProofReference(versionDir, ref) {
7
+ if (typeof ref !== 'string' || !ref.trim()) throw new Error('quality_proof_ref 必须是非空字符串');
8
+ const match = ref.trim().match(/^([^#]+)#([\w-]+)$/);
9
+ if (!match) throw new Error('quality_proof_ref 必须是项目相对路径加 Markdown 锚点,例如 verifications/INT-001-quality-proof.md#int-001');
10
+ const [, file, anchor] = match;
11
+ if (isAbsolute(file)) throw new Error('quality_proof_ref 不得使用绝对路径');
12
+ const projectDir = resolve(versionDir, '..', '..');
13
+ const filePath = resolve(projectDir, file);
14
+ const relation = relative(projectDir, filePath);
15
+ if (relation.startsWith('..') || isAbsolute(relation)) throw new Error('quality_proof_ref 不得越出项目目录');
16
+ if (!existsSync(filePath)) throw new Error(`quality_proof_ref 指向的文件不存在: ${file}`);
17
+ extractMdSection(readFileSync(filePath, 'utf-8'), anchor, 'Quality Proof');
18
+ return { filePath, anchor, ref: ref.trim() };
19
+ }
@@ -0,0 +1,32 @@
1
+ export function getIntentVerificationMethod(intent) {
2
+ return intent?.verification_method || intent?._optional?.verification_method || null;
3
+ }
4
+
5
+ function normalize(command) {
6
+ return String(command || '')
7
+ .replace(/^\s*(?:run|exec)\s+/i, '')
8
+ .replace(/\s+/g, ' ')
9
+ .trim();
10
+ }
11
+
12
+ function normalizePackageManager(command) {
13
+ return ['npm', 'pnpm', 'bun', 'yarn'].reduce(
14
+ (result, manager) => result.replace(new RegExp(`\\b${manager}\\b`, 'g'), '<PM>'),
15
+ command,
16
+ );
17
+ }
18
+
19
+ /** Whether a recorded reproduction command covers the Architect-declared method. */
20
+ export function commandCoversVerificationMethod(actualCommand, expectedMethod) {
21
+ const actual = normalize(actualCommand);
22
+ const expected = normalize(expectedMethod);
23
+ if (!actual || !expected) return false;
24
+ return expected.split('&&').every((part) => {
25
+ const expectedPart = normalize(part);
26
+ if (!expectedPart || actual.includes(expectedPart)) return true;
27
+ const actualNorm = normalizePackageManager(actual);
28
+ const expectedNorm = normalizePackageManager(expectedPart);
29
+ if (actualNorm.includes(expectedNorm)) return true;
30
+ return expectedPart.startsWith('node --test') && actualNorm.includes('<PM> test');
31
+ });
32
+ }
package/cli/src/verify.js CHANGED
@@ -3,56 +3,75 @@
3
3
 
4
4
  import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
- import { extractMdSection, readJsonFile } from './shared/md-utils.js';
6
+ import { extractMdSection, readJsonFile } from './shared/md-utils.js';
7
+ import { getIntent, getEffectiveVerificationEpoch, hasLegacyIntentRevision } from './intent-map.js';
8
+ import { formatIntentRef, resolveIntentRef } from './shared/intent-ref.js';
9
+ import { resolveQualityProofReference } from './shared/proof-reference.js';
7
10
 
8
11
  /** 合法判定结果 */
9
12
  const VALID_VERDICTS = ['passed', 'deviated', 'blocked', 'pending_human'];
10
13
 
11
- /** 四个必须覆盖的验证维度 */
12
- const REQUIRED_DIMENSIONS = [
13
- 'intent_fidelity',
14
- 'philosophy_consistency',
15
- 'baseline_compliance',
16
- 'acceptance_achievement',
17
- ];
14
+ /** 每个 Intent 都必须覆盖的基础验证维度。 */
15
+ const BASE_DIMENSIONS = [
16
+ 'intent_fidelity',
17
+ 'philosophy_consistency',
18
+ 'baseline_compliance',
19
+ 'acceptance_achievement',
20
+ ];
21
+
22
+ function getRequiredDimensions(intent) {
23
+ const dimensions = [...BASE_DIMENSIONS];
24
+ if (intent?.continuity_required) dimensions.push('preservation_achievement');
25
+ if (intent?.quality_contract) dimensions.push('quality_achievement');
26
+ return dimensions;
27
+ }
18
28
 
19
29
  /**
20
30
  * 写入一条验证记录(追加模式——同一 Intent 多次验证保留完整历史)。
21
31
  * 文件格式: { intent_id, records: [{ round, verdict, timestamp, ... }] }
22
- * @param {string} verificationsDirverifications/ 目录路径
32
+ * @param {string} versionDir当前 .loom/v{N}/ 目录,用于可信读取 Intent revision
33
+ * @param {string} verificationsDir — verifications/ 目录路径
23
34
  * @param {object} record — 验证记录
24
35
  * @param {string} record.intent_id — 如 "INT-001"
25
36
  * @param {string} record.verdict — passed | deviated | blocked
26
37
  * @param {string} record.timestamp — ISO 8601
27
38
  * @param {string} record.summary — 验证摘要
28
- * @param {object} record.dimensions — 四个维度的验证结果
39
+ * @param {object} record.dimensions — 基础维度,以及质量契约存在时的 quality_achievement
29
40
  * @param {string} [record.reproduction_command] — 复现验证的命令(如 "LLM_API_KEY=mock npm test")
30
41
  * @param {string} [record.deviation_detail] — 偏离说明(deviated 时)
31
42
  * @param {boolean} [record.reset_suggested] — 是否建议重置上下文
32
43
  * @returns {{ filePath: string, round: number, deviated_count: number, should_escalate: boolean }}
33
44
  */
34
- export function writeVerification(verificationsDir, record) {
35
- const errors = [];
36
- if (!record.intent_id) errors.push('缺少 intent_id');
45
+ export function writeVerification(versionDir, verificationsDir, record) {
46
+ const errors = [];
47
+ if (!record.intent_id) errors.push('缺少 intent_id');
37
48
  if (!record.verdict || !VALID_VERDICTS.includes(record.verdict)) {
38
49
  errors.push(`verdict 非法: "${record.verdict}" (合法: ${VALID_VERDICTS.join('|')})`);
39
50
  }
40
- if (!record.timestamp) errors.push('缺少 timestamp');
41
- if (!record.dimensions) errors.push('缺少 dimensions(四个维度结果)');
42
- // dimensions 结构校验:每个维度必须是 { verdict, evidence } 对象
43
- if (record.dimensions) {
44
- for (const dim of REQUIRED_DIMENSIONS) {
45
- const v = record.dimensions[dim];
46
- if (v === undefined) {
47
- errors.push(`dimensions.${dim} 缺失(四个维度必须全覆盖)`);
51
+ if (!record.timestamp) errors.push('缺少 timestamp');
52
+ if (!record.dimensions) errors.push('缺少 dimensions(适用验证维度结果)');
53
+ const intent = record.intent_id ? getIntent(versionDir, record.intent_id) : null;
54
+ if (intent && !['in_progress', 'needs_review'].includes(intent.status)) {
55
+ errors.push(`Intent ${record.intent_id} 当前状态为 ${intent.status};只能为 in_progress 或 needs_review 的 Intent 写入验证记录`);
56
+ }
57
+ const requiredDimensions = getRequiredDimensions(intent);
58
+ // dimensions 结构校验:每个维度必须是 { verdict, evidence } 对象
59
+ if (record.dimensions) {
60
+ for (const dim of requiredDimensions) {
61
+ const v = record.dimensions[dim];
62
+ if (v === undefined) {
63
+ errors.push(`dimensions.${dim} 缺失(当前 Intent 的适用维度必须全覆盖)`);
48
64
  } else if (typeof v === 'string') {
49
65
  errors.push(`dimensions.${dim} 是旧格式(枚举值),必须改成 { verdict, evidence } 对象`);
50
66
  } else if (typeof v !== 'object' || v === null) {
51
67
  errors.push(`dimensions.${dim} 必须是 { verdict, evidence } 对象`);
52
68
  } else {
53
- if (!VALID_VERDICTS.includes(v.verdict)) {
54
- errors.push(`dimensions.${dim}.verdict 非法: "${v.verdict}" (合法: ${VALID_VERDICTS.join('|')})`);
55
- }
69
+ if (!VALID_VERDICTS.includes(v.verdict)) {
70
+ errors.push(`dimensions.${dim}.verdict 非法: "${v.verdict}" (合法: ${VALID_VERDICTS.join('|')})`);
71
+ }
72
+ if (record.verdict === 'passed' && v.verdict !== 'passed') {
73
+ errors.push(`整体 verdict 为 passed 时,dimensions.${dim}.verdict 也必须是 passed`);
74
+ }
56
75
  if (!v.evidence || typeof v.evidence !== 'string' || v.evidence.trim() === '') {
57
76
  errors.push(`dimensions.${dim}.evidence 缺失——必须给出具体证据,不能只写"合规"`);
58
77
  } else {
@@ -66,12 +85,28 @@ export function writeVerification(verificationsDir, record) {
66
85
  errors.push(`dimensions.${dim}.evidence "${ev}" 是通用评价而非具体证据——必须写"对照了什么 + 在代码哪里看到/没看到"`);
67
86
  }
68
87
  }
69
- }
70
- }
71
- }
72
- if (errors.length > 0) {
73
- throw new Error(`验证记录校验失败:\n - ${errors.join('\n - ')}`);
74
- }
88
+ }
89
+ }
90
+ }
91
+ const qualityProofRef = record.dimensions?.quality_achievement?.quality_proof_ref;
92
+ if (intent?.quality_contract && record.verdict === 'passed' && !qualityProofRef) {
93
+ errors.push('声明 quality_contract 的 Intent 通过时必须提供 dimensions.quality_achievement.quality_proof_ref');
94
+ }
95
+ if (qualityProofRef !== undefined
96
+ && (typeof qualityProofRef !== 'string' || qualityProofRef.trim() === '')) {
97
+ errors.push('dimensions.quality_achievement.quality_proof_ref 必须是非空字符串');
98
+ } else if (qualityProofRef !== undefined) {
99
+ try {
100
+ resolveQualityProofReference(versionDir, qualityProofRef);
101
+ } catch (error) {
102
+ errors.push(error.message);
103
+ }
104
+ }
105
+ if (errors.length > 0) {
106
+ throw new Error(`验证记录校验失败:\n - ${errors.join('\n - ')}`);
107
+ }
108
+
109
+ const intentRevision = getEffectiveIntentRevision(intent);
75
110
 
76
111
  const filePath = join(verificationsDir, `${record.intent_id}.json`);
77
112
 
@@ -102,14 +137,16 @@ export function writeVerification(verificationsDir, record) {
102
137
  }
103
138
 
104
139
  // 追加新记录
105
- data.records.push({
106
- round,
140
+ data.records.push({
141
+ round,
142
+ intent_revision: intentRevision,
143
+ verification_epoch: getEffectiveVerificationEpoch(intent),
107
144
  verdict: record.verdict,
108
145
  timestamp: record.timestamp,
109
146
  summary: record.summary,
110
147
  dimensions: record.dimensions,
111
- reproduction_command: record.reproduction_command,
112
- deviation_detail: record.deviation_detail,
148
+ reproduction_command: record.reproduction_command,
149
+ deviation_detail: record.deviation_detail,
113
150
  reset_suggested: record.reset_suggested,
114
151
  });
115
152
 
@@ -126,59 +163,145 @@ export function writeVerification(verificationsDir, record) {
126
163
  * 读取某 Intent 的验证历史。
127
164
  * @returns {{ intent_id: string, records: array } | null}
128
165
  */
129
- export function getVerificationHistory(verificationsDir, intentId) {
166
+ export function getVerificationHistory(verificationsDir, intentId) {
130
167
  const filePath = join(verificationsDir, `${intentId}.json`);
131
168
  if (!existsSync(filePath)) {
132
169
  return null;
133
170
  }
134
- return readJsonFile(filePath, '验证记录');
135
- }
171
+ return readJsonFile(filePath, '验证记录');
172
+ }
173
+
174
+ /** Read each owning version's local records along the explicit predecessor graph. */
175
+ export function getAcrossVersionVerificationHistory(currentVersionDir, inputRef) {
176
+ const root = resolveIntentRef(currentVersionDir, inputRef);
177
+ const histories = [];
178
+ const visited = new Set();
179
+ const active = new Set();
180
+
181
+ function walk(resolved) {
182
+ if (active.has(resolved.ref)) throw new Error(`Intent lineage 存在循环: ${[...active, resolved.ref].join(' -> ')}`);
183
+ if (visited.has(resolved.ref)) return;
184
+ active.add(resolved.ref);
185
+ const intent = getIntent(resolved.versionDir, resolved.intentId);
186
+ const local = getVerificationHistory(join(resolved.versionDir, 'verifications'), resolved.intentId);
187
+ histories.push({
188
+ ref: resolved.ref,
189
+ source_version: resolved.version,
190
+ source_intent: resolved.intentId,
191
+ source_intent_id: resolved.intentId,
192
+ records: (local?.records || []).map((record) => ({
193
+ ...record,
194
+ source_version: resolved.version,
195
+ source_intent: resolved.intentId,
196
+ source_intent_id: resolved.intentId,
197
+ })),
198
+ });
199
+ for (const predecessor of intent.lineage?.predecessors || []) {
200
+ walk(resolveIntentRef(currentVersionDir, formatIntentRef(predecessor.version, predecessor.intent_id)));
201
+ }
202
+ active.delete(resolved.ref);
203
+ visited.add(resolved.ref);
204
+ }
205
+
206
+ walk(root);
207
+ return { intent_ref: root.ref, across_versions: true, histories };
208
+ }
136
209
 
137
210
  /**
138
211
  * 快捷创建验证记录——Agent 不用手动构造完整 JSON。
139
- * 内部用 summary 填充四个维度的 evidence,生成标准记录格式。
140
- * @param {string} verificationsDirverifications/ 目录路径
212
+ * 内部用 summary 填充适用维度的 evidence,生成标准记录格式。
213
+ * @param {string} versionDir当前 .loom/v{N}/ 目录
214
+ * @param {string} verificationsDir — verifications/ 目录路径
141
215
  * @param {string} intentId — 如 "INT-001"
142
216
  * @param {string} verdict — 'passed' | 'deviated' | 'blocked'
143
- * @param {string} summary — 验证摘要(也会作为四个维度的 evidence)
144
- * @param {object} [extras]
145
- * @param {string} [extras.reproduction_command] — 复现命令
217
+ * @param {string} summary — 验证摘要(也会作为所有适用维度的 evidence)
218
+ * @param {object} [extras]
219
+ * @param {string} [extras.reproduction_command] — 复现命令
220
+ * @param {string} [extras.quality_proof_ref] — Quality Proof 证据引用
221
+ * @param {string} [extras.preservation_evidence] — 对既有状态守恒的独立证据
146
222
  * @param {string} [extras.deviation_detail] — 偏离说明(deviated 时)
147
223
  * @returns {{ filePath: string, round: number, deviated_count: number, should_escalate: boolean }}
148
224
  */
149
- export function createQuickVerification(verificationsDir, intentId, verdict, summary, extras = {}) {
150
- const timestamp = new Date().toISOString();
151
- // summary 填充四个维度的 evidence——快捷命令不要求 Agent 逐维度写
152
- const dimensions = {};
153
- for (const dim of REQUIRED_DIMENSIONS) {
154
- dimensions[dim] = { verdict, evidence: summary };
155
- }
156
- return writeVerification(verificationsDir, {
225
+ export function createQuickVerification(versionDir, verificationsDir, intentId, verdict, summary, extras = {}) {
226
+ const timestamp = new Date().toISOString();
227
+ const intent = getIntent(versionDir, intentId);
228
+ // summary 填充适用维度的 evidence——快捷命令不要求 Agent 逐维度写
229
+ const dimensions = {};
230
+ for (const dim of getRequiredDimensions(intent)) {
231
+ dimensions[dim] = {
232
+ verdict,
233
+ evidence: dim === 'preservation_achievement'
234
+ ? (extras.preservation_evidence || summary)
235
+ : summary,
236
+ };
237
+ }
238
+ if (extras.quality_proof_ref && dimensions.quality_achievement) {
239
+ dimensions.quality_achievement.quality_proof_ref = extras.quality_proof_ref;
240
+ }
241
+ return writeVerification(versionDir, verificationsDir, {
157
242
  intent_id: intentId,
158
243
  verdict,
159
244
  timestamp,
160
245
  summary,
161
- dimensions,
162
- reproduction_command: extras.reproduction_command || null,
163
- deviation_detail: extras.deviation_detail || null,
164
- });
165
- }
246
+ dimensions,
247
+ reproduction_command: extras.reproduction_command || null,
248
+ deviation_detail: extras.deviation_detail || null,
249
+ });
250
+ }
166
251
 
167
252
  /**
168
253
  * 返回所有待验证的 Intent(有实现产物但还没验证记录的)。
169
254
  * 需要传入 Intent Map 来判断哪些 Intent 是 in_progress。
170
255
  */
171
- export function getPendingVerifications(versionDir, verificationsDir) {
256
+ export function getPendingVerifications(versionDir, verificationsDir) {
172
257
  const intentMap = readJsonFile(join(versionDir, '04_INTENT_MAP.json'), 'Intent Map');
173
258
  const pending = [];
174
259
  for (const [id, intent] of Object.entries(intentMap.intents)) {
175
- if (intent.status === 'in_progress') {
176
- const hasRecord = existsSync(join(verificationsDir, `${id}.json`));
177
- if (!hasRecord) pending.push(id);
178
- }
260
+ if (intent.status === 'in_progress' || intent.status === 'needs_review') {
261
+ const history = getVerificationHistory(verificationsDir, id);
262
+ if (!hasCurrentPassedVerification(intent, history)) pending.push(id);
263
+ }
179
264
  }
180
265
  return pending;
181
- }
266
+ }
267
+
268
+ /** Missing Intent revisions are revision 1 without mutating the map. */
269
+ export function getEffectiveIntentRevision(intent) {
270
+ return intent.revision ?? 1;
271
+ }
272
+
273
+ /**
274
+ * Legacy records count as revision 1 only while the Intent itself is legacy.
275
+ * Once revision is explicit, an untagged record cannot prove freshness.
276
+ */
277
+ export function getVerificationIntentRevision(intent, record) {
278
+ if (Number.isInteger(record?.intent_revision) && record.intent_revision >= 1) {
279
+ return record.intent_revision;
280
+ }
281
+ return hasLegacyIntentRevision(intent) ? 1 : null;
282
+ }
283
+
284
+ export function isVerificationCurrent(intent, record) {
285
+ return getVerificationIntentRevision(intent, record) === getEffectiveIntentRevision(intent)
286
+ && getVerificationEpoch(intent, record) === getEffectiveVerificationEpoch(intent);
287
+ }
288
+
289
+ export function getVerificationEpoch(intent, record) {
290
+ if (Number.isInteger(record?.verification_epoch) && record.verification_epoch >= 1) {
291
+ return record.verification_epoch;
292
+ }
293
+ return intent?.verification_epoch === undefined ? 1 : null;
294
+ }
295
+
296
+ export function getLatestPassedVerification(history) {
297
+ if (!Array.isArray(history?.records)) return null;
298
+ return [...history.records].reverse().find((record) => record.verdict === 'passed') ?? null;
299
+ }
300
+
301
+ export function hasCurrentPassedVerification(intent, history) {
302
+ const latest = history?.records?.[history.records.length - 1];
303
+ return latest?.verdict === 'passed' && isVerificationCurrent(intent, latest);
304
+ }
182
305
 
183
306
  /**
184
307
  * 列出所有验证记录文件。
@@ -48,14 +48,15 @@ export function readCurrentPointer(loomRoot) {
48
48
  * @param {string} projectDir — 项目根目录
49
49
  * @returns {{ version: string, created: string[], skipped: string[] }}
50
50
  */
51
- export function newVersion(projectDir) {
52
- const loomRoot = join(projectDir, '.loom');
53
- const { versions } = listVersions(loomRoot);
51
+ export function newVersion(projectDir) {
52
+ const loomRoot = join(projectDir, '.loom');
53
+ const { versions } = listVersions(loomRoot);
54
+ const parentVersion = readCurrentPointer(loomRoot);
54
55
  const nextNum = versions.length === 0
55
56
  ? 1
56
57
  : parseInt(versions[versions.length - 1].slice(1)) + 1;
57
58
  const nextV = `v${nextNum}`;
58
- const result = createVersionStructure(projectDir, nextV);
59
+ const result = createVersionStructure(projectDir, nextV, parentVersion);
59
60
  // 自动切换为当前版本
60
61
  writeFileSync(join(loomRoot, 'current'), nextV, 'utf-8');
61
62
  result.created.push('.loom/current');