spec-canon 0.1.12 → 0.1.14

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 CHANGED
@@ -12,6 +12,8 @@ SpecCanon 是一套面向 AI Coding Agent(如 Claude Code)的 **Spec-Driven
12
12
 
13
13
  核心理念:**先文档后代码,人做判断题,AI 做填空题。**
14
14
 
15
+ AI 编程工具本质上是一个把结构化 Spec 转成实现代码的高效转换器。Spec 越清晰、越完整,AI 就越能稳定地产出与系统现状一致、与变更目标对齐的可运行代码。
16
+
15
17
  SpecCanon 的本质是一个**文档状态机**:每份 Spec 文档是一个状态节点,状态转移由人触发,AI 负责生成状态内容。通过持续维护 Domain Spec 作为系统行为的单一事实来源(Single Source of Truth),让 AI 在每次开发时都拥有准确的系统全景。
16
18
 
17
19
  ## 核心机制
@@ -50,14 +52,21 @@ domain_spec.md(演化更新)
50
52
 
51
53
  ## 安装
52
54
 
55
+ 你可以直接运行最新版,也可以先全局安装后再使用 `spec-canon`:
56
+
57
+ ```bash
58
+ npx spec-canon@latest init
59
+ ```
60
+
53
61
  ```bash
54
62
  npm install -g spec-canon
63
+ spec-canon init
55
64
  ```
56
65
 
57
- 或直接通过 `npx` 使用(无需全局安装):
66
+ 如果只想先查看命令,也可以运行:
58
67
 
59
68
  ```bash
60
- npx spec-canon --help
69
+ npx spec-canon@latest --help
61
70
  ```
62
71
 
63
72
  ## 快速开始
@@ -66,7 +75,7 @@ npx spec-canon --help
66
75
  → 阅读 [guide/04_new_project_sop.md](docs/guide/04_new_project_sop.md),然后使用 `spec-canon prompt guide` 查看决策树
67
76
 
68
77
  ### 我要在已有项目中开发新需求
69
- 先运行 `spec-canon change start --goal ...` 建立 active change(待命名状态),运行 `ctx` 后 AI 会建议 change 名,再用 `change start -g <goal> -c <name>` 确认命名;然后用 `spec-canon change next` 查看候选步骤
78
+ 若暂时还没想好 change 名,先运行 `spec-canon change start --goal ...` 建立 active change(待命名状态);运行 `ctx` 后 AI 会给出命名建议,再由你确认或修改后执行 `change start -g <goal> -c <name>` 记录命名。若一开始就已明确 change 名,且确认其中序号就是当前 next seq,也可直接使用 `-g -c`;然后用 `spec-canon change next` 查看候选步骤
70
79
 
71
80
  ### 我要为已有项目引入 SDD
72
81
  → 阅读 [guide/05_iterative_project_sop.md](docs/guide/05_iterative_project_sop.md) 的冷启动策略,然后使用 `spec-canon prompt list --stage iterative`
@@ -76,9 +85,11 @@ npx spec-canon --help
76
85
 
77
86
  ## 常用命令
78
87
 
88
+ 未全局安装时,把下面的 `spec-canon` 替换为 `npx spec-canon@latest`。
89
+
79
90
  ```bash
80
91
  spec-canon init
81
- spec-canon init --domain auth
92
+ spec-canon domain create auth
82
93
  spec-canon sync # 给已初始化项目安全补齐新骨架
83
94
  spec-canon change start -g @docs/prd.md
84
95
  spec-canon change status
@@ -104,7 +115,7 @@ docs/
104
115
  ├── SOP.md ← 导航索引(入口)
105
116
  ├── guide/ ← 方法论(一次性阅读)
106
117
  │ ├── 00_introduction.md SDD 框架综合介绍(对外发布用)
107
- │ ├── 01_claude_md.md CLAUDE.md 书写指南
118
+ │ ├── 01_claude_md.md AI 配置文件指南(CLAUDE.md / AGENTS.md)
108
119
  │ ├── 02_spec_overview.md 三层文档体系总览
109
120
  │ ├── 03_spec_documents.md 各文档详解
110
121
  │ ├── 04_new_project_sop.md 新项目 SOP
@@ -2,7 +2,7 @@ import { join, resolve } from 'node:path';
2
2
  import { ActiveChangeError, clearActiveChange, getActiveChangeProgress, ensureSddRoot, getActiveChangeStatus, getChangeDir, readActiveChange, writeActiveChange, } from '../utils/active-change.js';
3
3
  import { getChangeSeq, getChangeType, getNextChangeSeq, isValidChangeName, } from '../utils/change.js';
4
4
  import { buildChangeNextPlan } from '../utils/change-next.js';
5
- import { ensureDir } from '../utils/fs.js';
5
+ import { ensureDir, fileExists } from '../utils/fs.js';
6
6
  import { logger } from '../utils/logger.js';
7
7
  export function registerChangeCommand(program) {
8
8
  const change = program
@@ -68,6 +68,17 @@ async function runStart(opts, rootDir) {
68
68
  const isIdempotent = existing.goal === opts.goal &&
69
69
  existing.change === opts.change;
70
70
  if (canConfirmPendingChange) {
71
+ const requestedSeq = getChangeSeq(opts.change);
72
+ if (requestedSeq !== existing.changeSeq) {
73
+ logger.error(`当前 pending active change 的序号是 ${existing.changeSeq},请使用 <type>-${existing.changeSeq}-<slug>;若不确定 change 名,请先运行 spec-canon prompt show ctx`);
74
+ process.exitCode = 1;
75
+ return;
76
+ }
77
+ if (await fileExists(getChangeDir(opts.change, rootDir))) {
78
+ logger.error(`change 目录已存在:spec-canon/changes/${opts.change}。为避免复用旧 change,请改用新的 change 名,或先运行 spec-canon prompt show ctx`);
79
+ process.exitCode = 1;
80
+ return;
81
+ }
71
82
  await ensureDir(getChangeDir(opts.change, rootDir));
72
83
  await writeActiveChange({
73
84
  ...existing,
@@ -84,12 +95,23 @@ async function runStart(opts, rootDir) {
84
95
  process.exitCode = 1;
85
96
  return;
86
97
  }
98
+ const nextChangeSeq = await getNextChangeSeq(rootDir);
87
99
  const changeSeq = opts.change
88
100
  ? getChangeSeq(opts.change)
89
- : await getNextChangeSeq(rootDir);
101
+ : nextChangeSeq;
90
102
  const changesDir = join(rootDir, 'spec-canon', 'changes');
91
103
  await ensureDir(changesDir);
92
104
  if (opts.change) {
105
+ if (changeSeq !== nextChangeSeq) {
106
+ logger.error(`change 名中的序号 ${changeSeq} 不是当前可用序号 ${nextChangeSeq}。为避免复用旧 change,请先运行 spec-canon change start -g <goal>,再执行 spec-canon prompt show ctx`);
107
+ process.exitCode = 1;
108
+ return;
109
+ }
110
+ if (await fileExists(getChangeDir(opts.change, rootDir))) {
111
+ logger.error(`change 目录已存在:spec-canon/changes/${opts.change}。为避免复用旧 change,请改用新的 change 名,或先运行 spec-canon change start -g <goal> 再执行 spec-canon prompt show ctx`);
112
+ process.exitCode = 1;
113
+ return;
114
+ }
93
115
  await ensureDir(getChangeDir(opts.change, rootDir));
94
116
  }
95
117
  await writeActiveChange({
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerDomainCommand(program: Command): void;
@@ -0,0 +1,38 @@
1
+ import { resolve } from 'node:path';
2
+ import { createDomainScaffold, DomainScaffoldError, } from '../utils/scaffold.js';
3
+ import { logger } from '../utils/logger.js';
4
+ export function registerDomainCommand(program) {
5
+ const domain = program
6
+ .command('domain')
7
+ .description('管理业务域骨架')
8
+ .option('-d, --dir <path>', '指定 SDD 项目根目录(默认当前目录)');
9
+ domain
10
+ .command('create <name>')
11
+ .description('创建业务域骨架')
12
+ .action(async (name, _opts, command) => {
13
+ await runCreate(name, getRootDir(command));
14
+ });
15
+ }
16
+ async function runCreate(name, rootDir) {
17
+ try {
18
+ const result = await createDomainScaffold(rootDir, name);
19
+ logger.success(`已创建业务域: ${name}`);
20
+ logger.dim(` ${result.createdPath}`);
21
+ if (result.manifestPath) {
22
+ logger.info(`已更新模板状态清单:${result.manifestPath}`);
23
+ }
24
+ }
25
+ catch (error) {
26
+ if (error instanceof DomainScaffoldError) {
27
+ logger.error(error.message);
28
+ process.exitCode = 1;
29
+ return;
30
+ }
31
+ throw error;
32
+ }
33
+ }
34
+ function getRootDir(command) {
35
+ const parent = command.parent;
36
+ const opts = parent?.opts() ?? {};
37
+ return resolve(opts.dir ?? process.cwd());
38
+ }
@@ -2,13 +2,13 @@ import { join, resolve } from 'node:path';
2
2
  import { loadTemplate } from '../templates/index.js';
3
3
  import { fileExists, } from '../utils/fs.js';
4
4
  import { logger } from '../utils/logger.js';
5
- import { SDD_ROOT, TEMPLATE_MANIFEST, buildManagedFiles, ensureBaseScaffoldDirs, reconcileManagedFile, writeTemplateManifest, } from '../utils/scaffold.js';
5
+ import { createDomainScaffold, DomainScaffoldError, SDD_ROOT, TEMPLATE_MANIFEST, buildManagedFiles, ensureBaseScaffoldDirs, reconcileManagedFile, writeTemplateManifest, } from '../utils/scaffold.js';
6
6
  export function registerInitCommand(program) {
7
7
  program
8
8
  .command('init')
9
9
  .description('初始化 SDD 文档结构')
10
10
  .option('-d, --dir <path>', '目标项目目录', process.cwd())
11
- .option('--domain <name>', '同时创建初始业务域')
11
+ .option('--domain <name>', '兼容入口:同时创建初始业务域(推荐改用 spec-canon domain create)')
12
12
  .option('--force', '覆盖已存在的 spec-canon 目录', false)
13
13
  .action(async (opts) => {
14
14
  await runInit(opts);
@@ -31,10 +31,9 @@ async function runInit(opts) {
31
31
  await ensureBaseScaffoldDirs(targetDir);
32
32
  const managedFiles = await buildManagedFiles(targetDir, {
33
33
  includeClaude: true,
34
- extraDomains: opts.domain ? [opts.domain] : [],
35
34
  });
36
35
  for (const file of managedFiles) {
37
- const overwriteExisting = file.relativePath !== 'CLAUDE.md';
36
+ const overwriteExisting = file.relativePath !== 'CLAUDE.md' && file.relativePath !== 'AGENTS.md';
38
37
  const status = await reconcileManagedFile(targetDir, file, {
39
38
  overwriteExisting,
40
39
  });
@@ -43,21 +42,44 @@ async function runInit(opts) {
43
42
  created.push(file.relativePath);
44
43
  }
45
44
  }
45
+ const AI_CONFIG_FILES = ['CLAUDE.md', 'AGENTS.md'];
46
46
  const sddSection = await loadTemplate('claude-md-section.md');
47
- const claudeStatus = statuses.find((status) => status.relativePath === 'CLAUDE.md');
48
- if (claudeStatus?.state !== 'created') {
49
- logger.warn('CLAUDE.md 已存在,请手动将以下 SDD 协议段添加到文件中:');
47
+ const createdFiles = AI_CONFIG_FILES.filter((name) => statuses.find((s) => s.relativePath === name)?.state === 'created');
48
+ const existingFiles = AI_CONFIG_FILES.filter((name) => statuses.find((s) => s.relativePath === name)?.state !== 'created');
49
+ if (existingFiles.length > 0) {
50
+ logger.warn(`${existingFiles.join(' / ')} 已存在,请手动将以下 SDD 协议段添加到文件中:`);
50
51
  logger.blank();
51
52
  logger.dim('─'.repeat(60));
52
53
  console.log(sddSection);
53
54
  logger.dim('─'.repeat(60));
54
55
  logger.blank();
55
56
  }
56
- else {
57
- logger.success('已创建 CLAUDE.md(含 SDD 协议段)');
57
+ if (createdFiles.length > 0) {
58
+ logger.success(`已创建 ${createdFiles.join(' + ')}(含 SDD 协议段)`);
58
59
  }
59
60
  if (opts.domain) {
60
- logger.success(`已创建业务域: ${opts.domain}`);
61
+ try {
62
+ const result = await createDomainScaffold(targetDir, opts.domain, {
63
+ writeManifest: false,
64
+ allowExisting: opts.force,
65
+ overwriteExisting: opts.force,
66
+ });
67
+ if (result.status.state === 'created') {
68
+ created.push(result.createdPath);
69
+ }
70
+ statuses.push(result.status);
71
+ logger.success(result.status.state === 'created'
72
+ ? `已创建业务域: ${opts.domain}`
73
+ : `已刷新业务域: ${opts.domain}`);
74
+ }
75
+ catch (error) {
76
+ if (error instanceof DomainScaffoldError) {
77
+ logger.error(error.message);
78
+ process.exitCode = 1;
79
+ return;
80
+ }
81
+ throw error;
82
+ }
61
83
  }
62
84
  const manifestAbsolutePath = join(targetDir, SDD_ROOT, TEMPLATE_MANIFEST);
63
85
  const manifestExisted = await fileExists(manifestAbsolutePath);
@@ -75,5 +97,16 @@ async function runInit(opts) {
75
97
  logger.dim(` ${file}`);
76
98
  }
77
99
  logger.blank();
78
- logger.info('下一步:编辑 CLAUDE.md 填写项目信息,然后开始你的第一个变更');
100
+ logger.info(opts.domain
101
+ ? '下一步:编辑 CLAUDE.md(及 AGENTS.md)填写项目信息,然后开始你的第一个变更'
102
+ : `下一步:编辑 CLAUDE.md(及 AGENTS.md)填写项目信息;如需先建业务域,运行 ${formatDomainCreateCommand(targetDir)}`);
103
+ }
104
+ function formatDomainCreateCommand(targetDir) {
105
+ if (targetDir === process.cwd()) {
106
+ return 'spec-canon domain create <name>';
107
+ }
108
+ return `spec-canon domain -d ${quoteShellArg(targetDir)} create <name>`;
109
+ }
110
+ function quoteShellArg(value) {
111
+ return JSON.stringify(value);
79
112
  }
@@ -97,7 +97,7 @@ export function formatManualReviewItem(item, targetDir) {
97
97
  lines.push(` 建议命令: cp ${quotedTemplatePath} ${quotedTargetPath}`);
98
98
  }
99
99
  if (item.compareMode === 'merge-section') {
100
- lines.push(' CLAUDE.md 只需按需合并 SDD 协议段,无需整文件覆盖');
100
+ lines.push(` ${item.path} 只需按需合并 SDD 协议段,无需整文件覆盖`);
101
101
  }
102
102
  return lines;
103
103
  }
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import updateNotifier from 'update-notifier';
3
3
  import { registerChangeCommand } from './commands/change.js';
4
+ import { registerDomainCommand } from './commands/domain.js';
4
5
  import { registerInitCommand } from './commands/init.js';
5
6
  import { registerPromptCommand } from './commands/prompt.js';
6
7
  import { registerSyncCommand } from './commands/sync.js';
@@ -12,6 +13,7 @@ program
12
13
  .description('CLI toolkit for Spec-Driven Development (SDD)')
13
14
  .version(CLI_VERSION);
14
15
  registerInitCommand(program);
16
+ registerDomainCommand(program);
15
17
  registerChangeCommand(program);
16
18
  registerPromptCommand(program);
17
19
  registerSyncCommand(program);
@@ -47,6 +47,8 @@ export interface SyncReport {
47
47
  }
48
48
  export declare class ScaffoldSyncError extends Error {
49
49
  }
50
+ export declare class DomainScaffoldError extends Error {
51
+ }
50
52
  export declare function ensureBaseScaffoldDirs(rootDir: string): Promise<void>;
51
53
  export declare function loadClaudeSection(): Promise<string>;
52
54
  export declare function buildManagedFiles(rootDir: string, opts?: {
@@ -60,3 +62,12 @@ export declare function reconcileManagedFile(rootDir: string, file: ManagedFileD
60
62
  export declare function inspectManagedFile(rootDir: string, file: ManagedFileDefinition): Promise<ManagedFileStatus>;
61
63
  export declare function writeTemplateManifest(rootDir: string, files: ManagedFileDefinition[], statuses: ManagedFileStatus[]): Promise<string>;
62
64
  export declare function syncScaffold(rootDir: string): Promise<SyncReport>;
65
+ export declare function createDomainScaffold(rootDir: string, domain: string, opts?: {
66
+ writeManifest?: boolean;
67
+ allowExisting?: boolean;
68
+ overwriteExisting?: boolean;
69
+ }): Promise<{
70
+ createdPath: string;
71
+ status: ManagedFileStatus;
72
+ manifestPath?: string;
73
+ }>;
@@ -21,8 +21,12 @@ const BASE_DIRS = [
21
21
  ];
22
22
  const RULES_FILE = join(SDD_ROOT, 'rules', 'RULES.md');
23
23
  const LEGACY_RULES_FILE = join(SDD_ROOT, 'skills', 'SKILL.md');
24
+ const LEGACY_RULES_H1 = '# SKILLS.md — 团队规则库';
25
+ const RULES_H1 = '# RULES.md — 团队规则库';
24
26
  export class ScaffoldSyncError extends Error {
25
27
  }
28
+ export class DomainScaffoldError extends Error {
29
+ }
26
30
  export async function ensureBaseScaffoldDirs(rootDir) {
27
31
  for (const dir of BASE_DIRS) {
28
32
  await ensureDir(join(rootDir, dir));
@@ -69,10 +73,17 @@ export async function buildManagedFiles(rootDir, opts = {}) {
69
73
  manualReviewReason: 'spec-canon/domains/README.md 是持续演化的活文档,当前内容与 CLI 初始基线存在差异,已保留,可按需参考基线结构手动比对',
70
74
  });
71
75
  if (opts.includeClaude) {
76
+ const aiConfigContent = await buildClaudeMdContent();
77
+ addManagedFile(files, {
78
+ relativePath: 'AGENTS.md',
79
+ sourceId: 'generated:agents-md',
80
+ desiredContent: aiConfigContent,
81
+ manualReviewReason: 'AGENTS.md 是用户自定义项目说明文件,CLI 未自动改写;如需吸收新版 SDD 协议,请手动合并当前 CLI 提供的协议段基线',
82
+ });
72
83
  addManagedFile(files, {
73
84
  relativePath: 'CLAUDE.md',
74
85
  sourceId: 'generated:claude-md',
75
- desiredContent: await buildClaudeMdContent(),
86
+ desiredContent: aiConfigContent,
76
87
  manualReviewReason: 'CLAUDE.md 是用户自定义项目说明文件,CLI 未自动改写;如需吸收新版 SDD 协议,请手动合并当前 CLI 提供的协议段基线',
77
88
  });
78
89
  }
@@ -112,6 +123,16 @@ export async function reconcileManagedFile(rootDir, file, opts = {}) {
112
123
  }
113
124
  export async function inspectManagedFile(rootDir, file) {
114
125
  const absolutePath = join(rootDir, file.relativePath);
126
+ if (!(await fileExists(absolutePath))) {
127
+ return {
128
+ relativePath: file.relativePath,
129
+ sourceId: file.sourceId,
130
+ templateHash: hashContent(file.desiredContent),
131
+ fileHash: hashContent(''),
132
+ state: 'preserved',
133
+ manualReviewReason: file.manualReviewReason,
134
+ };
135
+ }
115
136
  const currentContent = await readFileSafe(absolutePath);
116
137
  const state = currentContent === file.desiredContent
117
138
  ? 'unchanged'
@@ -186,6 +207,35 @@ export async function syncScaffold(rootDir) {
186
207
  manifestPath,
187
208
  };
188
209
  }
210
+ export async function createDomainScaffold(rootDir, domain, opts = {}) {
211
+ const sddDir = join(rootDir, SDD_ROOT);
212
+ if (!(await fileExists(sddDir))) {
213
+ throw new DomainScaffoldError('未找到 spec-canon/ 目录,请先运行 spec-canon init');
214
+ }
215
+ const domainDir = join(rootDir, SDD_ROOT, 'domains', domain);
216
+ const domainExists = await fileExists(domainDir);
217
+ if (domainExists && !opts.allowExisting) {
218
+ throw new DomainScaffoldError(`业务域已存在:${domain}`);
219
+ }
220
+ const createdPath = join(SDD_ROOT, 'domains', domain, 'domain_spec.md');
221
+ const files = await buildManagedFiles(rootDir, {
222
+ includeClaude: true,
223
+ includeExistingDomains: true,
224
+ extraDomains: [domain],
225
+ });
226
+ const domainFile = files.find((file) => file.relativePath === createdPath);
227
+ if (!domainFile) {
228
+ throw new DomainScaffoldError(`未找到业务域骨架定义:${createdPath}`);
229
+ }
230
+ const status = await reconcileManagedFile(rootDir, domainFile, {
231
+ overwriteExisting: opts.overwriteExisting ?? false,
232
+ });
233
+ if (opts.writeManifest === false) {
234
+ return { createdPath, status };
235
+ }
236
+ const manifestPath = await writeTemplateManifest(rootDir, files, [status]);
237
+ return { createdPath, status, manifestPath };
238
+ }
189
239
  async function buildClaudeMdContent() {
190
240
  const sddSection = await loadClaudeSection();
191
241
  return `${DEFAULT_CLAUDE_MD}\n${sddSection}`;
@@ -218,7 +268,7 @@ function buildManualReviewItem(status) {
218
268
  item.compareMode = 'full-file';
219
269
  return item;
220
270
  }
221
- if (status.sourceId === 'generated:claude-md') {
271
+ if (status.sourceId === 'generated:claude-md' || status.sourceId === 'generated:agents-md') {
222
272
  item.sourceKind = 'baseline';
223
273
  item.templatePath = getTemplatePath('claude-md-section.md');
224
274
  item.compareMode = 'merge-section';
@@ -236,7 +286,8 @@ async function applyLegacyRulesCompatibility(rootDir) {
236
286
  }
237
287
  const rulesExists = await fileExists(rulesPath);
238
288
  if (!rulesExists) {
239
- await writeFileSafe(rulesPath, await readFileSafe(legacyRulesPath));
289
+ const legacyRulesContent = await readFileSafe(legacyRulesPath);
290
+ await writeFileSafe(rulesPath, normalizeLegacyRulesContent(legacyRulesContent));
240
291
  migrated.push({
241
292
  path: RULES_FILE,
242
293
  fromPath: LEGACY_RULES_FILE,
@@ -251,21 +302,26 @@ async function applyLegacyRulesCompatibility(rootDir) {
251
302
  compareMode: 'full-file',
252
303
  });
253
304
  }
254
- const claudePath = join(rootDir, 'CLAUDE.md');
255
- if (!(await fileExists(claudePath))) {
256
- return { migrated, manualReview };
257
- }
258
- const claudeContent = await readFileSafe(claudePath);
259
- if (!containsLegacyRulesReference(claudeContent)) {
260
- return { migrated, manualReview };
305
+ const aiConfigFiles = [
306
+ { path: 'CLAUDE.md', name: 'CLAUDE.md' },
307
+ { path: 'AGENTS.md', name: 'AGENTS.md' },
308
+ ];
309
+ for (const file of aiConfigFiles) {
310
+ const filePath = join(rootDir, file.path);
311
+ if (!(await fileExists(filePath))) {
312
+ continue;
313
+ }
314
+ const content = await readFileSafe(filePath);
315
+ if (containsLegacyRulesReference(content)) {
316
+ manualReview.push({
317
+ path: file.name,
318
+ reason: `${file.name} 中仍引用旧规则路径,请将 spec-canon/skills/SKILL.md 更新为 spec-canon/rules/RULES.md`,
319
+ sourceKind: 'baseline',
320
+ templatePath: getTemplatePath('claude-md-section.md'),
321
+ compareMode: 'merge-section',
322
+ });
323
+ }
261
324
  }
262
- manualReview.push({
263
- path: 'CLAUDE.md',
264
- reason: 'CLAUDE.md 中仍引用旧规则路径,请将 spec-canon/skills/SKILL.md 更新为 spec-canon/rules/RULES.md',
265
- sourceKind: 'baseline',
266
- templatePath: getTemplatePath('claude-md-section.md'),
267
- compareMode: 'merge-section',
268
- });
269
325
  return { migrated, manualReview };
270
326
  }
271
327
  function containsLegacyRulesReference(content) {
@@ -275,6 +331,16 @@ function containsLegacyRulesReference(content) {
275
331
  'spec-canon/skills/SKILL.md',
276
332
  ].some((pattern) => content.includes(pattern));
277
333
  }
334
+ function normalizeLegacyRulesContent(content) {
335
+ if (content === LEGACY_RULES_H1) {
336
+ return RULES_H1;
337
+ }
338
+ const legacyHeadingWithNewline = `${LEGACY_RULES_H1}\n`;
339
+ if (content.startsWith(legacyHeadingWithNewline)) {
340
+ return `${RULES_H1}${content.slice(LEGACY_RULES_H1.length)}`;
341
+ }
342
+ return content;
343
+ }
278
344
  function hashContent(content) {
279
345
  return createHash('sha256').update(content).digest('hex');
280
346
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-canon",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "CLI toolkit for Spec-Driven Development (SDD)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -3,6 +3,7 @@
3
3
  ### 角色定义
4
4
  你是一名遵循 Spec-Driven Development 协议的高级工程师。
5
5
  核心原则:**先文档后代码,人做判断题,AI 做填空题。**
6
+ AI 编程工具本质上是把结构化 Spec 转成实现代码的高效转换器。编码时应以 Spec 为主输入,不应以零散对话替代;若 Spec 不清晰,先补齐或澄清,再进入实现。
6
7
 
7
8
  ### 核心规则
8
9
  - **Context First**:编码前,必须先阅读 `spec-canon/` 下对应的 Spec 文件
@@ -65,9 +66,10 @@ domain_spec 生命周期:变更启动时由 AI 从 change goal 出发,结合
65
66
 
66
67
  ```bash
67
68
  spec-canon sync # 给已初始化项目安全补齐 SDD 骨架
69
+ spec-canon domain create checkin # 显式创建业务域骨架
68
70
  spec-canon change start -g @docs/prd.md # 建立当前 active change(待命名)
69
- # → 运行 ctx 后 AI 建议 change 名
70
- spec-canon change start -g @docs/prd.md -c feat-001-checkin # 确认命名
71
+ # → 运行 ctx 后 AI 会给出命名建议;由你确认或修改后再记录
72
+ spec-canon change start -g @docs/prd.md -c feat-001-checkin # 确认命名;若名称已明确且序号就是当前 next seq,也可首次直接使用
71
73
  spec-canon change status # 查看当前进度与推荐下一步
72
74
  spec-canon change next # 查看当前候选 next prompts
73
75
  spec-canon change -d /path status # 指定目标项目目录
@@ -21,12 +21,13 @@
21
21
 
22
22
  ## 全新项目
23
23
 
24
- 1. 运行 `spec-canon init --domain <首个业务域名>` 初始化项目结构
25
- 2. 使用 `change start --goal -c <name>` 建立 active change(新项目跳过 ctx,直接命名)
26
- 3. 可先用 `change next` 查看当前候选步骤
27
- 4. 使用 `req` 生成 01_requirement.md(人工审阅 AC 并 Sign-off)
28
- 5. 依次使用 `iface` `impl-spec` `test-spec` → `impl` → `review` 完成首个变更
29
- 6. 使用 `domain-sync` 创建首份 domain_spec(人工审阅后写入;可省略 `-d` 自动发现受影响域)
24
+ 1. 运行 `spec-canon init` 初始化项目结构
25
+ 2. 运行 `spec-canon domain create <首个业务域名>` 创建首个业务域
26
+ 3. 使用 `change start --goal -c <name>` 建立 active change(新项目跳过 ctx,首个 change 通常可直接命名)
27
+ 4. 可先用 `change next` 查看当前候选步骤
28
+ 5. 使用 `req` 生成 01_requirement.md(人工审阅 AC 并 Sign-off)
29
+ 6. 依次使用 `iface` → `impl-spec` `test-spec` `impl` → `review` 完成首个变更
30
+ 7. 使用 `domain-sync` 创建首份 domain_spec(人工审阅后写入;可省略 `-d` 自动发现受影响域)
30
31
 
31
32
  > 新项目没有既有系统,00_context(系统现状)无需生成。架构决策自然归入 `impl-spec`(03_implementation)的 Design Decision 段。
32
33
 
@@ -39,8 +40,8 @@
39
40
  1. 运行 `spec-canon init` 初始化结构
40
41
  2. 使用 `change start --goal` 建立 active change
41
42
  3. 可先用 `change next` 查看当前候选步骤
42
- 4. 使用 `ctx` 生成 00_context.md,AI 自动识别主域并建议 change
43
- 5. 用 `change start -g <goal> -c <name>` 确认命名
43
+ 4. 使用 `ctx` 生成 00_context.md,AI 自动识别主域并给出 change 名建议
44
+ 5. 用 `change start -g <goal> -c <name>` 记录确认后的 change 名(可采纳或修改 AI 建议)
44
45
  6. 按日常流程完成变更(req → iface → impl-spec → test-spec → impl → review)
45
46
  7. 在 Archive 步骤使用 `domain-sync` 创建首份 domain_spec(可省略 `-d` 自动发现受影响域)
46
47
 
@@ -70,7 +71,7 @@
70
71
  | Step -1 | `change start --goal` | 建立 active change | 确认 goal 描述准确;若未确认 change 名,先接受待命名状态 |
71
72
  | Step -0.5 | `change next` | 查看候选 next prompts | 关注默认宽松模式下保留的 `iface` / `domain-sync` 候选,自己做判断 |
72
73
  | Step 0 | `ctx` | 生成 00_context.md | 审阅系统现状快照,确认 AI 自动识别的域、模块和待补充项 |
73
- | Step 0.5 | `change start -g <goal> -c <name>` | 确认 change 名 | AI ctx 中建议的名称是否准确 |
74
+ | Step 0.5 | `change start -g <goal> -c <name>` | 确认 change 名 | 采纳或修改 ctx 给出的建议;若名称已明确且序号就是当前 next seq,也可一开始直接用 `-g -c` |
74
75
  | Step 1 | `req` | 生成 01_requirement.md | 审阅 AC,Sign-off |
75
76
  | Step 2 | `iface` → `impl-spec` → `test-spec` | 生成 02/03/04 文档 | 审阅文件路径、步骤顺序 |
76
77
  | Step 3 | `impl` | 分步编码 | 逐步确认 |