@loviusc/sa-spec 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.
- package/bin/sa-spec.js +3 -0
- package/package.json +24 -0
- package/readme.md +593 -0
- package/src/cli.js +362 -0
- package/templates/AGENTS.md +90 -0
- package/templates/project/index.md +4 -0
- package/templates/project/req/db-schema.md +9 -0
- package/templates/project/req/handoff.md +33 -0
- package/templates/project/req/history.md +4 -0
- package/templates/project/req/openapi.yaml +5 -0
- package/templates/project/req/srs.md +26 -0
- package/templates/project/req/uml.puml +14 -0
- package/templates/project/system-spec.md +15 -0
- package/templates/readme.md +593 -0
- package/templates/rules/db-table.md +82 -0
- package/templates/rules/estimation.md +58 -0
- package/templates/rules/file-style.md +49 -0
- package/templates/rules/history.md +34 -0
- package/templates/rules/review.md +42 -0
- package/templates/rules/spec.md +44 -0
- package/templates/rules/system-spec.md +53 -0
- package/templates/rules/uml-style.md +27 -0
- package/templates/rules/wireframe.md +81 -0
- package/templates/skills/system-analysis/SKILL.md +141 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
|
|
5
|
+
const cwd = process.cwd();
|
|
6
|
+
const rootDir = path.resolve(__dirname, "..");
|
|
7
|
+
const templatesDir = path.join(rootDir, "templates");
|
|
8
|
+
|
|
9
|
+
function main() {
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const command = args[0];
|
|
12
|
+
|
|
13
|
+
switch (command) {
|
|
14
|
+
case "init":
|
|
15
|
+
initProject(args.slice(1));
|
|
16
|
+
return;
|
|
17
|
+
case "new":
|
|
18
|
+
createReqTemplate(args.slice(1));
|
|
19
|
+
return;
|
|
20
|
+
case "check":
|
|
21
|
+
runCheck();
|
|
22
|
+
return;
|
|
23
|
+
case "doctor":
|
|
24
|
+
runDoctor();
|
|
25
|
+
return;
|
|
26
|
+
case "status":
|
|
27
|
+
showStatus();
|
|
28
|
+
return;
|
|
29
|
+
case "template":
|
|
30
|
+
runTemplate(args.slice(1));
|
|
31
|
+
return;
|
|
32
|
+
case "help":
|
|
33
|
+
case "--help":
|
|
34
|
+
case "-h":
|
|
35
|
+
case undefined:
|
|
36
|
+
showHelp();
|
|
37
|
+
return;
|
|
38
|
+
default:
|
|
39
|
+
fail(`Unknown command: ${command}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function initProject(args = []) {
|
|
44
|
+
const includeRules = args.includes("--full");
|
|
45
|
+
const results = [
|
|
46
|
+
ensureGitRepository(),
|
|
47
|
+
ensureDir("skills"),
|
|
48
|
+
ensureDir(path.join("skills", "system-analysis")),
|
|
49
|
+
ensureDir("project"),
|
|
50
|
+
ensureFileFromTemplate("AGENTS.md", "AGENTS.md"),
|
|
51
|
+
ensureFileFromTemplate("readme.md", "readme.md"),
|
|
52
|
+
ensureFileFromTemplate(path.join("skills", "system-analysis", "SKILL.md"), path.join("skills", "system-analysis", "SKILL.md")),
|
|
53
|
+
ensureFileFromTemplate(path.join("project", "index.md"), path.join("project", "index.md")),
|
|
54
|
+
ensureFileFromTemplate(path.join("project", "system-spec.md"), path.join("project", "system-spec.md"))
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
if (includeRules) {
|
|
58
|
+
results.push(ensureDir("rules"));
|
|
59
|
+
for (const file of listFilesRecursive(path.join(templatesDir, "rules"))) {
|
|
60
|
+
const relativePath = path.relative(templatesDir, file);
|
|
61
|
+
results.push(ensureFileFromTemplate(relativePath, relativePath));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
printResultList("init", results);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ensureGitRepository() {
|
|
69
|
+
const gitVersion = spawnSync("git", ["--version"], { cwd, encoding: "utf8" });
|
|
70
|
+
if (gitVersion.error && gitVersion.error.code === "ENOENT") {
|
|
71
|
+
return {
|
|
72
|
+
action: "warn",
|
|
73
|
+
target: "git",
|
|
74
|
+
message: "Git 未安裝;後續歸檔會使用版本管理,請先安裝 Git。"
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (gitVersion.status !== 0) {
|
|
79
|
+
return {
|
|
80
|
+
action: "warn",
|
|
81
|
+
target: "git",
|
|
82
|
+
message: "無法確認 Git 是否可用;後續歸檔可能無法使用版本管理。"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const workTree = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, encoding: "utf8" });
|
|
87
|
+
if (workTree.status === 0 && workTree.stdout.trim() === "true") {
|
|
88
|
+
return { action: "skip", target: "git repository" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const init = spawnSync("git", ["init"], { cwd, encoding: "utf8" });
|
|
92
|
+
if (init.status !== 0) {
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return {
|
|
95
|
+
action: "warn",
|
|
96
|
+
target: "git init",
|
|
97
|
+
message: (init.stderr || init.stdout || "git init 執行失敗。").trim()
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { action: "update", target: ".git" };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function runDoctor() {
|
|
105
|
+
return checkRequiredPaths("sa-spec doctor");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function checkRequiredPaths(heading) {
|
|
109
|
+
const requiredPaths = [
|
|
110
|
+
"AGENTS.md",
|
|
111
|
+
path.join("skills", "system-analysis", "SKILL.md"),
|
|
112
|
+
path.join("project", "index.md"),
|
|
113
|
+
path.join("project", "system-spec.md")
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
let hasMissing = false;
|
|
117
|
+
if (heading) {
|
|
118
|
+
console.log(heading);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const relativePath of requiredPaths) {
|
|
122
|
+
const exists = fs.existsSync(path.join(cwd, relativePath));
|
|
123
|
+
console.log(`${exists ? "OK" : "MISSING"} ${relativePath}`);
|
|
124
|
+
if (!exists) {
|
|
125
|
+
hasMissing = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (hasMissing) {
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return !hasMissing;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function runCheck() {
|
|
137
|
+
console.log("sa-spec check");
|
|
138
|
+
if (!checkRequiredPaths(null)) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
console.log("");
|
|
142
|
+
showStatus(false);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function showStatus(showHeading = true) {
|
|
146
|
+
if (showHeading) {
|
|
147
|
+
console.log("sa-spec status");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const systemSpecPath = path.join(cwd, "project", "system-spec.md");
|
|
151
|
+
const indexPath = path.join(cwd, "project", "index.md");
|
|
152
|
+
|
|
153
|
+
if (fs.existsSync(systemSpecPath)) {
|
|
154
|
+
const content = fs.readFileSync(systemSpecPath, "utf8");
|
|
155
|
+
const match = content.match(/^- 狀態:\s*(.+)$/m);
|
|
156
|
+
console.log(`系統規格書狀態: ${match ? match[1].trim() : "未標示"}`);
|
|
157
|
+
} else {
|
|
158
|
+
console.log("系統規格書狀態: 缺少 project/system-spec.md");
|
|
159
|
+
process.exitCode = 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!fs.existsSync(indexPath)) {
|
|
163
|
+
console.log("需求索引: 缺少 project/index.md");
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const rows = parseIndexRows(fs.readFileSync(indexPath, "utf8"));
|
|
169
|
+
console.log(`需求總數: ${rows.length}`);
|
|
170
|
+
|
|
171
|
+
if (rows.length === 0) {
|
|
172
|
+
console.log("目前尚無需求。");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const row of rows.slice(-5)) {
|
|
177
|
+
console.log(`- ${row.id} ${row.title} [${row.status}]`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function runTemplate(args) {
|
|
182
|
+
const type = args[0];
|
|
183
|
+
|
|
184
|
+
if (type === "system-spec") {
|
|
185
|
+
outputTemplate(path.join("project", "system-spec.md"), args.slice(1));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (type === "req") {
|
|
190
|
+
createReqTemplate(args.slice(1), "template req");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fail("Usage: sa-spec template <system-spec|req> [options]");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function outputTemplate(templatePath, args) {
|
|
198
|
+
const outputIndex = args.indexOf("--output");
|
|
199
|
+
const output = outputIndex >= 0 ? args[outputIndex + 1] : null;
|
|
200
|
+
const content = readTemplate(templatePath);
|
|
201
|
+
|
|
202
|
+
if (args.includes("--stdout") || !output) {
|
|
203
|
+
process.stdout.write(content.endsWith("\n") ? content : `${content}\n`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
writeFileIfMissing(path.resolve(cwd, output), content);
|
|
208
|
+
console.log(`Created ${output}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function createReqTemplate(args, label = "new") {
|
|
212
|
+
const id = getOption(args, "--id");
|
|
213
|
+
const name = getOption(args, "--name");
|
|
214
|
+
|
|
215
|
+
if (!id || !name) {
|
|
216
|
+
fail("Usage: sa-spec template req --id REQ-001 --name login");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const slug = slugify(name);
|
|
221
|
+
const reqDir = path.join("project", `${id}-${slug}`);
|
|
222
|
+
const replacements = { REQ_ID: id, REQ_NAME: name };
|
|
223
|
+
const results = [
|
|
224
|
+
ensureDir(reqDir),
|
|
225
|
+
ensureFileFromTemplate(path.join("project", "req", "srs.md"), path.join(reqDir, "srs.md"), replacements),
|
|
226
|
+
ensureFileFromTemplate(path.join("project", "req", "uml.puml"), path.join(reqDir, "uml.puml"), replacements),
|
|
227
|
+
ensureFileFromTemplate(path.join("project", "req", "openapi.yaml"), path.join(reqDir, "openapi.yaml"), replacements),
|
|
228
|
+
ensureFileFromTemplate(path.join("project", "req", "db-schema.md"), path.join(reqDir, "db-schema.md"), replacements),
|
|
229
|
+
ensureFileFromTemplate(path.join("project", "req", "handoff.md"), path.join(reqDir, "handoff.md"), replacements),
|
|
230
|
+
ensureFileFromTemplate(path.join("project", "req", "history.md"), path.join(reqDir, "history.md"), replacements),
|
|
231
|
+
ensureIndexRow(id, name)
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
printResultList(label, results);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function ensureIndexRow(id, name) {
|
|
238
|
+
const indexPath = path.join(cwd, "project", "index.md");
|
|
239
|
+
if (!fs.existsSync(indexPath)) {
|
|
240
|
+
return { action: "skip", target: path.join("project", "index.md") };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const content = fs.readFileSync(indexPath, "utf8");
|
|
244
|
+
if (content.includes(`| ${id} |`)) {
|
|
245
|
+
return { action: "skip", target: path.join("project", "index.md") };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const now = formatLocalDateTime(new Date());
|
|
249
|
+
const row = `| ${id} | ${name} | 待補充 | 待確認 | ${now} | ${now} |`;
|
|
250
|
+
const nextContent = content.endsWith("\n") ? `${content}${row}\n` : `${content}\n${row}\n`;
|
|
251
|
+
fs.writeFileSync(indexPath, nextContent, "utf8");
|
|
252
|
+
return { action: "update", target: path.join("project", "index.md") };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function formatLocalDateTime(date) {
|
|
256
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
257
|
+
return [
|
|
258
|
+
date.getFullYear(),
|
|
259
|
+
pad(date.getMonth() + 1),
|
|
260
|
+
pad(date.getDate())
|
|
261
|
+
].join("-") + " " + [
|
|
262
|
+
pad(date.getHours()),
|
|
263
|
+
pad(date.getMinutes())
|
|
264
|
+
].join(":");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function ensureDir(relativeDir) {
|
|
268
|
+
const absoluteDir = path.join(cwd, relativeDir);
|
|
269
|
+
if (fs.existsSync(absoluteDir)) {
|
|
270
|
+
return { action: "skip", target: relativeDir };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fs.mkdirSync(absoluteDir, { recursive: true });
|
|
274
|
+
return { action: "create", target: relativeDir };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function ensureFileFromTemplate(templateRelativePath, targetRelativePath, replacements = {}) {
|
|
278
|
+
const targetAbsolutePath = path.join(cwd, targetRelativePath);
|
|
279
|
+
if (fs.existsSync(targetAbsolutePath)) {
|
|
280
|
+
return { action: "skip", target: targetRelativePath };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const content = applyReplacements(readTemplate(templateRelativePath), replacements);
|
|
284
|
+
writeFileIfMissing(targetAbsolutePath, content);
|
|
285
|
+
return { action: "create", target: targetRelativePath };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function writeFileIfMissing(filePath, content) {
|
|
289
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
290
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function readTemplate(relativePath) {
|
|
294
|
+
return fs.readFileSync(path.join(templatesDir, relativePath), "utf8");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function listFilesRecursive(dir) {
|
|
298
|
+
return fs
|
|
299
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
300
|
+
.flatMap((entry) => {
|
|
301
|
+
const absolutePath = path.join(dir, entry.name);
|
|
302
|
+
return entry.isDirectory() ? listFilesRecursive(absolutePath) : [absolutePath];
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function applyReplacements(content, replacements) {
|
|
307
|
+
return Object.entries(replacements).reduce(
|
|
308
|
+
(result, [key, value]) => result.split(`{{${key}}}`).join(value),
|
|
309
|
+
content
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function parseIndexRows(content) {
|
|
314
|
+
return content
|
|
315
|
+
.split("\n")
|
|
316
|
+
.filter((line) => line.startsWith("|") && !line.includes("---") && !line.includes("編號"))
|
|
317
|
+
.map((line) => line.split("|").map((part) => part.trim()))
|
|
318
|
+
.filter((parts) => parts.length >= 7)
|
|
319
|
+
.map((parts) => ({
|
|
320
|
+
id: parts[1],
|
|
321
|
+
title: parts[2],
|
|
322
|
+
status: parts[4]
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function getOption(args, flag) {
|
|
327
|
+
const index = args.indexOf(flag);
|
|
328
|
+
return index >= 0 ? args[index + 1] || null : null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function slugify(value) {
|
|
332
|
+
return value
|
|
333
|
+
.toLowerCase()
|
|
334
|
+
.trim()
|
|
335
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
336
|
+
.replace(/^-+|-+$/g, "");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function printResultList(label, results) {
|
|
340
|
+
console.log(`sa-spec ${label}`);
|
|
341
|
+
for (const result of results) {
|
|
342
|
+
const action = result.action === "create" ? "CREATED" : result.action === "update" ? "UPDATED" : result.action === "warn" ? "WARNING" : "SKIPPED";
|
|
343
|
+
console.log(`${action} ${result.target}${result.message ? ` - ${result.message}` : ""}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function showHelp() {
|
|
348
|
+
console.log(`sa-spec
|
|
349
|
+
|
|
350
|
+
Usage:
|
|
351
|
+
sa-spec init [--full]
|
|
352
|
+
sa-spec new --id REQ-001 --name login
|
|
353
|
+
sa-spec check
|
|
354
|
+
`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function fail(message) {
|
|
358
|
+
console.error(message);
|
|
359
|
+
process.exitCode = 1;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
main();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# 專案角色
|
|
2
|
+
|
|
3
|
+
你是一位資深系統分析協作者,負責把需求想法整理成可確認、可歸檔、可檢查的系統分析文件。
|
|
4
|
+
|
|
5
|
+
# 核心交付物
|
|
6
|
+
|
|
7
|
+
每個需求歸檔時固定包含:
|
|
8
|
+
|
|
9
|
+
- `srs.md`:需求規格。
|
|
10
|
+
- `uml.puml`:UML 圖。
|
|
11
|
+
- `openapi.yaml`:OpenAPI 草稿。
|
|
12
|
+
- `db-schema.md`:DB Schema 草稿。
|
|
13
|
+
- `handoff.md`:給 OpenSpec 或其他 AI 開發工具的開發交接摘要。
|
|
14
|
+
- `history.md`:版本歷程。
|
|
15
|
+
|
|
16
|
+
# 指令流程
|
|
17
|
+
|
|
18
|
+
只使用四個指令:
|
|
19
|
+
|
|
20
|
+
| 指令 | 用途 | 規則 |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `spec:init` | 初始化 | 載入並遵守 `AGENTS.md`、`skills/system-analysis/SKILL.md` 與必要規則,確認 `project/` 結構是否存在。 |
|
|
23
|
+
| `spec:propose` | 提出需求 | 使用者提出需求後,先整理需求摘要、目標、初步流程與待確認事項,不歸檔、不修改既有需求。 |
|
|
24
|
+
| `spec:analyze` | 分析與起草 | 先檢查新需求是否與 `project/` 內所有既有需求衝突;無衝突才產出 SRS、UML、OpenAPI、DB Schema 草稿;有衝突則只列出衝突與待釐清問題,待使用者釐清後再次執行 `spec:analyze`。 |
|
|
25
|
+
| `spec:commit` | 確認歸檔 | 只把使用者已確認的版本歸檔到該需求資料夾,更新 `project/index.md`、`project/system-spec.md` 與 `history.md`,並建立本機 Git commit。 |
|
|
26
|
+
|
|
27
|
+
# 重要限制
|
|
28
|
+
|
|
29
|
+
- 使用繁體中文,並採用台灣常用語。
|
|
30
|
+
- `spec:init` 必須檢查 Git 狀態:若 Git 未安裝,提醒使用者後續歸檔會使用版本管理但不可中斷初始化;若 Git 已安裝且目前目錄尚未初始化為 Git repository,則自動執行 `git init`;若已是 Git repository,僅回報狀態,不重複初始化。
|
|
31
|
+
- `project/system-spec.md` 由 `sa-spec init` 建立;`spec:analyze` 只能讀取它做衝突檢查,並提出待確認的系統規格更新建議,不得寫入;`spec:commit` 才能依使用者確認後的內容更新 `project/system-spec.md`。
|
|
32
|
+
- 不可自行幻想未確認的業務規則、角色、流程、資料或系統行為。
|
|
33
|
+
- `spec:analyze` 必須先檢查所有既有需求是否衝突。
|
|
34
|
+
- 若發現衝突,不得產出最終文件,不得修改任何需求文件,只能向使用者釐清。
|
|
35
|
+
- 衝突釐清後,流程回到 `spec:analyze`,重新檢查衝突並產出草稿;不可直接進入 `spec:commit`。
|
|
36
|
+
- 任何流程都不得修改其他需求資料夾中的文件,除非使用者明確指定該需求也要變更。
|
|
37
|
+
- `spec:commit` 只能套用已由 `spec:analyze` 產出的、且使用者已確認的無衝突版本。
|
|
38
|
+
- `handoff.md` 只能根據已確認的 SRS、UML、OpenAPI、DB Schema 與限制整理,不得新增未確認的開發假設。
|
|
39
|
+
- 交給 OpenSpec 或其他 AI 開發工具時,必須指定它先閱讀 `handoff.md`,並以 `handoff.md` 作為開發入口與限制摘要;其他規格文件作為詳細來源。
|
|
40
|
+
- `spec:commit` 完成文件歸檔後,必須自動建立本機 Git commit;commit message 以需求編號與需求名稱為主,並加上簡短需求說明,格式為 `docs(REQ-001): 需求名稱 - 簡短需求說明`。
|
|
41
|
+
- `spec:commit` 只有在使用者明確要求推送 remote,或專案設定啟用 auto-push 時,才可執行 `git push`;push 失敗時只回報認證或網路問題,不得回滾已完成的文件歸檔與本機 commit。
|
|
42
|
+
- 文件未經使用者確認,不得標示為 `已審核`。
|
|
43
|
+
- `uml.puml` 不限單一 UML 圖型;`spec:analyze` 必須依需求選擇最能說明流程、互動、狀態或資料關係的 UML 圖,例如活動圖、循序圖、使用案例圖、狀態圖或類別圖,不可為了固定格式硬產生活動圖。需求複雜時,可在同一個 `uml.puml` 內放多張 PlantUML 圖並清楚分段。
|
|
44
|
+
- 一次最多問 3 個待確認問題,優先詢問會影響範圍、資料、權限、API、DB 或驗收的問題。
|
|
45
|
+
|
|
46
|
+
# 衝突檢查重點
|
|
47
|
+
|
|
48
|
+
`spec:analyze` 至少檢查:
|
|
49
|
+
|
|
50
|
+
- 是否與既有需求目標或流程重疊。
|
|
51
|
+
- 是否改變既有角色、權限或資料存取規則。
|
|
52
|
+
- 是否影響既有 API 路徑、request/response 或狀態碼。
|
|
53
|
+
- 是否影響既有 DB Table、欄位、唯一性或關聯。
|
|
54
|
+
- 是否與既有驗收條件矛盾。
|
|
55
|
+
|
|
56
|
+
# 衝突處理流程
|
|
57
|
+
|
|
58
|
+
若 `spec:analyze` 發現衝突:
|
|
59
|
+
|
|
60
|
+
1. 列出衝突的既有需求編號與檔案。
|
|
61
|
+
2. 說明衝突原因與影響範圍。
|
|
62
|
+
3. 提出最多 3 個待釐清問題。
|
|
63
|
+
4. 停止產出 SRS、UML、OpenAPI、DB Schema。
|
|
64
|
+
5. 等使用者釐清後,再由使用者執行 `spec:analyze` 繼續。
|
|
65
|
+
6. 不得修改任何既有需求文件。
|
|
66
|
+
|
|
67
|
+
# 狀態機
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
init -> propose -> analyze
|
|
71
|
+
|
|
72
|
+
analyze 無衝突 -> 產出待確認版本 -> commit
|
|
73
|
+
analyze 有衝突 -> 釐清問題 -> analyze
|
|
74
|
+
|
|
75
|
+
commit -> 歸檔 -> 本機 Git commit
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
# 歸檔結構
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
project/
|
|
82
|
+
index.md
|
|
83
|
+
REQ-001-name/
|
|
84
|
+
srs.md
|
|
85
|
+
uml.puml
|
|
86
|
+
openapi.yaml
|
|
87
|
+
db-schema.md
|
|
88
|
+
handoff.md
|
|
89
|
+
history.md
|
|
90
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# {{REQ_ID}} {{REQ_NAME}} Handoff
|
|
2
|
+
|
|
3
|
+
- 狀態:待確認
|
|
4
|
+
- 來源文件:`srs.md`、`uml.puml`、`openapi.yaml`、`db-schema.md`
|
|
5
|
+
- 目標讀者:OpenSpec、Codex、Claude、Gemini 或其他 AI 開發工具
|
|
6
|
+
|
|
7
|
+
## 要開發什麼
|
|
8
|
+
待補充。
|
|
9
|
+
|
|
10
|
+
## 不要開發什麼
|
|
11
|
+
- 待補充。
|
|
12
|
+
|
|
13
|
+
## 已確認規則
|
|
14
|
+
- 待補充。
|
|
15
|
+
|
|
16
|
+
## 狀態與流程
|
|
17
|
+
待補充。
|
|
18
|
+
|
|
19
|
+
## API 契約
|
|
20
|
+
待補充;若無 API 需求,明確標示「無 API 需求」。
|
|
21
|
+
|
|
22
|
+
## DB 變更
|
|
23
|
+
待補充;若無 DB 需求,明確標示「無 DB 需求」。
|
|
24
|
+
|
|
25
|
+
## 驗收條件
|
|
26
|
+
- 待補充。
|
|
27
|
+
|
|
28
|
+
## 開發限制
|
|
29
|
+
- 不得自行新增未確認的角色、權限、資料、流程、狀態、API 或 DB 欄位。
|
|
30
|
+
- 若發現既有系統衝突,必須停止開發並回報。
|
|
31
|
+
|
|
32
|
+
## 建議實作範圍
|
|
33
|
+
- 待補充。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# {{REQ_ID}} {{REQ_NAME}} SRS
|
|
2
|
+
|
|
3
|
+
- 狀態:待確認
|
|
4
|
+
- 最新版本:v0.1
|
|
5
|
+
|
|
6
|
+
## 摘要
|
|
7
|
+
待補充。
|
|
8
|
+
|
|
9
|
+
## 使用者與目標
|
|
10
|
+
- 使用者/角色:待補充。
|
|
11
|
+
- 目標:待補充。
|
|
12
|
+
|
|
13
|
+
## 流程
|
|
14
|
+
待補充。
|
|
15
|
+
|
|
16
|
+
## 功能需求
|
|
17
|
+
- 待補充。
|
|
18
|
+
|
|
19
|
+
## 驗收條件
|
|
20
|
+
- 待補充。
|
|
21
|
+
|
|
22
|
+
## 待確認
|
|
23
|
+
- 待補充。
|
|
24
|
+
|
|
25
|
+
## 注意事項
|
|
26
|
+
- 無。
|