spec-canon 0.1.11 → 0.1.12

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
@@ -23,7 +23,7 @@ SpecCanon 的本质是一个**文档状态机**:每份 Spec 文档是一个状
23
23
  | **系统级** | `domains/README.md` | 持续更新 | 域间关系全景 |
24
24
  | **系统级** | `domains/<domain>/domain_spec.md` | 持续累积 | 系统当前行为(Canon) |
25
25
  | **Change 级** | `change-xxx/00_context~04_test_spec` | 单次生命周期 | 本次变更做了什么 |
26
- | **项目级** | `SKILL.md` / `AI_CHANGELOG.md` | 持续累积 | 团队学到了什么 |
26
+ | **项目级** | `RULES.md` / `AI_CHANGELOG.md` | 持续累积 | 团队学到了什么 |
27
27
 
28
28
  ### 闭环演化
29
29
 
@@ -94,7 +94,7 @@ spec-canon prompt guide
94
94
 
95
95
  `change` 还支持父命令选项 `-d, --dir <path>` 指定目标项目目录,默认当前目录。由于 `change next` 已用 `-d` 表示 `--domain`,指定目录时请写在子命令前:`spec-canon change -d /path/to/project next`。
96
96
 
97
- `sync` 会在项目内写入 `spec-canon/.template-manifest.json`,用于记录当前骨架状态,为后续更智能的升级能力预留基础。若有文件需要人工比对,CLI 会同时输出参考模板路径和建议的 `code --diff` / `diff -u` 命令,方便直接对照已安装包中的模板。
97
+ `sync` 会在项目内写入 `spec-canon/.template-manifest.json`,用于记录当前骨架状态,为后续更智能的升级能力预留基础。若有文件需要人工比对,CLI 会同时输出参考模板路径和建议的 `code --diff` / `diff -u` 命令,方便直接对照已安装包中的模板;对于 `spec-canon/templates/` 下的 6 个核心 Spec 模板,还会额外给出可直接覆盖的 `cp` 命令。
98
98
 
99
99
  ## 文档结构
100
100
 
@@ -1,6 +1,14 @@
1
1
  import { join, resolve } from 'node:path';
2
2
  import { ScaffoldSyncError, syncScaffold, } from '../utils/scaffold.js';
3
3
  import { logger } from '../utils/logger.js';
4
+ const COPYABLE_TEMPLATE_PATHS = new Set([
5
+ 'spec-canon/templates/00_context.md',
6
+ 'spec-canon/templates/01_requirement.md',
7
+ 'spec-canon/templates/02_interface.md',
8
+ 'spec-canon/templates/03_implementation.md',
9
+ 'spec-canon/templates/04_test_spec.md',
10
+ 'spec-canon/templates/domain_spec.md',
11
+ ]);
4
12
  export function registerSyncCommand(program) {
5
13
  program
6
14
  .command('sync')
@@ -19,6 +27,7 @@ async function runSync(opts) {
19
27
  logger.success('SDD 同步完成');
20
28
  logger.blank();
21
29
  console.log(`created: ${report.created.length}`);
30
+ console.log(`migrated: ${report.migrated.length}`);
22
31
  console.log(`up_to_date: ${report.upToDate.length}`);
23
32
  console.log(`preserved: ${report.preserved.length}`);
24
33
  console.log(`manual_review: ${report.manualReview.length}`);
@@ -30,6 +39,14 @@ async function runSync(opts) {
30
39
  }
31
40
  logger.blank();
32
41
  }
42
+ if (report.migrated.length > 0) {
43
+ logger.success('已迁移文件:');
44
+ for (const item of report.migrated) {
45
+ logger.dim(` ${item.fromPath} -> ${item.path}`);
46
+ logger.dim(` ${item.reason}`);
47
+ }
48
+ logger.blank();
49
+ }
33
50
  if (report.manualReview.length > 0) {
34
51
  logger.warn('需要人工比对:');
35
52
  for (const line of formatManualReviewSection(report.manualReview, targetDir)) {
@@ -54,16 +71,33 @@ export function formatManualReviewItem(item, targetDir) {
54
71
  ` ${item.reason}`,
55
72
  ];
56
73
  if (!item.templatePath) {
74
+ if (!item.referencePath) {
75
+ return lines;
76
+ }
77
+ const targetPath = join(targetDir, item.path);
78
+ const quotedReferencePath = quoteShellArg(item.referencePath);
79
+ const quotedTargetPath = quoteShellArg(targetPath);
80
+ lines.push(` 参考文件: ${item.referencePath}`);
81
+ lines.push(` 建议命令: code --diff ${quotedReferencePath} ${quotedTargetPath}`);
82
+ lines.push(` 建议命令: diff -u ${quotedReferencePath} ${quotedTargetPath}`);
57
83
  return lines;
58
84
  }
59
85
  const targetPath = join(targetDir, item.path);
60
86
  const quotedTemplatePath = quoteShellArg(item.templatePath);
61
87
  const quotedTargetPath = quoteShellArg(targetPath);
62
- lines.push(` 参考模板: ${item.templatePath}`);
88
+ const sourceLabel = item.compareMode === 'merge-section'
89
+ ? '当前 CLI SDD 协议段基线'
90
+ : item.sourceKind === 'baseline'
91
+ ? '当前 CLI 基线'
92
+ : '参考模板';
93
+ lines.push(` ${sourceLabel}: ${item.templatePath}`);
63
94
  lines.push(` 建议命令: code --diff ${quotedTemplatePath} ${quotedTargetPath}`);
64
95
  lines.push(` 建议命令: diff -u ${quotedTemplatePath} ${quotedTargetPath}`);
96
+ if (shouldSuggestTemplateCopy(item)) {
97
+ lines.push(` 建议命令: cp ${quotedTemplatePath} ${quotedTargetPath}`);
98
+ }
65
99
  if (item.compareMode === 'merge-section') {
66
- lines.push(' CLAUDE.md 只需合并 SDD 协议段,无需整文件覆盖');
100
+ lines.push(' CLAUDE.md 只需按需合并 SDD 协议段,无需整文件覆盖');
67
101
  }
68
102
  return lines;
69
103
  }
@@ -80,3 +114,9 @@ export function formatManualReviewSection(items, targetDir) {
80
114
  function quoteShellArg(value) {
81
115
  return JSON.stringify(value);
82
116
  }
117
+ function shouldSuggestTemplateCopy(item) {
118
+ return item.sourceKind === 'template' &&
119
+ item.compareMode === 'full-file' &&
120
+ item.templatePath !== undefined &&
121
+ COPYABLE_TEMPLATE_PATHS.has(item.path);
122
+ }
@@ -3,5 +3,5 @@ export declare function loadTemplate(name: string): Promise<string>;
3
3
  /** 7 个 Change Spec 模板 */
4
4
  export declare const SPEC_TEMPLATES: readonly ["00_context.md", "01_requirement.md", "02_interface.md", "03_implementation.md", "04_test_spec.md", "domain_spec.md", "AI_CHANGELOG.md"];
5
5
  /** CLI 独有模板 */
6
- export declare const CLI_TEMPLATES: readonly ["claude-md-section.md", "domains-readme.md", "skill.md"];
6
+ export declare const CLI_TEMPLATES: readonly ["claude-md-section.md", "domains-readme.md", "rules.md"];
7
7
  export declare function loadAllSpecTemplates(): Promise<Map<string, string>>;
@@ -24,7 +24,7 @@ export const SPEC_TEMPLATES = [
24
24
  export const CLI_TEMPLATES = [
25
25
  'claude-md-section.md',
26
26
  'domains-readme.md',
27
- 'skill.md',
27
+ 'rules.md',
28
28
  ];
29
29
  export async function loadAllSpecTemplates() {
30
30
  const map = new Map();
@@ -28,12 +28,19 @@ export interface TemplateManifest {
28
28
  }
29
29
  export interface SyncReport {
30
30
  created: string[];
31
+ migrated: Array<{
32
+ path: string;
33
+ fromPath: string;
34
+ reason: string;
35
+ }>;
31
36
  upToDate: string[];
32
37
  preserved: string[];
33
38
  manualReview: Array<{
34
39
  path: string;
35
40
  reason: string;
41
+ sourceKind?: 'template' | 'baseline';
36
42
  templatePath?: string;
43
+ referencePath?: string;
37
44
  compareMode?: 'full-file' | 'merge-section';
38
45
  }>;
39
46
  manifestPath: string;
@@ -16,9 +16,11 @@ const BASE_DIRS = [
16
16
  join(SDD_ROOT, 'domains'),
17
17
  join(SDD_ROOT, 'changes'),
18
18
  join(SDD_ROOT, 'decisions'),
19
- join(SDD_ROOT, 'skills'),
19
+ join(SDD_ROOT, 'rules'),
20
20
  join(SDD_ROOT, 'templates'),
21
21
  ];
22
+ const RULES_FILE = join(SDD_ROOT, 'rules', 'RULES.md');
23
+ const LEGACY_RULES_FILE = join(SDD_ROOT, 'skills', 'SKILL.md');
22
24
  export class ScaffoldSyncError extends Error {
23
25
  }
24
26
  export async function ensureBaseScaffoldDirs(rootDir) {
@@ -56,22 +58,22 @@ export async function buildManagedFiles(rootDir, opts = {}) {
56
58
  desiredContent: await loadTemplate('AI_CHANGELOG.md'),
57
59
  });
58
60
  addManagedFile(files, {
59
- relativePath: join(SDD_ROOT, 'skills', 'SKILL.md'),
60
- sourceId: 'template:skill.md',
61
- desiredContent: await loadTemplate('skill.md'),
61
+ relativePath: RULES_FILE,
62
+ sourceId: 'template:rules.md',
63
+ desiredContent: await loadTemplate('rules.md'),
62
64
  });
63
65
  addManagedFile(files, {
64
66
  relativePath: join(SDD_ROOT, 'domains', 'README.md'),
65
67
  sourceId: 'template:domains-readme.md',
66
68
  desiredContent: await loadTemplate('domains-readme.md'),
67
- manualReviewReason: 'spec-canon/domains/README.md 与当前模板不同,已保留,请手动比对',
69
+ manualReviewReason: 'spec-canon/domains/README.md 是持续演化的活文档,当前内容与 CLI 初始基线存在差异,已保留,可按需参考基线结构手动比对',
68
70
  });
69
71
  if (opts.includeClaude) {
70
72
  addManagedFile(files, {
71
73
  relativePath: 'CLAUDE.md',
72
74
  sourceId: 'generated:claude-md',
73
75
  desiredContent: await buildClaudeMdContent(),
74
- manualReviewReason: 'CLAUDE.md 已存在,未自动同步 SDD 协议段,请手动比对',
76
+ manualReviewReason: 'CLAUDE.md 是用户自定义项目说明文件,CLI 未自动改写;如需吸收新版 SDD 协议,请手动合并当前 CLI 提供的协议段基线',
75
77
  });
76
78
  }
77
79
  const domains = new Set(opts.extraDomains ?? []);
@@ -151,6 +153,7 @@ export async function syncScaffold(rootDir) {
151
153
  throw new ScaffoldSyncError('未找到 spec-canon/ 目录,请先运行 spec-canon init');
152
154
  }
153
155
  await ensureBaseScaffoldDirs(rootDir);
156
+ const legacyCompatibility = await applyLegacyRulesCompatibility(rootDir);
154
157
  const files = await buildManagedFiles(rootDir, {
155
158
  includeClaude: true,
156
159
  includeExistingDomains: true,
@@ -166,16 +169,20 @@ export async function syncScaffold(rootDir) {
166
169
  created: statuses
167
170
  .filter((status) => status.state === 'created')
168
171
  .map((status) => status.relativePath),
172
+ migrated: legacyCompatibility.migrated,
169
173
  upToDate: statuses
170
174
  .filter((status) => status.state === 'unchanged')
171
175
  .map((status) => status.relativePath),
172
176
  preserved: statuses
173
177
  .filter((status) => status.state === 'preserved')
174
178
  .map((status) => status.relativePath),
175
- manualReview: statuses
176
- .filter((status) => status.state === 'preserved' &&
177
- status.manualReviewReason !== undefined)
178
- .map((status) => buildManualReviewItem(status)),
179
+ manualReview: [
180
+ ...statuses
181
+ .filter((status) => status.state === 'preserved' &&
182
+ status.manualReviewReason !== undefined)
183
+ .map((status) => buildManualReviewItem(status)),
184
+ ...legacyCompatibility.manualReview,
185
+ ],
179
186
  manifestPath,
180
187
  };
181
188
  }
@@ -203,16 +210,71 @@ function buildManualReviewItem(status) {
203
210
  reason: status.manualReviewReason,
204
211
  };
205
212
  if (status.sourceId.startsWith('template:')) {
206
- item.templatePath = getTemplatePath(status.sourceId.slice('template:'.length));
213
+ const templateName = status.sourceId.slice('template:'.length);
214
+ item.sourceKind = templateName === 'domains-readme.md'
215
+ ? 'baseline'
216
+ : 'template';
217
+ item.templatePath = getTemplatePath(templateName);
207
218
  item.compareMode = 'full-file';
208
219
  return item;
209
220
  }
210
221
  if (status.sourceId === 'generated:claude-md') {
222
+ item.sourceKind = 'baseline';
211
223
  item.templatePath = getTemplatePath('claude-md-section.md');
212
224
  item.compareMode = 'merge-section';
213
225
  }
214
226
  return item;
215
227
  }
228
+ async function applyLegacyRulesCompatibility(rootDir) {
229
+ const migrated = [];
230
+ const manualReview = [];
231
+ const legacyRulesPath = join(rootDir, LEGACY_RULES_FILE);
232
+ const rulesPath = join(rootDir, RULES_FILE);
233
+ const legacyRulesExists = await fileExists(legacyRulesPath);
234
+ if (!legacyRulesExists) {
235
+ return { migrated, manualReview };
236
+ }
237
+ const rulesExists = await fileExists(rulesPath);
238
+ if (!rulesExists) {
239
+ await writeFileSafe(rulesPath, await readFileSafe(legacyRulesPath));
240
+ migrated.push({
241
+ path: RULES_FILE,
242
+ fromPath: LEGACY_RULES_FILE,
243
+ reason: '已从 spec-canon/skills/SKILL.md 复制内容到 spec-canon/rules/RULES.md,请确认后手动删除旧文件',
244
+ });
245
+ }
246
+ else {
247
+ manualReview.push({
248
+ path: RULES_FILE,
249
+ reason: '检测到 spec-canon/skills/SKILL.md 与 spec-canon/rules/RULES.md 同时存在,CLI 未自动合并,请手动比对并在确认后删除旧文件',
250
+ referencePath: legacyRulesPath,
251
+ compareMode: 'full-file',
252
+ });
253
+ }
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 };
261
+ }
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
+ return { migrated, manualReview };
270
+ }
271
+ function containsLegacyRulesReference(content) {
272
+ return [
273
+ 'SKILL.md',
274
+ 'spec-canon/skills/',
275
+ 'spec-canon/skills/SKILL.md',
276
+ ].some((pattern) => content.includes(pattern));
277
+ }
216
278
  function hashContent(content) {
217
279
  return createHash('sha256').update(content).digest('hex');
218
280
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-canon",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "CLI toolkit for Spec-Driven Development (SDD)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,7 +19,7 @@
19
19
  | 系统级 | `spec-canon/domains/<domain>/domain_spec.md` | 持续累积 | 系统当前行为(Canon) |
20
20
  | Change 级 | `spec-canon/changes/<change>/00~04` | 单次生命周期 | 本次变更做了什么 |
21
21
  | 项目级 | `spec-canon/decisions/AI_CHANGELOG.md` | 持续累积 | 技术决策留痕 |
22
- | 项目级 | `spec-canon/skills/SKILL.md` | 持续累积 | 团队规则库(开始任务前先阅读) |
22
+ | 项目级 | `spec-canon/rules/RULES.md` | 持续累积 | 团队规则库(开始任务前先阅读) |
23
23
 
24
24
  Change Spec 生成顺序(后者依赖前者作为上下文):
25
25
  `00_context → 01_requirement → 02_interface → 03_implementation → 04_test_spec`
@@ -78,7 +78,7 @@ spec-canon prompt show <id> [选项] # 输出指定提示词(自动替换
78
78
  # 示例: spec-canon prompt show req
79
79
  ```
80
80
 
81
- 若 `spec-canon` 版本升级后新增了骨架文件,优先运行 `spec-canon sync` 补齐缺失内容;该命令默认不会覆盖你已编辑过的文件。
81
+ 若 `spec-canon` 版本升级后新增了骨架文件,优先运行 `spec-canon sync` 补齐缺失内容;该命令默认不会覆盖你已编辑过的文件。若 `spec-canon/templates/` 下的 6 个核心 Spec 模板与当前版本不一致,CLI 会同时给出 `code --diff` / `diff -u` / `cp` 建议命令,便于确认后直接同步。
82
82
 
83
83
  `change next` 默认会保留 `iface`、`domain-sync` 等候选,让你自己选择 next;如确认不需要,可用 `--no-contract-change`、`--no-system-change` 收窄。
84
84
 
@@ -35,4 +35,4 @@ stateDiagram-v2
35
35
 
36
36
  ## 6. 已知约束与坑位(Known Constraints)
37
37
  - [约束/坑位描述]
38
- (来源: feat-001 开发过程 / SKILL.md)
38
+ (来源: feat-001 开发过程 / RULES.md)
@@ -13,7 +13,7 @@
13
13
  3. 数据流变化 → §3
14
14
  4. 新增接口 → §4
15
15
  5. 数据模型变更 → §5
16
- 6. 新坑位 → §6 + 同步到 SKILL.md
16
+ 6. 新坑位 → §6 + 同步到 RULES.md
17
17
  7. 00_context.md 中有而 domain_spec 中没有的信息 → 补充
18
18
  请先输出回填草稿,等我审阅。
19
19
 
@@ -80,7 +80,7 @@
80
80
  3. 数据流变化 → §3
81
81
  4. 新增接口 → §4
82
82
  5. 数据模型变更 → §5
83
- 6. 新坑位 → §6 + 同步到 SKILL.md
83
+ 6. 新坑位 → §6 + 同步到 RULES.md
84
84
  7. 00_context.md 中有而 domain_spec 中没有的信息 → 补充
85
85
  请先输出每个域的回填草稿,等我审阅。
86
86
 
@@ -8,7 +8,7 @@
8
8
  须标注 README.md 中记录的跨域调用关系(上游谁调用本域、本域调用哪些下游)
9
9
  4. §4 接口清单:从路由/Controller 文件中提取完整列表
10
10
  5. §5 数据模型:从 Entity/Migration 文件中提取
11
- 6. §6 已知约束:从代码注释、TODO、FIXME、SKILL.md 中提取
11
+ 6. §6 已知约束:从代码注释、TODO、FIXME、RULES.md 中提取
12
12
  7. 对每条信息标注置信度:
13
13
  ✅ 确定(代码中有明确逻辑)
14
14
  ⚠️ 推测(从代码模式推断)
@@ -0,0 +1,12 @@
1
+ # RULES.md — 团队规则库
2
+
3
+ > 项目积累的规则和经验。每次发现 AI 重复犯错的 pattern,追加为新规则。
4
+
5
+ ## 规则清单
6
+
7
+ <!-- 格式示例:
8
+ ### R-001: [规则标题]
9
+ - **场景**:[什么情况下触发]
10
+ - **规则**:[具体要求]
11
+ - **来源**:[哪个变更开发中发现]
12
+ -->