cdspec 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/AGENTS.md +14 -0
  2. package/CLAUDE.md +10 -0
  3. package/README.md +55 -0
  4. package/cdspec.config.yaml +34 -0
  5. package/dist/cli.js +94 -0
  6. package/dist/config/default.js +48 -0
  7. package/dist/config/loader.js +30 -0
  8. package/dist/config/path.js +11 -0
  9. package/dist/config/types.js +1 -0
  10. package/dist/skill-core/adapters/claudecode-adapter.js +35 -0
  11. package/dist/skill-core/adapters/codex-adapter.js +28 -0
  12. package/dist/skill-core/adapters/iflow-adapter.js +39 -0
  13. package/dist/skill-core/adapters/index.js +34 -0
  14. package/dist/skill-core/adapters/shared.js +36 -0
  15. package/dist/skill-core/agent-config.js +40 -0
  16. package/dist/skill-core/manifest-loader.js +63 -0
  17. package/dist/skill-core/scaffold.js +169 -0
  18. package/dist/skill-core/service.js +156 -0
  19. package/dist/skill-core/tool-interactions.js +70 -0
  20. package/dist/skill-core/types.js +1 -0
  21. package/dist/skill-core/validator.js +25 -0
  22. package/dist/task-core/parser.js +70 -0
  23. package/dist/task-core/service.js +28 -0
  24. package/dist/task-core/storage.js +159 -0
  25. package/dist/task-core/types.js +1 -0
  26. package/dist/utils/frontmatter.js +40 -0
  27. package/dist/utils/fs.js +37 -0
  28. package/package.json +29 -0
  29. package/src/cli.ts +105 -0
  30. package/src/config/default.ts +51 -0
  31. package/src/config/loader.ts +37 -0
  32. package/src/config/path.ts +13 -0
  33. package/src/config/types.ts +22 -0
  34. package/src/skill-core/adapters/claudecode-adapter.ts +45 -0
  35. package/src/skill-core/adapters/codex-adapter.ts +36 -0
  36. package/src/skill-core/adapters/iflow-adapter.ts +49 -0
  37. package/src/skill-core/adapters/index.ts +39 -0
  38. package/src/skill-core/adapters/shared.ts +45 -0
  39. package/src/skill-core/manifest-loader.ts +79 -0
  40. package/src/skill-core/scaffold.ts +192 -0
  41. package/src/skill-core/service.ts +199 -0
  42. package/src/skill-core/tool-interactions.ts +95 -0
  43. package/src/skill-core/types.ts +22 -0
  44. package/src/skill-core/validator.ts +28 -0
  45. package/src/task-core/parser.ts +89 -0
  46. package/src/task-core/service.ts +49 -0
  47. package/src/task-core/storage.ts +177 -0
  48. package/src/task-core/types.ts +15 -0
  49. package/src/types/yaml.d.ts +4 -0
  50. package/src/utils/frontmatter.ts +55 -0
  51. package/src/utils/fs.ts +41 -0
  52. package/templates/design-doc/SKILL.md +99 -0
  53. package/templates/design-doc/agents/openai.yaml +4 -0
  54. package/templates/design-doc/references//345/237/272/347/272/277/346/250/241/346/235/277.md +46 -0
  55. package/templates/design-doc/references//345/242/236/351/207/217/351/234/200/346/261/202/346/250/241/346/235/277.md +32 -0
  56. package/templates/design-doc/references//345/275/222/346/241/243/346/243/200/346/237/245/346/270/205/345/215/225.md +15 -0
  57. package/templates/design-doc/references//347/224/237/344/272/247/345/267/245/345/215/225/345/237/272/347/272/277/347/244/272/344/276/213.md +470 -0
  58. package/templates/design-doc/scripts/validate_doc_layout.sh +49 -0
  59. package/templates/frontend-develop-standard/SKILL.md +63 -0
  60. package/templates/frontend-develop-standard/agents/openai.yaml +4 -0
  61. package/templates/frontend-develop-standard/references/frontend_develop_standard.md +749 -0
  62. package/templates/standards-backend/SKILL.md +55 -0
  63. package/templates/standards-backend/agents/openai.yaml +4 -0
  64. package/templates/standards-backend/references/DDD/346/236/266/346/236/204/347/272/246/346/235/237.md +103 -0
  65. package/templates/standards-backend/references/JUC/345/271/266/345/217/221/350/247/204/350/214/203.md +232 -0
  66. package/templates/standards-backend/references//344/274/240/347/273/237/344/270/211/345/261/202/346/236/266/346/236/204/347/272/246/346/235/237.md +35 -0
  67. package/templates/standards-backend/references//345/220/216/347/253/257/345/274/200/345/217/221/350/247/204/350/214/203.md +49 -0
  68. package/templates/standards-backend/references//346/225/260/346/215/256/345/272/223/350/256/276/350/256/241/350/247/204/350/214/203.md +116 -0
  69. package/templates/standards-backend/references//350/256/276/350/256/241/346/250/241/345/274/217/350/220/275/345/234/260/346/211/213/345/206/214.md +395 -0
  70. package/tests/skill.test.ts +191 -0
  71. package/tests/task.test.ts +55 -0
  72. package/tsconfig.json +16 -0
  73. package/vitest.config.ts +9 -0
@@ -0,0 +1,177 @@
1
+ import { readFile, rm, writeFile, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ensureDir, pathExists } from "../utils/fs.js";
4
+ import { parseFrontmatter, stringifyFrontmatter } from "../utils/frontmatter.js";
5
+ import { ArchiveRecord, TaskItem, TaskStatus } from "./types.js";
6
+
7
+ const ROOT_DIR = ".cdspec";
8
+ const TASKS_DIR = "tasks";
9
+ const ARCHIVE_DIR = "archive";
10
+
11
+ export function taskDir(cwd: string): string {
12
+ return path.join(cwd, ROOT_DIR, TASKS_DIR);
13
+ }
14
+
15
+ export function archiveDir(cwd: string): string {
16
+ return path.join(cwd, ROOT_DIR, ARCHIVE_DIR);
17
+ }
18
+
19
+ export async function initTaskWorkspace(cwd: string): Promise<void> {
20
+ await ensureDir(taskDir(cwd));
21
+ await ensureDir(archiveDir(cwd));
22
+ }
23
+
24
+ export async function saveTask(cwd: string, task: TaskItem): Promise<void> {
25
+ await initTaskWorkspace(cwd);
26
+ const targetFile = path.join(taskDir(cwd), `${task.id}.md`);
27
+ const body = `# ${task.title}\n\n- status: ${task.status}\n- source: ${task.source}\n`;
28
+ const content = stringifyFrontmatter(
29
+ {
30
+ id: task.id,
31
+ title: task.title,
32
+ status: task.status,
33
+ source: task.source,
34
+ created_at: task.createdAt,
35
+ updated_at: task.updatedAt
36
+ },
37
+ body
38
+ );
39
+ await writeFile(targetFile, content, "utf8");
40
+ }
41
+
42
+ export async function loadTask(cwd: string, id: string): Promise<TaskItem | null> {
43
+ const file = path.join(taskDir(cwd), `${id}.md`);
44
+ if (!(await pathExists(file))) return null;
45
+ const raw = await readFile(file, "utf8");
46
+ const parsed = parseFrontmatter(raw);
47
+ return {
48
+ id: parsed.attributes.id,
49
+ title: parsed.attributes.title,
50
+ status: parseStatus(parsed.attributes.status),
51
+ source: parsed.attributes.source,
52
+ createdAt: parsed.attributes.created_at,
53
+ updatedAt: parsed.attributes.updated_at
54
+ };
55
+ }
56
+
57
+ export async function updateTaskStatus(
58
+ cwd: string,
59
+ id: string,
60
+ next: TaskStatus
61
+ ): Promise<TaskItem> {
62
+ const current = await loadTask(cwd, id);
63
+ if (!current) {
64
+ throw new Error(`Task "${id}" not found.`);
65
+ }
66
+ if (!canTransition(current.status, next)) {
67
+ throw new Error(`Invalid status transition: ${current.status} -> ${next}.`);
68
+ }
69
+ const updated: TaskItem = {
70
+ ...current,
71
+ status: next,
72
+ updatedAt: new Date().toISOString()
73
+ };
74
+ await saveTask(cwd, updated);
75
+ return updated;
76
+ }
77
+
78
+ export async function archiveTask(cwd: string, id: string): Promise<ArchiveRecord> {
79
+ const current = await loadTask(cwd, id);
80
+ if (!current) {
81
+ throw new Error(`Task "${id}" not found.`);
82
+ }
83
+ if (current.status !== "done") {
84
+ throw new Error(`Task "${id}" must be done before archive.`);
85
+ }
86
+
87
+ const archived: ArchiveRecord = {
88
+ ...current,
89
+ archivedAt: new Date().toISOString()
90
+ };
91
+ const sourceFile = path.join(taskDir(cwd), `${id}.md`);
92
+ const archiveFile = path.join(archiveDir(cwd), `${id}.md`);
93
+ const body = `# ${archived.title}\n\nArchived from ${sourceFile.replaceAll("\\", "/")}.\n`;
94
+ const content = stringifyFrontmatter(
95
+ {
96
+ id: archived.id,
97
+ title: archived.title,
98
+ status: archived.status,
99
+ source: archived.source,
100
+ created_at: archived.createdAt,
101
+ updated_at: archived.updatedAt,
102
+ archived_at: archived.archivedAt
103
+ },
104
+ body
105
+ );
106
+ await writeFile(archiveFile, content, "utf8");
107
+ await rm(sourceFile, { force: true });
108
+ return archived;
109
+ }
110
+
111
+ export async function refreshTaskIndex(cwd: string): Promise<void> {
112
+ await initTaskWorkspace(cwd);
113
+ const tasks = await loadByDir(taskDir(cwd));
114
+ const grouped = {
115
+ todo: tasks.filter((task) => task.status === "todo"),
116
+ in_progress: tasks.filter((task) => task.status === "in_progress"),
117
+ done: tasks.filter((task) => task.status === "done")
118
+ };
119
+ const lines = [
120
+ "# Task Index",
121
+ "",
122
+ "## todo",
123
+ ...grouped.todo.map((task) => `- [${task.id}] ${task.title}`),
124
+ "",
125
+ "## in_progress",
126
+ ...grouped.in_progress.map((task) => `- [${task.id}] ${task.title}`),
127
+ "",
128
+ "## done",
129
+ ...grouped.done.map((task) => `- [${task.id}] ${task.title}`),
130
+ ""
131
+ ];
132
+ await writeFile(path.join(taskDir(cwd), "index.md"), lines.join("\n"), "utf8");
133
+ }
134
+
135
+ export async function refreshArchiveIndex(cwd: string): Promise<void> {
136
+ await initTaskWorkspace(cwd);
137
+ const records = await loadByDir(archiveDir(cwd));
138
+ const lines = [
139
+ "# Archive Index",
140
+ "",
141
+ ...records.map((record) => `- [${record.id}] ${record.title}`),
142
+ ""
143
+ ];
144
+ await writeFile(path.join(archiveDir(cwd), "index.md"), lines.join("\n"), "utf8");
145
+ }
146
+
147
+ async function loadByDir(dirPath: string): Promise<TaskItem[]> {
148
+ if (!(await pathExists(dirPath))) return [];
149
+ const entries = await readdir(dirPath, { withFileTypes: true });
150
+ const result: TaskItem[] = [];
151
+ for (const entry of entries) {
152
+ if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name === "index.md") continue;
153
+ const raw = await readFile(path.join(dirPath, entry.name), "utf8");
154
+ const parsed = parseFrontmatter(raw);
155
+ result.push({
156
+ id: parsed.attributes.id,
157
+ title: parsed.attributes.title,
158
+ status: parseStatus(parsed.attributes.status),
159
+ source: parsed.attributes.source,
160
+ createdAt: parsed.attributes.created_at,
161
+ updatedAt: parsed.attributes.updated_at
162
+ });
163
+ }
164
+ return result;
165
+ }
166
+
167
+ function parseStatus(input: string): TaskStatus {
168
+ if (input === "todo" || input === "in_progress" || input === "done") return input;
169
+ return "todo";
170
+ }
171
+
172
+ function canTransition(current: TaskStatus, next: TaskStatus): boolean {
173
+ if (current === next) return true;
174
+ if (current === "todo" && next === "in_progress") return true;
175
+ if (current === "in_progress" && next === "done") return true;
176
+ return false;
177
+ }
@@ -0,0 +1,15 @@
1
+ export type TaskStatus = "todo" | "in_progress" | "done";
2
+
3
+ export interface TaskItem {
4
+ id: string;
5
+ title: string;
6
+ status: TaskStatus;
7
+ source: string;
8
+ createdAt: string;
9
+ updatedAt: string;
10
+ }
11
+
12
+ export interface ArchiveRecord extends TaskItem {
13
+ archivedAt: string;
14
+ }
15
+
@@ -0,0 +1,4 @@
1
+ declare module "yaml" {
2
+ export function parse(input: string): unknown;
3
+ }
4
+
@@ -0,0 +1,55 @@
1
+ export interface FrontmatterResult {
2
+ attributes: Record<string, string>;
3
+ body: string;
4
+ }
5
+
6
+ export function parseFrontmatter(content: string): FrontmatterResult {
7
+ const normalized = content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
8
+ if (!normalized.startsWith("---\n")) {
9
+ return { attributes: {}, body: normalized };
10
+ }
11
+
12
+ const endIndex = normalized.indexOf("\n---\n", 4);
13
+ if (endIndex === -1) {
14
+ return { attributes: {}, body: normalized };
15
+ }
16
+
17
+ const header = normalized.slice(4, endIndex);
18
+ const body = normalized.slice(endIndex + 5);
19
+ const attributes: Record<string, string> = {};
20
+
21
+ for (const rawLine of header.split("\n")) {
22
+ const line = rawLine.trim();
23
+ if (!line || line.startsWith("#")) continue;
24
+ const idx = line.indexOf(":");
25
+ if (idx <= 0) continue;
26
+
27
+ const key = line.slice(0, idx).trim();
28
+ let value = line.slice(idx + 1).trim();
29
+ value = stripQuotes(value);
30
+ attributes[key] = value;
31
+ }
32
+
33
+ return { attributes, body };
34
+ }
35
+
36
+ export function stringifyFrontmatter(
37
+ attributes: Record<string, string>,
38
+ body: string
39
+ ): string {
40
+ const lines = Object.entries(attributes).map(([key, value]) => {
41
+ const escaped = value.replace(/"/g, '\\"');
42
+ return `${key}: "${escaped}"`;
43
+ });
44
+ return `---\n${lines.join("\n")}\n---\n\n${body.trimEnd()}\n`;
45
+ }
46
+
47
+ function stripQuotes(value: string): string {
48
+ if (
49
+ (value.startsWith('"') && value.endsWith('"')) ||
50
+ (value.startsWith("'") && value.endsWith("'"))
51
+ ) {
52
+ return value.slice(1, -1);
53
+ }
54
+ return value;
55
+ }
@@ -0,0 +1,41 @@
1
+ import { mkdir, readdir, rm, stat, copyFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export async function ensureDir(dirPath: string): Promise<void> {
5
+ await mkdir(dirPath, { recursive: true });
6
+ }
7
+
8
+ export async function pathExists(target: string): Promise<boolean> {
9
+ try {
10
+ await stat(target);
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ export async function emptyDir(dirPath: string): Promise<void> {
18
+ await rm(dirPath, { recursive: true, force: true });
19
+ await mkdir(dirPath, { recursive: true });
20
+ }
21
+
22
+ export async function copyDir(srcDir: string, dstDir: string): Promise<void> {
23
+ await ensureDir(dstDir);
24
+ const entries = await readdir(srcDir, { withFileTypes: true });
25
+ for (const entry of entries) {
26
+ const srcPath = path.join(srcDir, entry.name);
27
+ const dstPath = path.join(dstDir, entry.name);
28
+ if (entry.isDirectory()) {
29
+ await copyDir(srcPath, dstPath);
30
+ continue;
31
+ }
32
+ await copyFile(srcPath, dstPath);
33
+ }
34
+ }
35
+
36
+ export async function listDirs(dirPath: string): Promise<string[]> {
37
+ if (!(await pathExists(dirPath))) return [];
38
+ const entries = await readdir(dirPath, { withFileTypes: true });
39
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
40
+ }
41
+
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: design-doc
3
+ description: 管理业务详细设计文档的全生命周期:首次基线、增量 feature 文档、归档合并。适用于首次开发与迭代改造,避免小改动频繁新建基线版本文件,同时保留 feature 变更历史。
4
+ ---
5
+
6
+ # 详细设计文档规范
7
+
8
+ ## 何时使用
9
+ 当用户要做以下事情时使用本技能:
10
+ 1. 首次建立某业务的详细设计基线文档。
11
+ 2. 在既有业务上做增量改造,需要单独写 feature 设计文档。
12
+ 3. 需求上线后把 feature 合并回基线,并保留 feature 历史。
13
+
14
+ ## 目标
15
+ 1. 基线文档始终保持“当前真实设计”。
16
+ 2. feature 文档只描述“增量变化”,避免重复拷贝全量内容。
17
+ 3. 小改动默认不新建基线版本文件,直接更新基线。
18
+ 4. 归档后既能追溯 feature 历史,也能一眼看到当前基线。
19
+
20
+ ## 目录规范(硬约束)
21
+ 以 `docs/<业务>/` 为根目录,固定结构如下(必须遵守):
22
+
23
+ ```text
24
+ docs/<业务>/
25
+ baseline-<业务>.md
26
+ feature/
27
+ feature-YYYYMMDD-<slug>.md
28
+ archive-log.md
29
+ ```
30
+
31
+ 说明:
32
+ 1. `baseline-<业务>.md`:唯一基线文档,持续维护。
33
+ 2. `feature/*.md`:每个迭代一个 feature 文档。
34
+ 3. `archive-log.md`:记录每个 feature 的归档时间、归档人、合并摘要。
35
+ 4. 同一个业务只允许一个目录,例如“生产订单”只能落在 `docs/生产订单/`。
36
+ 5. 本技能产出的业务文档禁止写到 `docs/` 根目录或其他目录。
37
+ 6. 同一个业务目录下只允许一个基线文件:`baseline-<业务>.md`。
38
+
39
+ ## 路径保障规则(部门推广版)
40
+ 1. 当用户指定某业务时,先确定业务目录:`docs/<业务>/`。
41
+ 2. 若目录不存在:创建目录及标准骨架(baseline、feature、archive-log)。
42
+ 3. 若目录已存在:只允许在该目录内新增/修改,不得跨目录写入。
43
+ 4. 若发现同业务多份 baseline(如 `baseline-<业务>-v2.md` 与 `baseline-<业务>.md` 并存):
44
+ - 默认停止写入并先与用户确认;
45
+ - 仅在用户明确同意“里程碑双基线”时继续。
46
+ 5. feature 文件必须写到 `docs/<业务>/feature/feature-YYYYMMDD-<slug>.md`。
47
+ 6. 归档日志必须写到 `docs/<业务>/archive-log.md`。
48
+
49
+ ## 标准流程
50
+
51
+ ### A. 首次开发(建立基线)
52
+ 1. 创建 `baseline-<业务>.md`,按基线模板填写。
53
+ 2. 创建 `archive-log.md`,初始化为空日志。
54
+ 3. 若已有历史文档,先整理为一份“可读、可执行”的基线后再进入增量模式。
55
+
56
+ 基线模板见:`references/基线模板.md`
57
+ 基线示例见:`references/生产工单基线示例.md`
58
+
59
+ ### B. 增量开发(创建 feature)
60
+ 1. 新建 `feature/feature-YYYYMMDD-<slug>.md`。
61
+ 2. 只写变更内容,不重复整份基线。
62
+ 3. 每个变更项都要指向基线对应章节(例如:`baseline-生产工单.md#2.1`)。
63
+ 4. feature 文档至少覆盖:数据库变化、接口变化、关键方法伪代码变化。
64
+
65
+ feature 模板见:`references/增量需求模板.md`
66
+
67
+ ### C. 归档合并(上线后执行)
68
+ 1. 将 feature 中已验收内容合并到 `baseline-<业务>.md`。
69
+ 2. 在 feature 文档头部更新归档元数据:`status=archived`、`merged_to`、`merged_at`。
70
+ 3. 向 `archive-log.md` 追加一条归档记录。
71
+ 4. feature 文件保留,不删除。
72
+
73
+ 归档清单见:`references/归档检查清单.md`
74
+
75
+ ## 版本与变更策略
76
+ 1. 小改动:直接改 `baseline-<业务>.md`,不新建基线版本文件。
77
+ 2. 中改动:走 feature -> 合并归档流程。
78
+ 3. 大改动(架构重构/跨域重排):允许打里程碑版本(如 `baseline-<业务>-v2.md`),但需在 `archive-log.md` 记录原因。
79
+
80
+ ## 执行约束
81
+ 1. 任何改动先更新文档,再推进代码。
82
+ 2. 接口定义优先使用 `jsonc` 代码块示例,避免重复维护表格。
83
+ 3. 索引策略保持克制:优先关联 ID 索引,避免无效堆叠。
84
+ 4. 文档中的英文方法名与代码方法名必须一一对应。
85
+ 5. 文档不写个人本地绝对路径(如 `/Users/...`),代码来源仅写仓库/模块名称。
86
+ 6. 文档不强绑定“单控制器基线”,接口按业务路由分组展示即可。
87
+ 7. `archive-log.md` 的“合并人”必须使用真实 `git config user.name`。
88
+ 8. 写入前必须先做目录校验,校验通过后再落文档(可执行 `scripts/validate_doc_layout.sh <业务>`)。
89
+
90
+ ## 输出要求(对 AI)
91
+ 1. 当用户要求“增量改造”时,默认生成 feature 文档,不复制全量基线。
92
+ 2. 当用户要求“归档”时,默认执行:
93
+ - 更新 baseline
94
+ - 更新 feature 状态
95
+ - 追加 archive-log
96
+ 3. 默认不新建新的基线版本文件,除非用户明确要求“里程碑重置基线”。
97
+ 4. 生成文档时先对齐 `references/生产工单基线示例.md` 的表达风格,再落模板内容。
98
+ 5. 每次执行后必须明确输出本次写入的目标路径清单(至少 baseline/feature/archive-log 之一)。
99
+ 6. 若路径不符合 `docs/<业务>/` 规则,必须先纠正路径再执行,不得直接写入。
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "详细设计文档规范"
3
+ short_description: "管理业务详细设计文档全生命周期:基线、feature、归档"
4
+ default_prompt: "使用 $design-doc-lifecycle 按目录约束完成详细设计文档的基线建立、增量 feature 文档编写与归档合并。"
@@ -0,0 +1,46 @@
1
+ # <业务>详细设计(基线)
2
+
3
+ > 目录约束:本文件必须位于 `docs/<业务>/baseline-<业务>.md`,且同业务目录下仅此一份 baseline。
4
+
5
+ ## 1. 文档信息
6
+ - 业务:<业务>
7
+ - 状态:active
8
+ - 最后更新:<YYYY-MM-DD>
9
+
10
+ ## 2. 数据库设计
11
+ ### 2.1 核心表字段
12
+ - 表A(字段、类型、非空、默认值、字典key、说明)
13
+ - 表B(字段、类型、非空、默认值、字典key、说明)
14
+
15
+ ### 2.2 索引与约束
16
+ - 只保留必要索引,优先关联ID索引。
17
+
18
+ ### 2.3 字典项说明
19
+ - key -> values
20
+
21
+ ## 3. 关键前后端接口定义(JSON 示例)
22
+ ### 3.1 <接口名>
23
+ 入参示例:
24
+ ```jsonc
25
+ {
26
+ "fieldA": "value" // 说明
27
+ }
28
+ ```
29
+ 出参示例:
30
+ ```jsonc
31
+ {
32
+ "success": true,
33
+ "result": {
34
+ "fieldB": "value" // 说明
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## 4. 关键方法清单
40
+ - 中文动作 -> 英文方法名 -> 签名
41
+
42
+ ## 5. 关键方法伪代码
43
+ ### 5.1 <methodName>(<中文动作>)
44
+ ```text
45
+ # 关键流程伪代码(中文注释)
46
+ ```
@@ -0,0 +1,32 @@
1
+ # <业务>增量设计(feature)
2
+
3
+ > 目录约束:本文件必须位于 `docs/<业务>/feature/feature-YYYYMMDD-<slug>.md`。
4
+
5
+ ## 1. 元数据
6
+ - feature_key: feature-<YYYYMMDD>-<slug>
7
+ - status: draft
8
+ - baseline: docs/<业务>/baseline-<业务>.md
9
+ - created_at: <YYYY-MM-DD>
10
+ - merged_to: <归档后填写>
11
+ - merged_at: <归档后填写>
12
+
13
+ ## 2. 变更背景
14
+ - 为什么要改。
15
+
16
+ ## 3. 数据库增量
17
+ - 基线章节引用:<如 baseline-xxx.md#2.1>
18
+ - 新增/修改/删除字段与索引。
19
+
20
+ ## 4. 接口增量(JSON 示例)
21
+ - 基线章节引用:<如 baseline-xxx.md#3.1>
22
+ - 只写变化接口。
23
+
24
+ ## 5. 方法增量
25
+ - 基线章节引用:<如 baseline-xxx.md#5.2>
26
+ - 只写变化的方法签名与伪代码。
27
+
28
+ ## 6. 合并清单(归档前打勾)
29
+ - [ ] 已合并数据库变化到基线
30
+ - [ ] 已合并接口变化到基线
31
+ - [ ] 已合并方法变化到基线
32
+ - [ ] 已更新 feature 状态为 archived
@@ -0,0 +1,15 @@
1
+ # 归档操作清单
2
+
3
+ 0. 先执行目录校验:`scripts/validate_doc_layout.sh <业务>`。
4
+ 1. 打开目标 feature 文档,确认状态为 `draft` 且已验收。
5
+ 2. 按 feature 内容逐项合并到基线文档。
6
+ 3. 更新 feature 元数据:
7
+ - `status: archived`
8
+ - `merged_to: docs/<业务>/baseline-<业务>.md`
9
+ - `merged_at: <YYYY-MM-DD>`
10
+ 4. 在 `archive-log.md` 追加记录:
11
+ - feature_key
12
+ - 合并日期
13
+ - 合并人(取 `git config user.name`)
14
+ - 摘要(数据库/接口/方法)
15
+ 5. 自检:基线是否已包含所有变更,feature 是否保留原文。