spec-canon 0.1.15 → 0.1.17
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 +8 -1
- package/dist/commands/change.js +20 -6
- package/dist/commands/prompt.js +3 -2
- package/dist/prompts/catalog.d.ts +1 -0
- package/dist/prompts/catalog.js +5 -0
- package/dist/prompts/include.d.ts +1 -0
- package/dist/prompts/include.js +35 -0
- package/dist/utils/change-next.d.ts +6 -1
- package/dist/utils/change-next.js +109 -36
- package/dist/utils/change.d.ts +2 -0
- package/dist/utils/change.js +14 -0
- package/package.json +3 -2
- package/templates/00_context.md +38 -27
- package/templates/01_requirement.md +35 -13
- package/templates/02_interface.md +31 -28
- package/templates/03_implementation.md +52 -54
- package/templates/04_test_spec.md +29 -14
- package/templates/claude-md-section.md +7 -1
- package/templates/prompts/daily/step0_context.md +9 -0
- package/templates/prompts/daily/step1_requirement.md +16 -0
- package/templates/prompts/daily/step2_implementation.md +18 -3
- package/templates/prompts/daily/step2_interface.md +17 -0
- package/templates/prompts/daily/step2_test_spec.md +17 -0
- package/templates/prompts/daily/step3_execute.md +19 -8
- package/templates/prompts/guide.md +5 -0
- package/templates/prompts/shared/spark_protocol.md +25 -0
package/README.md
CHANGED
|
@@ -101,12 +101,18 @@ spec-canon prompt show <id>
|
|
|
101
101
|
spec-canon prompt guide
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
`change next` 会根据当前 active change
|
|
104
|
+
`change next` 会根据当前 active change 的文档进度和变更规模输出候选步骤列表。用 `--scale small|medium|complex` 指定规模:small≈`req→impl→review`、medium 增加 `iface`、complex 才铺开 `ctx/impl-spec/test-spec/domain-sync`。省略 `--scale` 时按 type 推默认(fix/chore/docs/style/ci/test→small,feat/refactor/perf→medium,complex 不自动推导)。在规模基础上还可用 `--no-contract-change`、`--no-system-change`、`-d <domain>` 进一步收窄。
|
|
105
|
+
|
|
106
|
+
`ctx` 内置 Explore project context preflight:生成 `00_context` 前会先探索项目入口、SDD 文档、历史决策、近期提交和真实代码入口;只有 goal 过大或无法定位主域时才暂停澄清,普通证据缺口会写入“待补充 / 待确认”。
|
|
107
|
+
|
|
108
|
+
`req`、`iface`、`impl-spec`、`test-spec` 内置 spark gate:生成正式 Spec 前会先检查是否存在关键澄清点或多方案决策。若存在,提示词会要求 AI 暂停写文件,先提出问题或方案比较,等待人工确认后再继续;若输入已经明确,则直接生成目标 Spec。
|
|
105
109
|
|
|
106
110
|
`change` 还支持父命令选项 `-d, --dir <path>` 指定目标项目目录,默认当前目录。由于 `change next` 已用 `-d` 表示 `--domain`,指定目录时请写在子命令前:`spec-canon change -d /path/to/project next`。
|
|
107
111
|
|
|
108
112
|
`sync` 会在项目内写入 `spec-canon/.template-manifest.json`,用于记录当前骨架状态,为后续更智能的升级能力预留基础。若有文件需要人工比对,CLI 会同时输出参考模板路径和建议的 `code --diff` / `diff -u` 命令,方便直接对照已安装包中的模板;对于 `spec-canon/templates/` 下的 6 个核心 Spec 模板,还会额外给出可直接覆盖的 `cp` 命令。
|
|
109
113
|
|
|
114
|
+
仓库内额外提供了实验性 `skills/` 原型,用于把“CLI 输出 prompt 再复制粘贴给 agent”的动作收敛为 agent 内部直接调用 CLI 并执行。它们目前仅用于内部验证,不属于稳定 CLI 接口。
|
|
115
|
+
|
|
110
116
|
## 文档结构
|
|
111
117
|
|
|
112
118
|
```
|
|
@@ -124,6 +130,7 @@ docs/
|
|
|
124
130
|
├── prompts/ ← 提示词文件(CLI `spec-canon prompt` 数据源)
|
|
125
131
|
├── reference/ ← 速查表(随时查阅)
|
|
126
132
|
└── templates/ ← Spec 模板(7 个)
|
|
133
|
+
skills/ ← 实验性 SDD skills(内部验证)
|
|
127
134
|
```
|
|
128
135
|
|
|
129
136
|
## ROADMAP
|
package/dist/commands/change.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readdir, stat } from 'node:fs/promises';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { ActiveChangeError, clearActiveChange, getActiveChangeProgress, ensureSddRoot, getActiveChangeStatus, getChangeDir, readActiveChange, writeActiveChange, } from '../utils/active-change.js';
|
|
4
|
-
import { getChangeSeq, getChangeType, getNextChangeSeq, isValidChangeName, } from '../utils/change.js';
|
|
5
|
-
import { buildChangeNextPlan } from '../utils/change-next.js';
|
|
4
|
+
import { CHANGE_TYPE_VALUES, getChangeSeq, getChangeType, getNextChangeSeq, isValidChangeName, isValidChangeType, } from '../utils/change.js';
|
|
5
|
+
import { buildChangeNextPlan, SCALE_VALUES, } from '../utils/change-next.js';
|
|
6
6
|
import { ensureDir, fileExists } from '../utils/fs.js';
|
|
7
7
|
import { logger } from '../utils/logger.js';
|
|
8
8
|
const IGNORED_PRECREATED_CHANGE_FILES = new Set([
|
|
@@ -43,6 +43,7 @@ export function registerChangeCommand(program) {
|
|
|
43
43
|
.command('next')
|
|
44
44
|
.description('输出当前可选的下一步提示词候选')
|
|
45
45
|
.option('--type <type>', '变更类型(feat / fix / refactor / perf / chore / ci / docs / style / test)')
|
|
46
|
+
.option('--scale <scale>', '变更规模(small / medium / complex);省略则按 type 推导')
|
|
46
47
|
.option('--no-contract-change', '收窄候选:本次不涉及接口 / Schema / 数据契约变化')
|
|
47
48
|
.option('--no-system-change', '收窄候选:本次不会沉淀系统级知识')
|
|
48
49
|
.option('-d, --domain <name>', '将 domain-sync 收窄到单域')
|
|
@@ -263,9 +264,20 @@ async function runNext(opts, rootDir) {
|
|
|
263
264
|
try {
|
|
264
265
|
await ensureSddRoot(rootDir);
|
|
265
266
|
const active = await readActiveChange(rootDir);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
if (opts.type && !isValidChangeType(opts.type)) {
|
|
268
|
+
logger.error(`--type 仅支持 ${CHANGE_TYPE_VALUES.join(' / ')}`);
|
|
269
|
+
process.exitCode = 1;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (opts.scale && !SCALE_VALUES.includes(opts.scale)) {
|
|
273
|
+
logger.error(`--scale 仅支持 ${SCALE_VALUES.join(' / ')}`);
|
|
274
|
+
process.exitCode = 1;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const type = resolveNextType(active?.change, opts.type) ?? undefined;
|
|
278
|
+
// type 只在需要推导规模时才必需;显式 --scale 时可缺省。
|
|
279
|
+
if (!type && !opts.scale) {
|
|
280
|
+
logger.error('change next 需要指定 --type 或 --scale,或在 confirmed active change 下从 change 名自动推导');
|
|
269
281
|
process.exitCode = 1;
|
|
270
282
|
return;
|
|
271
283
|
}
|
|
@@ -276,6 +288,7 @@ async function runNext(opts, rootDir) {
|
|
|
276
288
|
type,
|
|
277
289
|
contractChange: opts.contractChange,
|
|
278
290
|
systemChange: opts.systemChange,
|
|
291
|
+
scale: opts.scale,
|
|
279
292
|
domain: opts.domain,
|
|
280
293
|
});
|
|
281
294
|
printNextPlan(plan, {
|
|
@@ -321,7 +334,8 @@ function resolveNextType(change, explicitType) {
|
|
|
321
334
|
return getChangeType(change);
|
|
322
335
|
}
|
|
323
336
|
function printNextPlan(plan, opts) {
|
|
324
|
-
console.log(`type: ${opts.type}`);
|
|
337
|
+
console.log(`type: ${opts.type ?? '(未指定,按 --scale)'}`);
|
|
338
|
+
console.log(`scale: ${plan.scale}`);
|
|
325
339
|
console.log(`contract-change: ${opts.contractChange ? '默认纳入候选' : '已收窄'}`);
|
|
326
340
|
console.log(`system-change: ${opts.systemChange ? '默认纳入候选' : '已收窄'}`);
|
|
327
341
|
console.log(`domain: ${opts.domain ?? '(自动发现受影响域)'}`);
|
package/dist/commands/prompt.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { ActiveChangeError, readActiveChange } from '../utils/active-change.js';
|
|
4
|
-
import { findPrompt, getPromptsDir, listPrompts, loadPromptContent, } from '../prompts/catalog.js';
|
|
4
|
+
import { findPrompt, getPromptsDir, listPrompts, loadExpandedPromptContent, loadPromptContent, } from '../prompts/catalog.js';
|
|
5
5
|
import { renderPrompt } from '../prompts/renderer.js';
|
|
6
6
|
import { getNextChangeSeq } from '../utils/change.js';
|
|
7
7
|
import { logger } from '../utils/logger.js';
|
|
@@ -101,11 +101,12 @@ async function runShow(id, opts) {
|
|
|
101
101
|
process.exitCode = 1;
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
|
-
const template = await loadPromptContent(entry);
|
|
105
104
|
if (opts.raw) {
|
|
105
|
+
const template = await loadPromptContent(entry);
|
|
106
106
|
process.stdout.write(template);
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
|
+
const template = await loadExpandedPromptContent(entry);
|
|
109
110
|
const variables = await resolveVariables(entry.id, opts);
|
|
110
111
|
if (!variables)
|
|
111
112
|
return;
|
|
@@ -16,5 +16,6 @@ export declare function loadCatalog(): Promise<CatalogData>;
|
|
|
16
16
|
export declare function findPrompt(id: string): Promise<PromptEntry | undefined>;
|
|
17
17
|
export declare function listPrompts(stage?: string): Promise<PromptEntry[]>;
|
|
18
18
|
export declare function loadPromptContent(entry: PromptEntry): Promise<string>;
|
|
19
|
+
export declare function loadExpandedPromptContent(entry: PromptEntry): Promise<string>;
|
|
19
20
|
export declare function getPromptsDir(): string;
|
|
20
21
|
export {};
|
package/dist/prompts/catalog.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { expandPromptIncludes } from './include.js';
|
|
4
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
6
|
const PROMPTS_DIR = resolve(__dirname, '../../templates/prompts');
|
|
6
7
|
let cachedCatalog = null;
|
|
@@ -26,6 +27,10 @@ export async function loadPromptContent(entry) {
|
|
|
26
27
|
const filePath = join(PROMPTS_DIR, entry.file);
|
|
27
28
|
return readFile(filePath, 'utf-8');
|
|
28
29
|
}
|
|
30
|
+
export async function loadExpandedPromptContent(entry) {
|
|
31
|
+
const template = await loadPromptContent(entry);
|
|
32
|
+
return expandPromptIncludes(template, PROMPTS_DIR);
|
|
33
|
+
}
|
|
29
34
|
export function getPromptsDir() {
|
|
30
35
|
return PROMPTS_DIR;
|
|
31
36
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function expandPromptIncludes(template: string, baseDir: string): Promise<string>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
const INCLUDE_LINE_RE = /^[ \t]*<!--\s*@include\s+(\S+)\s*-->[ \t\r]*$/;
|
|
4
|
+
export async function expandPromptIncludes(template, baseDir) {
|
|
5
|
+
const resolvedBaseDir = resolve(baseDir);
|
|
6
|
+
const lines = template.split('\n');
|
|
7
|
+
const expanded = [];
|
|
8
|
+
for (const line of lines) {
|
|
9
|
+
const match = line.match(INCLUDE_LINE_RE);
|
|
10
|
+
if (!match) {
|
|
11
|
+
expanded.push(line);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const includePath = match[1];
|
|
15
|
+
if (isAbsolute(includePath)) {
|
|
16
|
+
throw new Error(`Include path must be relative: ${includePath}`);
|
|
17
|
+
}
|
|
18
|
+
const targetPath = resolve(resolvedBaseDir, includePath);
|
|
19
|
+
const relativePath = relative(resolvedBaseDir, targetPath);
|
|
20
|
+
if (relativePath === '' ||
|
|
21
|
+
relativePath.startsWith('..') ||
|
|
22
|
+
isAbsolute(relativePath)) {
|
|
23
|
+
throw new Error(`Include path escapes prompts directory: ${includePath}`);
|
|
24
|
+
}
|
|
25
|
+
const content = await readFile(targetPath, 'utf-8');
|
|
26
|
+
const hasNestedInclude = content
|
|
27
|
+
.split('\n')
|
|
28
|
+
.some((contentLine) => INCLUDE_LINE_RE.test(contentLine));
|
|
29
|
+
if (hasNestedInclude) {
|
|
30
|
+
throw new Error(`Nested include is not supported: ${includePath}`);
|
|
31
|
+
}
|
|
32
|
+
expanded.push(content);
|
|
33
|
+
}
|
|
34
|
+
return expanded.join('\n');
|
|
35
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { ActiveChange, ActiveChangeProgress } from './active-change.js';
|
|
2
2
|
import type { ChangeType } from './change.js';
|
|
3
3
|
export type NextCandidateId = 'ctx' | 'req' | 'iface' | 'impl-spec' | 'test-spec' | 'impl' | 'review' | 'domain-sync' | 'change clear';
|
|
4
|
+
export type ChangeScale = 'small' | 'medium' | 'complex';
|
|
5
|
+
export declare const SCALE_VALUES: readonly ChangeScale[];
|
|
4
6
|
export interface ChangeNextOptions {
|
|
5
|
-
type
|
|
7
|
+
type?: ChangeType;
|
|
6
8
|
contractChange: boolean;
|
|
7
9
|
systemChange: boolean;
|
|
10
|
+
scale?: ChangeScale;
|
|
8
11
|
domain?: string;
|
|
9
12
|
}
|
|
10
13
|
export interface NextCandidate {
|
|
@@ -22,10 +25,12 @@ export interface HiddenCandidate {
|
|
|
22
25
|
export interface ChangeNextPlan {
|
|
23
26
|
active: ActiveChange | null;
|
|
24
27
|
progress: ActiveChangeProgress | null;
|
|
28
|
+
scale: ChangeScale;
|
|
25
29
|
blockers: string[];
|
|
26
30
|
warnings: string[];
|
|
27
31
|
notes: string[];
|
|
28
32
|
candidates: NextCandidate[];
|
|
29
33
|
hidden: HiddenCandidate[];
|
|
30
34
|
}
|
|
35
|
+
export declare function resolveScale(type: ChangeType | undefined, explicit?: ChangeScale): ChangeScale;
|
|
31
36
|
export declare function buildChangeNextPlan(active: ActiveChange | null, progress: ActiveChangeProgress | null, options: ChangeNextOptions): ChangeNextPlan;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export const SCALE_VALUES = ['small', 'medium', 'complex'];
|
|
1
2
|
const CHANGE_REQUIRED = [
|
|
2
3
|
'req',
|
|
3
4
|
'iface',
|
|
@@ -8,6 +9,38 @@ const CHANGE_REQUIRED = [
|
|
|
8
9
|
'domain-sync',
|
|
9
10
|
'change clear',
|
|
10
11
|
];
|
|
12
|
+
// 规模 → 该规模默认纳入的候选集(与文档「任务规模裁剪」表一致)。
|
|
13
|
+
// small : 小 Bugfix / 小需求 req → impl → review
|
|
14
|
+
// medium : 中等需求 req → iface → impl → review → domain-sync
|
|
15
|
+
// complex: 复杂 / 跨域 ctx → req → iface → impl-spec → test-spec → impl → review → domain-sync
|
|
16
|
+
const SCALE_CANDIDATES = {
|
|
17
|
+
small: ['req', 'impl', 'review', 'change clear'],
|
|
18
|
+
medium: ['req', 'iface', 'impl', 'review', 'domain-sync', 'change clear'],
|
|
19
|
+
complex: [
|
|
20
|
+
'ctx',
|
|
21
|
+
'req',
|
|
22
|
+
'iface',
|
|
23
|
+
'impl-spec',
|
|
24
|
+
'test-spec',
|
|
25
|
+
'impl',
|
|
26
|
+
'review',
|
|
27
|
+
'domain-sync',
|
|
28
|
+
'change clear',
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
// 省略 --scale 时由 type 推导默认规模。complex 永远不自动推导——
|
|
32
|
+
// 它需要跨域 / 系统级证据,必须由人或 agent 显式 --scale complex。
|
|
33
|
+
const DEFAULT_SCALE_BY_TYPE = {
|
|
34
|
+
feat: 'medium',
|
|
35
|
+
fix: 'small',
|
|
36
|
+
refactor: 'medium',
|
|
37
|
+
perf: 'medium',
|
|
38
|
+
chore: 'small',
|
|
39
|
+
ci: 'small',
|
|
40
|
+
docs: 'small',
|
|
41
|
+
style: 'small',
|
|
42
|
+
test: 'small',
|
|
43
|
+
};
|
|
11
44
|
const TYPE_LABELS = {
|
|
12
45
|
feat: '功能变更',
|
|
13
46
|
fix: '缺陷修复',
|
|
@@ -19,6 +52,11 @@ const TYPE_LABELS = {
|
|
|
19
52
|
style: '样式调整',
|
|
20
53
|
test: '测试变更',
|
|
21
54
|
};
|
|
55
|
+
const SCALE_LABELS = {
|
|
56
|
+
small: '小改动',
|
|
57
|
+
medium: '中等改动',
|
|
58
|
+
complex: '复杂 / 跨域',
|
|
59
|
+
};
|
|
22
60
|
const CANDIDATE_TITLES = {
|
|
23
61
|
ctx: '生成 00_context',
|
|
24
62
|
req: '生成 01_requirement',
|
|
@@ -30,23 +68,37 @@ const CANDIDATE_TITLES = {
|
|
|
30
68
|
'domain-sync': '归档:domain_spec + 域间关系',
|
|
31
69
|
'change clear': '清理 active change',
|
|
32
70
|
};
|
|
71
|
+
export function resolveScale(type, explicit) {
|
|
72
|
+
if (explicit)
|
|
73
|
+
return explicit;
|
|
74
|
+
// 命令层保证 type 与 scale 至少有一个存在;此处兜底以防被内部误用。
|
|
75
|
+
return type ? DEFAULT_SCALE_BY_TYPE[type] : 'medium';
|
|
76
|
+
}
|
|
77
|
+
function scaleIncludes(scale, id) {
|
|
78
|
+
return SCALE_CANDIDATES[scale].includes(id);
|
|
79
|
+
}
|
|
80
|
+
function scaleHiddenReason(scale) {
|
|
81
|
+
return `当前规模 ${scale}(${SCALE_LABELS[scale]})不含此步;如确有需要,用 --scale 提升规模。`;
|
|
82
|
+
}
|
|
33
83
|
export function buildChangeNextPlan(active, progress, options) {
|
|
84
|
+
const scale = resolveScale(options.type, options.scale);
|
|
34
85
|
if (!active) {
|
|
35
|
-
return buildNoActivePlan(options.type);
|
|
86
|
+
return buildNoActivePlan(options.type, scale);
|
|
36
87
|
}
|
|
37
88
|
if (!active.change) {
|
|
38
|
-
return buildPendingPlan(active, options);
|
|
89
|
+
return buildPendingPlan(active, options, scale);
|
|
39
90
|
}
|
|
40
|
-
return buildConfirmedPlan(active, progress ?? emptyProgress(), options);
|
|
91
|
+
return buildConfirmedPlan(active, progress ?? emptyProgress(), options, scale);
|
|
41
92
|
}
|
|
42
|
-
function buildNoActivePlan(type) {
|
|
43
|
-
const notes = [typeNote(type)];
|
|
44
|
-
if (
|
|
93
|
+
function buildNoActivePlan(type, scale) {
|
|
94
|
+
const notes = [typeNote(type), scaleNote(scale)];
|
|
95
|
+
if (scale === 'small') {
|
|
45
96
|
notes.push('如果只是极小修复,也可以直接更新 AI_CHANGELOG,无需先建立 active change。');
|
|
46
97
|
}
|
|
47
98
|
return {
|
|
48
99
|
active: null,
|
|
49
100
|
progress: null,
|
|
101
|
+
scale,
|
|
50
102
|
blockers: ['先运行 `spec-canon change start -g <goal>` 建立 active change。'],
|
|
51
103
|
warnings: [],
|
|
52
104
|
notes,
|
|
@@ -59,7 +111,7 @@ function buildNoActivePlan(type) {
|
|
|
59
111
|
],
|
|
60
112
|
};
|
|
61
113
|
}
|
|
62
|
-
function buildPendingPlan(active, options) {
|
|
114
|
+
function buildPendingPlan(active, options, scale) {
|
|
63
115
|
const hidden = CHANGE_REQUIRED.map((id) => ({
|
|
64
116
|
id,
|
|
65
117
|
reason: '当前是 pending active change,需先确认 change 名。',
|
|
@@ -79,16 +131,17 @@ function buildPendingPlan(active, options) {
|
|
|
79
131
|
return {
|
|
80
132
|
active,
|
|
81
133
|
progress: emptyProgress(),
|
|
134
|
+
scale,
|
|
82
135
|
blockers: [
|
|
83
136
|
'如需运行依赖 CHANGE 的提示词,先执行 `spec-canon change start -g <goal> -c <change>` 确认 change 名。',
|
|
84
137
|
],
|
|
85
138
|
warnings: [],
|
|
86
|
-
notes: [typeNote(options.type)],
|
|
139
|
+
notes: [typeNote(options.type), scaleNote(scale)],
|
|
87
140
|
candidates: [
|
|
88
141
|
{
|
|
89
142
|
id: 'ctx',
|
|
90
143
|
title: CANDIDATE_TITLES['ctx'],
|
|
91
|
-
reason: 'pending active change 已具备 GOAL,可先生成 00_context
|
|
144
|
+
reason: 'pending active change 已具备 GOAL,可先生成 00_context 并获得 change 命名建议。',
|
|
92
145
|
command: 'spec-canon prompt show ctx',
|
|
93
146
|
priority: 0,
|
|
94
147
|
},
|
|
@@ -96,59 +149,63 @@ function buildPendingPlan(active, options) {
|
|
|
96
149
|
hidden: dedupeHidden(hidden),
|
|
97
150
|
};
|
|
98
151
|
}
|
|
99
|
-
function buildConfirmedPlan(active, progress, options) {
|
|
152
|
+
function buildConfirmedPlan(active, progress, options, scale) {
|
|
100
153
|
const warnings = getWarnings(progress);
|
|
101
154
|
const candidates = [];
|
|
102
155
|
const hidden = [];
|
|
156
|
+
const ctxInScale = scaleIncludes(scale, 'ctx');
|
|
103
157
|
pushCandidate(candidates, {
|
|
104
158
|
id: 'ctx',
|
|
105
|
-
enabled: !progress.context,
|
|
159
|
+
enabled: ctxInScale && !progress.context,
|
|
106
160
|
title: CANDIDATE_TITLES['ctx'],
|
|
107
161
|
reason: '00_context.md 尚未生成,适合先补齐系统现状。',
|
|
108
162
|
command: 'spec-canon prompt show ctx',
|
|
109
163
|
priority: 0,
|
|
110
|
-
hiddenReason: '00_context.md 已存在。',
|
|
111
|
-
});
|
|
164
|
+
hiddenReason: ctxInScale ? '00_context.md 已存在。' : scaleHiddenReason(scale),
|
|
165
|
+
}, hidden);
|
|
112
166
|
pushCandidate(candidates, {
|
|
113
167
|
id: 'req',
|
|
114
|
-
enabled: !progress.requirement,
|
|
168
|
+
enabled: scaleIncludes(scale, 'req') && !progress.requirement,
|
|
115
169
|
title: CANDIDATE_TITLES['req'],
|
|
116
170
|
reason: '01_requirement.md 尚未生成,建议先明确需求与验收标准。',
|
|
117
171
|
command: 'spec-canon prompt show req',
|
|
118
172
|
priority: 10,
|
|
119
173
|
hiddenReason: '01_requirement.md 已存在。',
|
|
120
174
|
}, hidden);
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
175
|
+
const ifaceInScale = scaleIncludes(scale, 'iface');
|
|
176
|
+
const ifaceHiddenReason = !ifaceInScale
|
|
177
|
+
? scaleHiddenReason(scale)
|
|
178
|
+
: !options.contractChange
|
|
179
|
+
? '已使用 --no-contract-change 收窄候选。'
|
|
180
|
+
: '02_interface.md 已存在。';
|
|
124
181
|
pushCandidate(candidates, {
|
|
125
182
|
id: 'iface',
|
|
126
|
-
enabled: options.contractChange && !progress.interface,
|
|
183
|
+
enabled: ifaceInScale && options.contractChange && !progress.interface,
|
|
127
184
|
title: CANDIDATE_TITLES['iface'],
|
|
128
|
-
reason:
|
|
185
|
+
reason: '当前规模纳入接口/契约设计,且 02_interface.md 尚未生成。',
|
|
129
186
|
command: 'spec-canon prompt show iface',
|
|
130
187
|
priority: progress.requirement ? 20 : 45,
|
|
131
|
-
hiddenReason:
|
|
132
|
-
? '02_interface.md 已存在。'
|
|
133
|
-
: '已使用 --no-contract-change 收窄候选。',
|
|
188
|
+
hiddenReason: ifaceHiddenReason,
|
|
134
189
|
}, hidden);
|
|
190
|
+
const implSpecInScale = scaleIncludes(scale, 'impl-spec');
|
|
135
191
|
pushCandidate(candidates, {
|
|
136
192
|
id: 'impl-spec',
|
|
137
|
-
enabled: !progress.implementation,
|
|
193
|
+
enabled: implSpecInScale && !progress.implementation,
|
|
138
194
|
title: CANDIDATE_TITLES['impl-spec'],
|
|
139
195
|
reason: implSpecReason(options.type, progress),
|
|
140
196
|
command: 'spec-canon prompt show impl-spec',
|
|
141
197
|
priority: implSpecPriority(options.type, progress),
|
|
142
|
-
hiddenReason: '03_implementation.md 已存在。',
|
|
198
|
+
hiddenReason: implSpecInScale ? '03_implementation.md 已存在。' : scaleHiddenReason(scale),
|
|
143
199
|
}, hidden);
|
|
200
|
+
const testSpecInScale = scaleIncludes(scale, 'test-spec');
|
|
144
201
|
pushCandidate(candidates, {
|
|
145
202
|
id: 'test-spec',
|
|
146
|
-
enabled: !progress.testSpec,
|
|
203
|
+
enabled: testSpecInScale && !progress.testSpec,
|
|
147
204
|
title: CANDIDATE_TITLES['test-spec'],
|
|
148
205
|
reason: testSpecReason(options.type, progress),
|
|
149
206
|
command: 'spec-canon prompt show test-spec',
|
|
150
207
|
priority: progress.implementation ? 32 : 36,
|
|
151
|
-
hiddenReason: '04_test_spec.md 已存在。',
|
|
208
|
+
hiddenReason: testSpecInScale ? '04_test_spec.md 已存在。' : scaleHiddenReason(scale),
|
|
152
209
|
}, hidden);
|
|
153
210
|
pushCandidate(candidates, {
|
|
154
211
|
id: 'impl',
|
|
@@ -170,32 +227,40 @@ function buildConfirmedPlan(active, progress, options) {
|
|
|
170
227
|
? 'AI_CHANGELOG 已包含当前 change。'
|
|
171
228
|
: '当前还缺少足够的前置 Spec/编码结果。',
|
|
172
229
|
}, hidden);
|
|
230
|
+
const domainSyncInScale = scaleIncludes(scale, 'domain-sync');
|
|
231
|
+
const domainSyncInScope = domainSyncInScale && options.systemChange;
|
|
173
232
|
const domainSyncCommand = options.domain
|
|
174
233
|
? `spec-canon prompt show domain-sync -d ${options.domain}`
|
|
175
234
|
: 'spec-canon prompt show domain-sync';
|
|
176
235
|
const domainSyncReason = options.domain
|
|
177
236
|
? `AI_CHANGELOG 已更新,可将回填范围收窄到域 ${options.domain}。`
|
|
178
237
|
: 'AI_CHANGELOG 已更新,可默认让 domain-sync 自动发现受影响域。';
|
|
238
|
+
const domainSyncHiddenReason = progress.domainSynced
|
|
239
|
+
? 'domain-sync 已完成。'
|
|
240
|
+
: !domainSyncInScale
|
|
241
|
+
? scaleHiddenReason(scale)
|
|
242
|
+
: !options.systemChange
|
|
243
|
+
? '已使用 --no-system-change 收窄候选。'
|
|
244
|
+
: '完成 review 并更新 AI_CHANGELOG 后再执行。';
|
|
179
245
|
pushCandidate(candidates, {
|
|
180
246
|
id: 'domain-sync',
|
|
181
|
-
enabled: progress.changelogUpdated && !progress.domainSynced
|
|
247
|
+
enabled: domainSyncInScope && progress.changelogUpdated && !progress.domainSynced,
|
|
182
248
|
title: CANDIDATE_TITLES['domain-sync'],
|
|
183
249
|
reason: domainSyncReason,
|
|
184
250
|
command: domainSyncCommand,
|
|
185
251
|
priority: 80,
|
|
186
|
-
hiddenReason:
|
|
187
|
-
? 'domain-sync 已完成。'
|
|
188
|
-
: options.systemChange
|
|
189
|
-
? '完成 review 并更新 AI_CHANGELOG 后再执行。'
|
|
190
|
-
: '已使用 --no-system-change 收窄候选。',
|
|
252
|
+
hiddenReason: domainSyncHiddenReason,
|
|
191
253
|
}, hidden);
|
|
254
|
+
const clearReason = progress.domainSynced
|
|
255
|
+
? 'domain-sync 已完成,可清理当前 active change。'
|
|
256
|
+
: !domainSyncInScale
|
|
257
|
+
? `当前规模 ${scale}(${SCALE_LABELS[scale]})无需 domain-sync,可在 review 后直接清理。`
|
|
258
|
+
: '已使用 --no-system-change 收窄归档步骤,可直接清理 active change。';
|
|
192
259
|
pushCandidate(candidates, {
|
|
193
260
|
id: 'change clear',
|
|
194
|
-
enabled: progress.domainSynced || (progress.changelogUpdated && !
|
|
261
|
+
enabled: progress.domainSynced || (progress.changelogUpdated && !domainSyncInScope),
|
|
195
262
|
title: CANDIDATE_TITLES['change clear'],
|
|
196
|
-
reason:
|
|
197
|
-
? 'domain-sync 已完成,可清理当前 active change。'
|
|
198
|
-
: '已使用 --no-system-change 收窄归档步骤,可直接清理 active change。',
|
|
263
|
+
reason: clearReason,
|
|
199
264
|
command: 'spec-canon change clear',
|
|
200
265
|
priority: 90,
|
|
201
266
|
hiddenReason: '当前 change 仍未完成 Review/归档。',
|
|
@@ -203,10 +268,12 @@ function buildConfirmedPlan(active, progress, options) {
|
|
|
203
268
|
return {
|
|
204
269
|
active,
|
|
205
270
|
progress,
|
|
271
|
+
scale,
|
|
206
272
|
blockers: [],
|
|
207
273
|
warnings,
|
|
208
274
|
notes: [
|
|
209
275
|
typeNote(options.type),
|
|
276
|
+
scaleNote(scale),
|
|
210
277
|
options.domain
|
|
211
278
|
? `domain-sync 将限定在单域 ${options.domain}。`
|
|
212
279
|
: 'domain-sync 未指定 -d,将默认自动发现受影响域。',
|
|
@@ -280,8 +347,14 @@ function getWarnings(progress) {
|
|
|
280
347
|
return warnings;
|
|
281
348
|
}
|
|
282
349
|
function typeNote(type) {
|
|
350
|
+
if (!type) {
|
|
351
|
+
return '变更类型:未指定(已由 --scale 显式指定规模)。';
|
|
352
|
+
}
|
|
283
353
|
return `变更类型:${type}(${TYPE_LABELS[type]})。`;
|
|
284
354
|
}
|
|
355
|
+
function scaleNote(scale) {
|
|
356
|
+
return `变更规模:${scale}(${SCALE_LABELS[scale]});候选已按规模裁剪。`;
|
|
357
|
+
}
|
|
285
358
|
function emptyProgress() {
|
|
286
359
|
return {
|
|
287
360
|
context: false,
|
package/dist/utils/change.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare const CHANGE_NAME_PATTERN: RegExp;
|
|
2
2
|
export type ChangeType = 'feat' | 'fix' | 'refactor' | 'perf' | 'chore' | 'ci' | 'docs' | 'style' | 'test';
|
|
3
|
+
export declare const CHANGE_TYPE_VALUES: readonly ChangeType[];
|
|
4
|
+
export declare function isValidChangeType(value: string): value is ChangeType;
|
|
3
5
|
export declare function isValidChangeName(change: string): boolean;
|
|
4
6
|
export declare function getChangeSeq(change: string): string | null;
|
|
5
7
|
export declare function getChangeType(change: string): ChangeType | null;
|
package/dist/utils/change.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { readdir } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
export const CHANGE_NAME_PATTERN = /^(feat|fix|refactor|perf|chore|ci|docs|style|test)-(\d{3})-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
4
|
+
export const CHANGE_TYPE_VALUES = [
|
|
5
|
+
'feat',
|
|
6
|
+
'fix',
|
|
7
|
+
'refactor',
|
|
8
|
+
'perf',
|
|
9
|
+
'chore',
|
|
10
|
+
'ci',
|
|
11
|
+
'docs',
|
|
12
|
+
'style',
|
|
13
|
+
'test',
|
|
14
|
+
];
|
|
15
|
+
export function isValidChangeType(value) {
|
|
16
|
+
return CHANGE_TYPE_VALUES.includes(value);
|
|
17
|
+
}
|
|
4
18
|
export function isValidChangeName(change) {
|
|
5
19
|
return CHANGE_NAME_PATTERN.test(change);
|
|
6
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-canon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "CLI toolkit for Spec-Driven Development (SDD)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"prebuild": "node scripts/sync-templates.mjs",
|
|
29
29
|
"build": "tsc",
|
|
30
30
|
"dev": "tsx src/index.ts",
|
|
31
|
-
"test": "
|
|
31
|
+
"test:skills": "node scripts/validate-skills.mjs",
|
|
32
|
+
"test": "npm run test:skills && vitest run",
|
|
32
33
|
"preversion": "npm test",
|
|
33
34
|
"version": "node scripts/sync-site-version.mjs",
|
|
34
35
|
"prepublishOnly": "npm run build"
|
package/templates/00_context.md
CHANGED
|
@@ -2,47 +2,58 @@
|
|
|
2
2
|
> 生成时间: YYYY-MM-DD | 生成者: Claude Code + @[工程师]
|
|
3
3
|
> 本文档描述启动本变更时的系统现状,作为 01-04 的共享前提。
|
|
4
4
|
|
|
5
|
+
<!-- 本模板是"必答清单",不是"必填表单":
|
|
6
|
+
- 按本次变更裁剪——删除与本次无关的小节,优于堆砌无关现状。
|
|
7
|
+
- 每节的 > 引文说明"本节要回答什么 / 谁消费它",写内容时对齐该目的。
|
|
8
|
+
- Evidence Map 记录真实查证结果;查不到就写进"未找到或待补充",不要凭空补全。
|
|
9
|
+
- 占位符 [xxx] 替换为真实内容,方括号本身不要保留。 -->
|
|
10
|
+
|
|
11
|
+
## 0. Evidence Map(取证范围)
|
|
12
|
+
> 本节要回答:本文档的结论基于哪些真实来源——区分"查证过"与"凭印象"。
|
|
13
|
+
- 项目入口:[README.md / AGENTS.md / CLAUDE.md,记录已阅读项]
|
|
14
|
+
- SDD 文档:[domains/README.md / 相关 domain_spec.md / RULES.md / AI_CHANGELOG.md,记录已阅读项]
|
|
15
|
+
- 近期提交:[最近 5-10 条 git log 中与本次变更相关的提交]
|
|
16
|
+
- 代码入口:[Controller / Router / Handler / DTO / 数据模型 / 服务模块等真实代码路径]
|
|
17
|
+
- 未找到或待补充:[缺失的文档、无法定位的模块、需人工确认的信息]
|
|
18
|
+
|
|
5
19
|
## 1. 业务背景(Business Context)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
20
|
+
> 本节要回答:这次变更所处的业务域、用户场景、为什么现在做。
|
|
21
|
+
> 下游消费者:01_requirement 的 Background 可直接引用。
|
|
22
|
+
|
|
23
|
+
[从 PRD 提炼,2-3 段即可。]
|
|
9
24
|
|
|
10
25
|
## 2. 系统现状(Current State)
|
|
26
|
+
> 本节要回答:动手前,相关模块/数据/接口/依赖各是什么样。只列与本次相关的部分。
|
|
11
27
|
|
|
12
28
|
### 2.1 相关模块
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
(→ 下游消费者:03_implementation 的 File Changes、04_test_spec 的 Regression Impact)
|
|
29
|
+
> 下游消费者:03 的 File Changes、04 的 Regression Impact。
|
|
30
|
+
- `[模块名]`([真实路径]):负责 [职责],当前状态 [正常 / 有已知问题]
|
|
16
31
|
|
|
17
32
|
### 2.2 相关数据模型
|
|
18
|
-
|
|
19
|
-
- `
|
|
20
|
-
(→ 下游消费者:02_interface 的字段命名对齐、03_implementation 的 Data Changes)
|
|
33
|
+
> 下游消费者:02 的字段命名对齐、03 的 Data Changes。
|
|
34
|
+
- `[表名]`:[关键字段、当前索引、与其他表的关联关系]
|
|
21
35
|
|
|
22
36
|
### 2.3 相关接口(已有的)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
> 下游消费者:02 的风格一致性、错误码体系对齐。
|
|
38
|
+
> 取证要求:以真实代码入口(Controller/Router/Handler/路由/OpenAPI)为准,与文档冲突时以代码为准。
|
|
39
|
+
- `[方法 路径]`:[用途、当前调用方]
|
|
26
40
|
|
|
27
41
|
### 2.4 依赖服务
|
|
28
|
-
|
|
29
|
-
- [外部服务]
|
|
42
|
+
> 下游消费者:01 的 Constraints、03 的容错设计。
|
|
43
|
+
- [内部 / 外部服务]:[服务名、协议、SLA 或容量约束(如有)]
|
|
30
44
|
|
|
31
45
|
## 3. 技术约束(Technical Constraints)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- [兼容性
|
|
35
|
-
- [安全]:涉及 [用户隐私/资金] 数据,需 [脱敏/审计]
|
|
36
|
-
(→ 下游消费者:01_requirement 的 Constraints、02_interface 的版本策略)
|
|
46
|
+
> 本节要回答:哪些既有约束会限制本次方案选择。
|
|
47
|
+
> 下游消费者:01 的 Constraints、02 的版本策略。
|
|
48
|
+
- [性能 / 数据量 / 兼容性 / 安全等真实约束;无相关约束的维度删除]
|
|
37
49
|
|
|
38
50
|
## 4. 历史决策(Prior Decisions)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- [
|
|
42
|
-
|
|
51
|
+
> 本节要回答:过去在相关问题上做过哪些决定,避免本次重复评估或推翻。
|
|
52
|
+
> 下游消费者:03 的 Design Decision。
|
|
53
|
+
- [[日期] 曾考虑 [方案] 因 [原因] 放弃 / 选择 [方案] 并留下 [约束](来源:AI_CHANGELOG)]
|
|
54
|
+
- [无相关历史决策则删除本节]
|
|
43
55
|
|
|
44
56
|
## 5. 已知风险与坑位(Known Pitfalls)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- [
|
|
48
|
-
(→ 下游消费者:03_implementation 的坑位规避、04_test_spec 的边界用例)
|
|
57
|
+
> 本节要回答:相关代码/数据里有哪些"踩过的雷"。
|
|
58
|
+
> 下游消费者:03 的坑位规避、04 的边界用例。
|
|
59
|
+
- [来自团队经验或代码证据的具体坑位;无则删除本节]
|
|
@@ -3,14 +3,26 @@
|
|
|
3
3
|
> PRD 来源: [PRD 链接或编号]
|
|
4
4
|
> 系统现状: @spec-canon/changes/change-xxx/00_context.md
|
|
5
5
|
|
|
6
|
+
<!-- 本模板是"必答清单",不是"必填表单":
|
|
7
|
+
- 按本次变更裁剪——删除不适用的小节,优于留空或写"无"。
|
|
8
|
+
- 每节的 > 引文说明"本节要回答什么 / 谁消费它",写内容时对齐该目的。
|
|
9
|
+
- 占位符 [xxx] 替换为真实内容,方括号本身不要保留。 -->
|
|
10
|
+
|
|
6
11
|
## 1. Background(背景)
|
|
7
|
-
|
|
12
|
+
> 本节要回答:为什么做、解决什么问题。
|
|
13
|
+
> 下游消费者:02/03 理解需求来由。
|
|
14
|
+
|
|
15
|
+
[1-2 段即可。]
|
|
8
16
|
|
|
9
17
|
## 2. Goals(目标)
|
|
18
|
+
> 本节要回答:本次要达成的结果(是什么,不是怎么做)。
|
|
19
|
+
|
|
10
20
|
- Goal-1: [要达成的结果]
|
|
11
|
-
- Goal-2: [要达成的结果]
|
|
12
21
|
|
|
13
22
|
## 3. Scope(范围)
|
|
23
|
+
> 本节要回答:边界在哪——做什么、明确不做什么。
|
|
24
|
+
> 下游消费者:03 的 File Changes 不得越出 In Scope。
|
|
25
|
+
|
|
14
26
|
### In Scope(本次做)
|
|
15
27
|
- [条目化列出]
|
|
16
28
|
|
|
@@ -18,23 +30,33 @@
|
|
|
18
30
|
- [条目化列出,明确边界]
|
|
19
31
|
|
|
20
32
|
## 4. Acceptance Criteria(验收标准)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
> 本节要回答:怎样算做对了——可验证的合约,是整个变更的锚点。
|
|
34
|
+
> 下游消费者:04 的每条用例必须对齐某条 AC。
|
|
35
|
+
|
|
36
|
+
- AC-1: [具体、可验证的验收条件]
|
|
37
|
+
- [至少覆盖:主流程 + 幂等/重复 + 2 个失败场景]
|
|
25
38
|
|
|
26
39
|
## 5. Constraints(约束)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
-
|
|
40
|
+
> 本节要回答:必须满足的非功能约束(来自 00_context §3)。
|
|
41
|
+
> 下游消费者:02 的版本策略、03 的方案选择。
|
|
42
|
+
|
|
43
|
+
- [性能 / 安全 / 兼容 / 依赖等;无相关维度删除]
|
|
31
44
|
|
|
32
45
|
## 6. Risks & Rollout(风险与上线策略)
|
|
46
|
+
> 本节要回答:有什么风险、怎么灰度、怎么回滚。
|
|
47
|
+
> 下游消费者:03 的 Rollback、04 的回归优先级。
|
|
48
|
+
|
|
33
49
|
- 风险:[已识别的风险]
|
|
34
|
-
-
|
|
35
|
-
- 回滚预案:[如 关闭功能开关即可回滚]
|
|
50
|
+
- 灰度 / 回滚:[策略;若为低风险纯新增,说明原因即可]
|
|
36
51
|
|
|
37
52
|
## 7. Consistency Check(一致性自检)
|
|
38
53
|
- [ ] 各 AC 之间无矛盾
|
|
39
54
|
- [ ] 约束条件在所有 AC 场景下成立
|
|
40
|
-
- [ ] In/Out Scope 边界无灰色地带
|
|
55
|
+
- [ ] In/Out Scope 边界无灰色地带
|
|
56
|
+
|
|
57
|
+
<!-- 以下留痕仅在生成本 Spec 过程中实际触发过 spark gate(停下向人确认)时填写;
|
|
58
|
+
全程无需澄清则删除整块,不要写"无"。 -->
|
|
59
|
+
## 8. Clarification Notes(澄清留痕 · 仅 gate 触发时填写)
|
|
60
|
+
- 已确认:[确认过的需求解释、业务边界或验收口径]
|
|
61
|
+
- 选定/放弃方案:[多种需求切法时,记录采用哪种、放弃哪种及原因]
|
|
62
|
+
- 仍待确认:[不阻塞 01、但后续需确认的问题]
|
|
@@ -3,62 +3,65 @@
|
|
|
3
3
|
> 系统现状: @spec-canon/changes/change-xxx/00_context.md
|
|
4
4
|
> 对齐 01: @spec-canon/changes/change-xxx/01_requirement.md
|
|
5
5
|
|
|
6
|
+
<!-- 本模板是"必答清单",不是"必填表单":
|
|
7
|
+
- 按本次变更裁剪——删除不适用的小节(如无数据返回则删 Data Schema)。
|
|
8
|
+
- 每节的 > 引文说明"本节要回答什么 / 谁消费它"。
|
|
9
|
+
- 表格/示例中的 field、错误码等均为占位示例,替换为真实契约,不要照抄。
|
|
10
|
+
- 占位符 [xxx] 替换为真实内容,方括号本身不要保留。 -->
|
|
11
|
+
|
|
6
12
|
## 1. Overview(总览)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
> 本节要回答:这组接口的统一规则——鉴权、幂等、版本、错误码体系。
|
|
14
|
+
> 这些必须与 00_context §2.3 的既有接口风格一致。
|
|
15
|
+
|
|
16
|
+
- 鉴权方式:[与既有接口一致的鉴权]
|
|
17
|
+
- 幂等要求:[幂等键及语义]
|
|
18
|
+
- 版本策略:[向后兼容策略]
|
|
10
19
|
- 错误码体系:[遵循项目统一错误码规范]
|
|
11
20
|
|
|
12
21
|
## 2. Endpoints
|
|
22
|
+
> 本节要回答:每个端点的完整契约——它是前后端/服务间的唯一真相。
|
|
23
|
+
> 下游消费者:03 据此实现、04 据此做契约测试。
|
|
13
24
|
|
|
14
25
|
### API-1: [HTTP方法] [路径]
|
|
15
|
-
> 对应 AC: AC-
|
|
26
|
+
> 对应 AC: [AC-x]
|
|
16
27
|
|
|
17
28
|
**Request**
|
|
18
29
|
|
|
19
30
|
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|
|
20
31
|
|---|---|---|---|---|
|
|
21
|
-
|
|
|
22
|
-
| field2 | int | N | 描述 | 范围 |
|
|
32
|
+
| [字段] | [类型] | [Y/N] | [说明] | [正则/长度/枚举/范围] |
|
|
23
33
|
|
|
24
34
|
**Response (Success)**
|
|
25
35
|
|
|
26
36
|
| 字段 | 类型 | Nullable | 说明 |
|
|
27
37
|
|---|---|---|---|
|
|
28
|
-
|
|
|
29
|
-
| field2 | string | Y | 描述 |
|
|
38
|
+
| [字段] | [类型] | [Y/N] | [说明] |
|
|
30
39
|
|
|
31
40
|
**Error Codes**
|
|
32
41
|
|
|
33
42
|
| 错误码 | HTTP Status | 说明 | 触发条件 |
|
|
34
43
|
|---|---|---|---|
|
|
35
|
-
|
|
|
36
|
-
| UNAUTHORIZED | 401 | 未授权 | Token 无效或过期 |
|
|
44
|
+
| [错误码] | [状态码] | [说明] | [触发条件] |
|
|
37
45
|
|
|
38
46
|
**Examples**
|
|
39
47
|
```json
|
|
40
|
-
//
|
|
41
|
-
Request: { "field1": "value" }
|
|
42
|
-
Response: { "field1": true, "field2": "xxx" }
|
|
43
|
-
|
|
44
|
-
// 成功 — 幂等场景
|
|
45
|
-
Request: { "field1": "value" } (第二次请求)
|
|
46
|
-
Response: { "field1": true, "field2": "xxx" } (结果一致)
|
|
47
|
-
|
|
48
|
-
// 失败 — 参数错误
|
|
49
|
-
Request: { "field1": "" }
|
|
50
|
-
Response: { "code": "INVALID_PARAM", "message": "field1 不能为空" }
|
|
48
|
+
// 至少给出三种真实样例:成功首次、幂等重复、典型失败
|
|
51
49
|
```
|
|
52
50
|
|
|
53
51
|
## 3. Data Schema(可选,供 DTO 生成使用)
|
|
52
|
+
> 仅当需要前端/调用方直接生成类型时填写;否则删除本节。
|
|
53
|
+
|
|
54
54
|
```typescript
|
|
55
|
-
|
|
56
|
-
interface XxxResponse {
|
|
57
|
-
field1: boolean;
|
|
58
|
-
field2: string | null;
|
|
59
|
-
}
|
|
55
|
+
[类型定义]
|
|
60
56
|
```
|
|
61
57
|
|
|
62
58
|
## 4. Notes(补充说明)
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
> 本节要回答:契约之外、调用方仍需知道的事(兼容注意、与其他接口的联动)。
|
|
60
|
+
|
|
61
|
+
- [无则删除本节]
|
|
62
|
+
|
|
63
|
+
<!-- 以下留痕仅在生成本 Spec 过程中实际触发过 spark gate 时填写;无则删除整块,不要写"无"。 -->
|
|
64
|
+
## 5. Contract Decisions(契约决策留痕 · 仅 gate 触发时填写)
|
|
65
|
+
- 已确认:[接口形式、调用方、兼容边界、字段语义等确认判断]
|
|
66
|
+
- 选定/放弃方案:[REST/RPC/事件/批处理等形式的取舍及理由]
|
|
67
|
+
- 仍待确认:[不阻塞 02、但后续需确认的契约问题]
|
|
@@ -1,81 +1,79 @@
|
|
|
1
1
|
# Implementation Spec: [功能名称]
|
|
2
2
|
> Spec Owner: @[工程师姓名] | Status: draft / approved
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
> 上游依赖(生成前必读): 00_context.md + 01_requirement.md + 02_interface.md
|
|
4
|
+
|
|
5
|
+
<!-- 本模板是"必答清单",不是"必填表单":
|
|
6
|
+
- 按本次变更裁剪——删除不适用的小节,优于留空或写"无"。
|
|
7
|
+
- 每节的 > 引文说明"本节要回答什么 / 谁消费它",写内容时对齐该目的,不要套形式。
|
|
8
|
+
- 占位符 [xxx] 替换为真实内容,方括号本身不要保留。 -->
|
|
6
9
|
|
|
7
10
|
## 1. Goal Recap(目标复述)
|
|
8
|
-
|
|
11
|
+
> 本节要回答:你对"要实现什么"的理解是否与 01/02 一致——这是后续所有决策的锚点。
|
|
12
|
+
|
|
13
|
+
用 3-5 行复述本次要实现什么。若复述与 01 的 AC 有任何出入,停下回到 01 对齐,不要带着分歧往下写。
|
|
9
14
|
|
|
10
15
|
## 2. Design Decision(设计决策)
|
|
16
|
+
> 本节要回答:为什么用这个方案,且它不与系统既有约束冲突。
|
|
17
|
+
> 下游消费者:未来变更读这里,避免重复评估已否决方案(同 00_context §4 的作用)。
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
[
|
|
19
|
+
**选定方案**
|
|
20
|
+
[描述方案,以及它如何满足 01 的 AC 和 02 的契约。]
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
**为什么是它**
|
|
23
|
+
[- 若只有一个合理方案:直接陈述"为何这是唯一合理解",不要为凑数编造备选。
|
|
24
|
+
- 若存在多个真实可行方案:逐一列出方案、各自 trade-off、选定理由。散文或表格皆可,形式服从于把取舍讲清楚。]
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| 复杂度 | ★★☆ | ★★★★ | ★★★ |
|
|
20
|
-
| 性能 | ★★★ | ★★★★★ | ★★★★ |
|
|
21
|
-
| 可维护性 | ★★★★★ | ★★★ | ★★ |
|
|
22
|
-
| 风险 | ★★ | ★★★ | ★★★★ |
|
|
26
|
+
**与既有约束的核对**
|
|
27
|
+
[对照 00_context §4 历史决策、§5 已知坑位,说明本方案未踩已否决方案、未触发已知坑位;若有冲突,在此说明冲突与处理方式,而非绕过。]
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
<!-- 以下留痕仅在生成本 Spec 过程中实际触发过 spark gate(停下向人确认)时填写;
|
|
30
|
+
全程无需澄清则删除整块,不要写"无"。 -->
|
|
31
|
+
**决策留痕(仅 gate 触发时填写)**
|
|
32
|
+
- 已确认假设:[人工确认过的实现边界 / 迁移 / 灰度 / 回滚判断]
|
|
33
|
+
- 确认来源:[来自哪次澄清或哪份上游文档]
|
|
34
|
+
- 仍待确认:[不阻塞 03、但执行前需再确认的问题]
|
|
25
35
|
|
|
26
36
|
## 3. File Changes(变更范围)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- `path/to/
|
|
37
|
+
> 本节要回答:动哪些文件、各自动什么——直接来自 00_context §2 系统现状。
|
|
38
|
+
> 下游消费者:04_test_spec 据此推导回归范围;impl 阶段据此逐文件落地。
|
|
39
|
+
|
|
40
|
+
- `path/to/FileA` — [新增/修改什么,对应哪个 AC 或 Endpoint]
|
|
41
|
+
- [按真实代码路径列全,新文件标注"新增"]
|
|
42
|
+
|
|
43
|
+
## 4. Data Changes(数据变更)
|
|
44
|
+
> 仅当涉及表/索引/字段变更时填写;纯逻辑变更删除本节。
|
|
45
|
+
> 下游消费者:04 的迁移测试、§7 的回退脚本。
|
|
31
46
|
|
|
32
|
-
## 4. Data Changes(数据变更,如适用)
|
|
33
47
|
```sql
|
|
34
|
-
|
|
35
|
-
ALTER TABLE xxx ADD COLUMN yyy;
|
|
36
|
-
CREATE UNIQUE INDEX idx_xxx ON table(col1, col2);
|
|
48
|
+
[DDL;同时说明对存量数据的影响和迁移方式]
|
|
37
49
|
```
|
|
38
50
|
|
|
39
51
|
## 5. Core Logic(核心逻辑)
|
|
52
|
+
> 本节要回答:关键流程怎么走、靠什么保证正确性。
|
|
53
|
+
> 下游消费者:04 的用例设计、impl 阶段的编码。
|
|
40
54
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
1. 解析参数,校验格式
|
|
45
|
-
2. 开启事务
|
|
46
|
-
3. 尝试插入记录(唯一约束保证幂等)
|
|
47
|
-
- 冲突 → 返回已处理结果
|
|
48
|
-
4. 插入成功 → 执行业务逻辑
|
|
49
|
-
5. 提交事务
|
|
50
|
-
6. 查询最新状态 → 组装响应
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### 不变量断言(复杂逻辑时使用)
|
|
54
|
-
- INV-1: [不变量描述]
|
|
55
|
-
- INV-2: [不变量描述]
|
|
55
|
+
**主流程**
|
|
56
|
+
[伪代码或分步描述。重点写"非显然"的部分——幂等如何保证、事务边界、并发与失败回退;显然的 CRUD 不必展开。]
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
stateDiagram-v2
|
|
60
|
-
[描述状态流转]
|
|
61
|
-
```
|
|
58
|
+
**不变量 / 状态流转(复杂逻辑时)**
|
|
59
|
+
[列出必须始终成立的不变量,或画状态图。逻辑简单则删除本小节。]
|
|
62
60
|
|
|
63
61
|
## 6. Execution Plan(分步执行计划)
|
|
62
|
+
> 本节要回答:按什么顺序施工、每步如何自证没坏。
|
|
63
|
+
> 下游消费者:impl 阶段严格按此逐步执行,Gate 不过不进。
|
|
64
|
+
> 硬性要求:每个 Step 必须有可运行的验证命令;无法验证的步骤要么拆细,要么说明为何无法自动验证。
|
|
64
65
|
|
|
65
66
|
### Step 1: [步骤标题]
|
|
66
|
-
- 动作:[
|
|
67
|
+
- 动作:[做什么]
|
|
67
68
|
- 文件:`path/to/file`
|
|
68
|
-
- ✅ 验证:`[
|
|
69
|
+
- ✅ 验证:`[编译/测试/lint 命令]`(通过才进入下一步)
|
|
69
70
|
|
|
70
|
-
### Step 2:
|
|
71
|
-
- 动作:[具体做什么]
|
|
72
|
-
- 文件:`path/to/file`
|
|
73
|
-
- ✅ 验证:`[验证命令]`
|
|
74
|
-
|
|
75
|
-
### Step 3: [步骤标题]
|
|
76
|
-
...
|
|
71
|
+
### Step 2: ...
|
|
77
72
|
|
|
78
73
|
## 7. Rollback & Compatibility(回滚与兼容)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
> 本节要回答:出问题怎么退、会不会伤到现有功能。
|
|
75
|
+
> 下游消费者:01 的 Rollout 策略、04 的回归影响。
|
|
76
|
+
|
|
77
|
+
- 回滚方式:[配置开关 / 反向迁移 / 版本回退,哪种]
|
|
78
|
+
- 兼容影响:[影响哪些现有调用方或数据]
|
|
79
|
+
- [若本变更无回滚风险(如纯新增、无状态),说明原因即可]
|
|
@@ -4,31 +4,46 @@
|
|
|
4
4
|
> 对齐 01: AC-1 ~ AC-N
|
|
5
5
|
> 对齐 02: 所有 Endpoints
|
|
6
6
|
|
|
7
|
+
<!-- 本模板是"必答清单",不是"必填表单":
|
|
8
|
+
- 按本次变更裁剪——无接口变更则删契约测试行,无并发风险则删并发用例。
|
|
9
|
+
- 每节的 > 引文说明"本节要回答什么 / 谁消费它"。
|
|
10
|
+
- 表格内的工具名为示例,替换为本项目真实技术栈。
|
|
11
|
+
- 占位符 [xxx] 替换为真实内容,方括号本身不要保留。 -->
|
|
12
|
+
|
|
7
13
|
## 1. Test Scope(测试范围)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
14
|
+
> 本节要回答:测哪些层、不测哪些。
|
|
15
|
+
|
|
16
|
+
- [按项目分层列出需覆盖的层级]
|
|
11
17
|
|
|
12
18
|
## 2. Test Strategy(测试策略)
|
|
19
|
+
> 本节要回答:每层用什么测试类型、什么工具、覆盖什么重点。
|
|
20
|
+
|
|
13
21
|
| 层级 | 测试类型 | 工具 | 覆盖重点 |
|
|
14
22
|
|---|---|---|---|
|
|
15
|
-
|
|
|
16
|
-
| Controller | 集成测试 | MockMvc / WebTestClient | 参数校验、响应格式 |
|
|
17
|
-
| 契约 | Schema 比对 | 自定义断言 | Response 与 02_interface 一致 |
|
|
23
|
+
| [层级] | [类型] | [本项目工具] | [重点] |
|
|
18
24
|
|
|
19
25
|
## 3. Test Cases(用例清单)
|
|
26
|
+
> 本节要回答:每条 AC 如何被具体用例验证——每条用例必须对齐某条 AC。
|
|
27
|
+
> 下游消费者:impl 阶段据此写测试。
|
|
20
28
|
|
|
21
29
|
| 编号 | 场景 | 输入 | 期望结果 | 对齐 AC |
|
|
22
30
|
|---|---|---|---|---|
|
|
23
|
-
| TC-01 | [正常路径] | [输入] | [期望] | AC-
|
|
24
|
-
|
|
|
25
|
-
| TC-03 | [参数异常] | [输入] | [期望错误码] | AC-2 |
|
|
26
|
-
| TC-04 | [并发场景] | [输入] | [期望] | AC-1 |
|
|
31
|
+
| TC-01 | [正常路径] | [输入] | [期望] | [AC-x] |
|
|
32
|
+
| [覆盖:主流程 + 幂等 + 失败 + 必要时并发,对齐 01 的全部 AC] | | | | |
|
|
27
33
|
|
|
28
34
|
## 4. Test Data(数据准备)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- Mock 依赖:[
|
|
35
|
+
> 本节要回答:跑测试前要准备什么数据、Mock 什么依赖。
|
|
36
|
+
|
|
37
|
+
- 前置数据 / 时间假设 / Mock 依赖:[按需填写]
|
|
32
38
|
|
|
33
39
|
## 5. Regression Impact(回归影响)
|
|
34
|
-
|
|
40
|
+
> 本节要回答:本次改动可能波及哪些现有功能、需回归什么。
|
|
41
|
+
> 依据:00_context §2 的模块关系。
|
|
42
|
+
|
|
43
|
+
- [列出需回归的现有功能]
|
|
44
|
+
|
|
45
|
+
<!-- 以下留痕仅在生成本 Spec 过程中实际触发过 spark gate 时填写;无则删除整块,不要写"无"。 -->
|
|
46
|
+
## 6. Test Decisions(测试决策留痕 · 仅 gate 触发时填写)
|
|
47
|
+
- 已确认:[自动化/手工边界、回归范围、风险覆盖优先级]
|
|
48
|
+
- 选定/放弃策略:[采用的测试组合及理由、未采用方式及原因]
|
|
49
|
+
- 仍待确认:[不阻塞 04、但执行测试前需确认的问题]
|
|
@@ -80,9 +80,15 @@ spec-canon prompt show <id> [选项] # 输出指定提示词(自动替换
|
|
|
80
80
|
# 示例: spec-canon prompt show req
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
`ctx` 内置 Explore project context preflight:生成 `00_context` 前先探索项目入口、SDD 文档、历史决策、近期提交和真实代码入口;goal 过大或无法定位主域时会先暂停澄清,普通证据缺口写入“待补充 / 待确认”。
|
|
84
|
+
|
|
85
|
+
`req`、`iface`、`impl-spec`、`test-spec` 内置 spark gate:生成正式 Spec 前会先检查关键澄清点或多方案决策;若存在阻塞问题,先暂停写文件并等待人工确认,确认后再把结果沉淀到对应 Spec。gate 强度按变更规模分级——小改动只在"会导致返工或破坏现状的硬阻塞"时暂停,迁移/灰度/回滚等问题仅复杂/跨域变更才核对,避免小需求被全套清单打断。
|
|
86
|
+
|
|
87
|
+
若当前环境已安装与本工作流配套的 `sdd-*` skills,可把它们视为这些 CLI 命令的对话入口:skill 负责路由和自动推进,底层真相仍是 `spec-canon` CLI 与 `spec-canon/` 文档。若未安装,则继续按上面的 CLI 流程执行。
|
|
88
|
+
|
|
83
89
|
若 `spec-canon` 版本升级后新增了骨架文件,优先运行 `spec-canon sync` 补齐缺失内容;该命令默认不会覆盖你已编辑过的文件。若 `spec-canon/templates/` 下的 6 个核心 Spec 模板与当前版本不一致,CLI 会同时给出 `code --diff` / `diff -u` / `cp` 建议命令,便于确认后直接同步。
|
|
84
90
|
|
|
85
|
-
`change next`
|
|
91
|
+
`change next` 会按变更规模裁剪候选:`--scale small|medium|complex` 直接指定规模(small≈`req→impl→review`、medium 增加 `iface`、complex 才铺开 `ctx/impl-spec/test-spec/domain-sync`)。省略 `--scale` 时按 type 推默认(fix/chore/docs/style/ci/test→small,feat/refactor/perf→medium,complex 不自动推导)。在规模基础上还可叠加 `--no-contract-change`、`--no-system-change`、`-d <domain>` 进一步收窄。
|
|
86
92
|
|
|
87
93
|
`change` 支持父命令选项 `-d, --dir <path>` 指定目标项目目录;由于 `change next` 已使用 `-d` 作为 `--domain`,目录参数需写在子命令前面。
|
|
88
94
|
|
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
5. 先在输出开头单独给出一行:`建议 change 名:<type>-{{CHANGE_SEQ}}-<slug>`
|
|
14
14
|
{{/if}}
|
|
15
15
|
|
|
16
|
+
先执行 Explore project context preflight:
|
|
17
|
+
1. 阅读项目入口文档:README.md、AGENTS.md 或 CLAUDE.md(若存在),了解项目定位、开发命令和本地规则。
|
|
18
|
+
2. 阅读 SDD 入口:@spec-canon/domains/README.md、相关 `domain_spec.md`、@spec-canon/rules/RULES.md、@spec-canon/decisions/AI_CHANGELOG.md(若存在)。
|
|
19
|
+
3. 查看最近 5-10 条 git log,提取与本次 goal 可能相关的近期变更、历史决策或风险信号。
|
|
20
|
+
4. 判断 goal 是否适合单个 change:若明显包含多个独立子系统或多个互不依赖目标,先停止,给出拆分建议,等待我确认后再生成 00_context。
|
|
21
|
+
5. 若无法定位主域或主模块,先停止,最多提出 3 个定位问题;不要凭空选择域。
|
|
22
|
+
6. 若只是部分证据缺失,不要停止生成;在 00_context 中标注“待补充 / 待确认”,并说明已检查过哪些来源。
|
|
23
|
+
7. 本阶段只做项目上下文探索和证据报告,不生成 01_requirement 的 AC,不设计 02_interface,不选择 03_implementation 技术方案,不制定 04_test_spec 测试策略。
|
|
24
|
+
|
|
16
25
|
然后按以下顺序工作:
|
|
17
26
|
1. 根据 goal 判断 change type,并解释判断依据
|
|
18
27
|
2. 从 goal 中提炼 slug
|
|
@@ -1,6 +1,22 @@
|
|
|
1
|
+
<!-- @include shared/spark_protocol.md -->
|
|
2
|
+
|
|
1
3
|
若存在 @spec-canon/changes/{{CHANGE}}/00_context.md,请先阅读;否则跳过。
|
|
2
4
|
阅读以下 change goal,生成 spec-canon/changes/{{CHANGE}}/01_requirement.md。
|
|
3
5
|
按 @spec-canon/templates/01_requirement.md 模板生成文档。
|
|
6
|
+
|
|
7
|
+
生成前先执行 01_requirement 阶段 gate(强度随变更规模,见 Spark Gate Protocol;小改动只核对"始终核对"项的硬阻塞):
|
|
8
|
+
|
|
9
|
+
始终核对(含小改动):
|
|
10
|
+
1. 若 goal 存在多种解释,先停止并让我确认采用哪一种解释。
|
|
11
|
+
2. 若 Scope / Out of Scope 不清,先提出边界问题,不要自行扩大范围。
|
|
12
|
+
3. 若 AC 无法验证、互相冲突,或缺少主流程 / 幂等 / 失败场景,先指出缺口并让我确认。
|
|
13
|
+
|
|
14
|
+
中等及以上变更追加核对:
|
|
15
|
+
4. 若性能、安全、兼容、依赖等关键约束缺失且会影响需求判断,先提问确认。
|
|
16
|
+
5. 若需求过大,可能需要拆成多个 change,先给出拆分建议并等待确认。
|
|
17
|
+
|
|
18
|
+
无论规模,本阶段只做需求澄清,不提前设计接口或实现方案。
|
|
19
|
+
|
|
4
20
|
要求:
|
|
5
21
|
1. AC 至少覆盖主流程 + 幂等 + 2 个失败场景
|
|
6
22
|
2. §3 Scope:若已有 00_context.md,基于其 §2 标注本次变更涉及的模块/文件;否则基于代码库自行分析
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
+
<!-- @include shared/spark_protocol.md -->
|
|
2
|
+
|
|
1
3
|
请阅读 @spec-canon/changes/{{CHANGE}}/ 下的 00_context.md + 01_requirement.md + 02_interface.md,
|
|
2
4
|
生成 03_implementation.md。
|
|
3
5
|
按 @spec-canon/templates/03_implementation.md 模板生成文档。
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
生成前先执行 03_implementation 阶段 gate(强度随变更规模,见 Spark Gate Protocol;小改动只核对"始终核对"项的硬阻塞):
|
|
8
|
+
|
|
9
|
+
始终核对(含小改动):
|
|
10
|
+
1. §2 Design Decision:参考 00_context.md §4 历史决策和 §5 已知坑位评估方案;有明确最优方案直接选定并说明理由。
|
|
11
|
+
2. 若方案与历史决策或已知坑位冲突,先说明冲突,不要绕过已有约束。
|
|
12
|
+
3. 若文件改动范围或某步骤的验证命令无法明确,先停止并提出问题。
|
|
13
|
+
|
|
14
|
+
中等及以上变更追加核对:
|
|
15
|
+
4. 若存在多个可行实现方案,先给出 2-3 个方案、trade-off、推荐方案和理由,等待我确认后再继续。
|
|
16
|
+
|
|
17
|
+
复杂 / 跨域变更追加核对:
|
|
18
|
+
5. 若数据迁移、灰度、回滚或跨域兼容策略无法明确,先停止并提出问题。
|
|
19
|
+
|
|
20
|
+
无论规模,本阶段只做实施设计、变更范围和验证路径,不直接编码。
|
|
21
|
+
|
|
7
22
|
File Changes 基于 00_context.md §2,Core Logic 规避 00_context.md §5 已知坑位。
|
|
8
23
|
每个 Step 必须有验证命令。
|
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
<!-- @include shared/spark_protocol.md -->
|
|
2
|
+
|
|
1
3
|
请阅读 @spec-canon/changes/{{CHANGE}}/ 下的 00_context.md + 01_requirement.md,
|
|
2
4
|
并扫描代码库中的真实接口入口(优先看 `Controller` / `Router` / `Handler` / 路由定义;若项目已有 OpenAPI 或接口 DTO,也一并参考),
|
|
3
5
|
生成 02_interface.md。
|
|
4
6
|
按 @spec-canon/templates/02_interface.md 模板生成文档。
|
|
5
7
|
|
|
8
|
+
生成前先执行 02_interface 阶段 gate(强度随变更规模,见 Spark Gate Protocol;小接口改动聚焦"始终核对"项的硬阻塞):
|
|
9
|
+
|
|
10
|
+
始终核对:
|
|
11
|
+
1. 若无法确认本次是否需要接口、事件、数据契约或外部可见行为变化,先停止并让我确认。
|
|
12
|
+
2. 若 01_requirement.md 的 AC 不足以推导接口行为,先要求补充或修正 01,不要自行发明契约。
|
|
13
|
+
3. 若新契约与真实代码中的既有接口风格冲突,先说明冲突和取舍方案,等待确认后再生成 02。
|
|
14
|
+
|
|
15
|
+
中等及以上变更追加核对:
|
|
16
|
+
4. 若字段语义、幂等策略、兼容策略或错误码边界有分歧,先提出阻塞契约设计的问题。
|
|
17
|
+
|
|
18
|
+
复杂 / 跨域变更追加核对:
|
|
19
|
+
5. 若 REST / RPC / 事件 / 批处理等边界形式存在多个可行方案,先给出 2-3 个方案、trade-off、推荐方案和理由。
|
|
20
|
+
|
|
21
|
+
无论规模,本阶段只做系统边界和契约设计,不提前做文件级实施计划。
|
|
22
|
+
|
|
6
23
|
要求:
|
|
7
24
|
1. 先用 00_context.md §2.3 快速定位相关已有接口,再回看真实代码入口确认路径、HTTP 方法、参数组织、请求/响应结构、鉴权方式、错误响应风格和 DTO 命名
|
|
8
25
|
2. 真实代码是接口契约的第一事实来源;00_context.md 用于汇总风格、补充背景和交叉校验
|
|
@@ -1,5 +1,22 @@
|
|
|
1
|
+
<!-- @include shared/spark_protocol.md -->
|
|
2
|
+
|
|
1
3
|
请阅读 @spec-canon/changes/{{CHANGE}}/ 下的全部 Spec,
|
|
2
4
|
生成 04_test_spec.md。
|
|
3
5
|
按 @spec-canon/templates/04_test_spec.md 模板生成文档。
|
|
6
|
+
|
|
7
|
+
生成前先执行 04_test_spec 阶段 gate(强度随变更规模,见 Spark Gate Protocol;小改动只核对"始终核对"项的硬阻塞):
|
|
8
|
+
|
|
9
|
+
始终核对(含小改动):
|
|
10
|
+
1. 若回归范围不清,先基于 00_context.md §2 标出不确定点并等待确认。
|
|
11
|
+
2. 若测试数据、Mock 策略或外部依赖处理不明确,先停止并提出问题。
|
|
12
|
+
3. 若发现 01 / 02 / 03 之间存在影响测试设计的上游 Spec 矛盾,先停止并指出应回到哪个文档修正,不要在 04 中自行改写需求、接口或实施方案。
|
|
13
|
+
|
|
14
|
+
中等及以上变更追加核对:
|
|
15
|
+
4. 若自动化测试和手工验收的边界未定,先提出测试投入取舍问题。
|
|
16
|
+
5. 若风险覆盖优先级需要人工取舍,先列出风险和建议覆盖顺序。
|
|
17
|
+
|
|
18
|
+
复杂 / 跨域变更追加核对:
|
|
19
|
+
6. 若是否需要契约测试、迁移测试、性能测试或并发测试存在分歧,先给出 2-3 个测试策略方案、trade-off、推荐方案和理由。
|
|
20
|
+
|
|
4
21
|
Regression Impact 基于 00_context.md §2 的模块关系推导,
|
|
5
22
|
边界用例参考 00_context.md §5 的已知坑位。
|
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
请基于 @spec-canon/changes/{{CHANGE}}/ 下实际存在的 Spec 文档执行编码,并遵循以下优先级与停机规则:
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
1. 若存在 @spec-canon/changes/{{CHANGE}}/03_implementation.md
|
|
4
|
+
- 以 03_implementation.md 作为施工图执行:改动范围、步骤顺序、验证方式默认以 03 为准
|
|
5
|
+
- 若存在 02_interface.md,以其校验接口 / 数据契约;若存在 01_requirement.md,以其校验 AC、Scope 和非目标
|
|
6
|
+
- 若 03 与已存在的 01 / 02 或真实代码冲突,或 03 不足以确定文件范围、步骤顺序,或在结合已存在的 04_test_spec.md 后仍不足以确定验证方式,先停止并报告差异,不要自行猜测继续编码
|
|
7
|
+
- 若存在 00_context.md,可补读其相关段落(历史决策、已知坑位、依赖影响)辅助判断;若仍有歧义,建议先回补或修正 03
|
|
8
|
+
- 按 03 中的 Step 列表逐步执行;每步完成后优先运行该步骤在 03 中给出的验证命令。若 03 未写当前步骤的验证命令且存在 04_test_spec.md,优先执行 04 中与当前步骤相关的测试、回归和边界验证。只有当 03 与 04 都未给出可执行验证时,才补最小必要验证并说明理由。验证通过后报告,然后我确认是否继续下一步
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
2. 若存在 @spec-canon/changes/{{CHANGE}}/02_interface.md 但无 03
|
|
11
|
+
- 以 02_interface.md 作为契约约束,以 01_requirement.md 作为验收与范围约束,并结合真实代码定位实际修改入口
|
|
12
|
+
- 在开始编码前,先输出一个“临时执行计划”,至少包含:计划修改的文件 / 模块、步骤顺序、每步验证命令、关键假设或风险
|
|
13
|
+
- 先等待我确认临时执行计划,再开始第一步编码;不要一边规划一边直接改代码
|
|
14
|
+
- 若无法从 01 + 02 + 代码库安全推出文件范围、核心方案或验证方式,或发现涉及陌生模块、跨域依赖、历史坑位不清晰的高风险改动,则停止并建议先补 03_implementation.md;若存在 00_context.md,可先补读后再判断
|
|
15
|
+
- 开始执行后按步推进,每步完成后运行对应验证,通过后报告,然后我确认是否继续下一步
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
3. 若只有 @spec-canon/changes/{{CHANGE}}/01_requirement.md
|
|
18
|
+
- 以 01_requirement.md 的 AC、Scope、Constraints 作为唯一显式约束,并结合真实代码定位修改入口
|
|
19
|
+
- 在开始编码前,先输出一个“最小临时计划”,至少包含:计划修改的文件 / 模块、步骤顺序、每步验证命令、关键假设
|
|
20
|
+
- 先等待我确认最小临时计划,再开始第一步编码
|
|
21
|
+
- 若实现过程中发现接口 / 数据模型变更信号,应停止并建议先补 02_interface.md;若文件范围、风险或执行顺序仍不清晰,应停止并建议补 00_context.md 或 03_implementation.md
|
|
22
|
+
- 开始执行后按步推进,每步完成后运行对应验证,通过后报告,然后我确认是否继续下一步
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## 决策树:选择正确的提示词序列
|
|
4
4
|
|
|
5
|
+
`req`、`iface`、`impl-spec`、`test-spec` 内置 spark gate:生成正式 Spec 前会先检查是否存在关键澄清点或多方案决策。若存在,AI 会暂停写文件,先提出问题或方案比较,等待人工确认后再继续;这不是新增流程节点,而是 01-04 文档生成前的强制澄清协议。
|
|
6
|
+
|
|
7
|
+
`ctx` 内置 Explore project context preflight:生成 00_context 前会先探索项目入口、SDD 文档、历史决策、近期提交和真实代码入口。只有 goal 过大或无法定位主域时才暂停澄清;普通证据缺口会写入“待补充 / 待确认”。
|
|
8
|
+
|
|
5
9
|
```
|
|
6
10
|
你的项目处于什么阶段?
|
|
7
11
|
│
|
|
@@ -86,6 +90,7 @@
|
|
|
86
90
|
- **复杂需求**:完整流程 change start → domain-sync
|
|
87
91
|
|
|
88
92
|
> `(ctx →)` 表示可选:若 AI 需要理解系统现状(如涉及陌生模块或跨域依赖),小需求 / 中等需求也应前置 `ctx`。
|
|
93
|
+
> 01-04 的 spark gate 只在关键问题未确认时暂停;若输入已经明确,会直接生成目标 Spec。
|
|
89
94
|
> 若确认要复用已有历史内容的 change 目录,需显式使用 `change start ... --reuse-change --yes`;默认会拒绝复用以避免上下文污染。
|
|
90
95
|
|
|
91
96
|
补充:如果不想手动判断当前该跑哪一步,可在任一阶段执行 `spec-canon change next`。该命令默认输出候选步骤列表,而不是唯一推荐;如确认没有接口变化或无需系统级归档,可用 `--no-contract-change`、`--no-system-change` 收窄。
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## Spark Gate Protocol(适用于 01-04)
|
|
2
|
+
|
|
3
|
+
### 第一步:按变更规模定 gate 强度
|
|
4
|
+
|
|
5
|
+
先判断本次变更规模——依据 change 名中的类型前缀(feat/fix/refactor/perf/chore/ci/docs/style/test)、`00_context`(若有),以及裁剪三问(是否需理解系统现状 / 是否动接口或数据模型 / 是否产生系统级知识):
|
|
6
|
+
|
|
7
|
+
- **小改动**(小 bugfix / 小需求 / docs / style / 单点修复):只在出现"会导致返工或破坏现状的硬阻塞"时才停——需求有歧义、改动与现状直接冲突、上游 Spec 矛盾。不要为可选优化、未来扩展或不影响本次结果的边角问题提问。
|
|
8
|
+
- **中等改动**:在硬阻塞之外,额外就接口/数据契约的关键分歧提问。
|
|
9
|
+
- **复杂 / 跨域改动**:执行完整 gate,含数据迁移、灰度、回滚、跨域兼容等。
|
|
10
|
+
|
|
11
|
+
宁可少问、问到点子上,也不要用一长串清单打断小改动。每个阶段 gate 的条目已标注适用强度,按本次规模只核对对应层级。
|
|
12
|
+
|
|
13
|
+
### 第二步:判断该强度下是否存在关键澄清点
|
|
14
|
+
|
|
15
|
+
若不存在,直接继续生成目标 Spec。
|
|
16
|
+
|
|
17
|
+
若存在关键澄清点:
|
|
18
|
+
1. 停止生成或修改目标 Spec 文件。
|
|
19
|
+
2. 最多提出 3 个会阻塞当前 Spec 的问题。
|
|
20
|
+
3. 若是方案取舍,给出 2-3 个方案、trade-off、推荐方案和理由。
|
|
21
|
+
4. 等用户确认后,再将确认结果写入目标 Spec。
|
|
22
|
+
|
|
23
|
+
用户确认前,不要生成目标 Spec 文件。
|
|
24
|
+
不要创建独立 spark 文档作为事实源;确认结果必须沉淀到当前目标 Spec。
|
|
25
|
+
若发现上游 Spec 矛盾,停止并指出应回到哪个上游文档修正。
|