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 +2 -2
- package/dist/commands/sync.js +42 -2
- package/dist/templates/index.d.ts +1 -1
- package/dist/templates/index.js +1 -1
- package/dist/utils/scaffold.d.ts +7 -0
- package/dist/utils/scaffold.js +73 -11
- package/package.json +1 -1
- package/templates/claude-md-section.md +2 -2
- package/templates/domain_spec.md +1 -1
- package/templates/prompts/daily/domain_sync.md +2 -2
- package/templates/prompts/iterative/cold_batch_baseline.md +1 -1
- package/templates/rules.md +12 -0
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
|
-
| **项目级** | `
|
|
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
|
|
package/dist/commands/sync.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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", "
|
|
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>>;
|
package/dist/templates/index.js
CHANGED
package/dist/utils/scaffold.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/scaffold.js
CHANGED
|
@@ -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, '
|
|
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:
|
|
60
|
-
sourceId: 'template:
|
|
61
|
-
desiredContent: await loadTemplate('
|
|
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
|
|
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:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
@@ -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/
|
|
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
|
|
package/templates/domain_spec.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
3. 数据流变化 → §3
|
|
14
14
|
4. 新增接口 → §4
|
|
15
15
|
5. 数据模型变更 → §5
|
|
16
|
-
6. 新坑位 → §6 + 同步到
|
|
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 + 同步到
|
|
83
|
+
6. 新坑位 → §6 + 同步到 RULES.md
|
|
84
84
|
7. 00_context.md 中有而 domain_spec 中没有的信息 → 补充
|
|
85
85
|
请先输出每个域的回填草稿,等我审阅。
|
|
86
86
|
|