@loviusc/sa-spec 0.1.3 → 0.2.1
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/package.json +1 -1
- package/readme.md +106 -3
- package/src/cli.js +277 -11
- package/templates/AGENTS.md +7 -1
- package/templates/project/system-spec.md +1 -0
- package/templates/readme.md +106 -3
- package/templates/skills/system-analysis/SKILL.md +7 -1
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -235,9 +235,10 @@ sa-spec init --full
|
|
|
235
235
|
|
|
236
236
|
| 指令 | 用途 |
|
|
237
237
|
|---|---|
|
|
238
|
-
| `sa-spec init` |
|
|
239
|
-
| `sa-spec
|
|
240
|
-
| `sa-spec
|
|
238
|
+
| `sa-spec init [--full] [--lang zh-TW|en|zh-CN|ja|ko]` | 建立文件骨架與規則文件(支援 5 種語言,預設 `zh-TW`)。 |
|
|
239
|
+
| `sa-spec new [--id REQ-001] --name login [--branch]` | 建立需求資料夾與空白文件模板(未帶 `--id` 時啟用多人草稿模式,可選 `--branch` 自動建立並切換至 `feat/<name>` 分支)。 |
|
|
240
|
+
| `sa-spec finalize --name login` | 將草稿需求定號歸檔為最新序號(自動計算最新號碼、重命名目錄、批次置換 `{{REQ_ID}}` 並重建索引)。 |
|
|
241
|
+
| `sa-spec reindex` | 掃描 `project/REQ-*` 目錄,依序號重新生成乾淨無衝突的 `project/index.md`。 |
|
|
241
242
|
| `sa-spec check` | 檢查基本文件是否存在。 |
|
|
242
243
|
| `sa-spec status` | 顯示系統規格與需求索引狀態。 |
|
|
243
244
|
| `sa-spec doctor` | 檢查必要檔案是否存在。 |
|
|
@@ -349,6 +350,89 @@ docs(REQ-001): 番茄時鐘 - 新增專注與休息計時需求
|
|
|
349
350
|
|
|
350
351
|
只有在使用者明確要求推送 remote,或專案設定啟用 auto-push 時,才會執行 `git push`。如果 push 失敗,只回報認證或網路問題,不會回滾已完成的文件歸檔與本機 commit。
|
|
351
352
|
|
|
353
|
+
## 多人協作解決方案(Git-Flow 與延遲綁定 ID)
|
|
354
|
+
|
|
355
|
+
在單人開發時,循序流水號(`REQ-001`、`REQ-002`)簡單直覺;但在多人平行協作時,容易引發兩大痛點:
|
|
356
|
+
1. **序號競爭撞車**:Alice 與 Bob 同時在各自的分支開發,雙方本機都取了 `REQ-003`,合併回主幹時發生目錄重名衝突。
|
|
357
|
+
2. **中心化檔案衝突**:每次歸檔都要修改 `project/index.md`,導致頻繁的 Git merge conflict。
|
|
358
|
+
|
|
359
|
+
`sa-spec v0.2.0` 引進 **「Feature Branch 隔離 + 延遲綁定序號(Late-Binding ID)」** 完整解決方案:
|
|
360
|
+
|
|
361
|
+
```mermaid
|
|
362
|
+
flowchart TD
|
|
363
|
+
A[開新需求: sa-spec new --name cart --branch] --> B[建立並切換至 feat/cart 分支<br/>以 project/cart/ 建立草稿目錄]
|
|
364
|
+
B --> C[AI 分析與起草: sca<br/>內部使用 {{REQ_ID}} 佔位<br/>自動同步 master 規格防護]
|
|
365
|
+
C --> D{使用者確認通過<br/>執行 scc 或 finalize}
|
|
366
|
+
D --> E[查詢 master 最新序號 N]
|
|
367
|
+
E --> F[重命名為 project/REQ-N+1-cart/<br/>批次置換 {{REQ_ID}} 為正式號碼]
|
|
368
|
+
F --> G[sa-spec reindex: 動態重構 index.md]
|
|
369
|
+
G --> H[合併回 master: 零衝突、序號永遠連續!]
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### 多人協作四步流程
|
|
373
|
+
|
|
374
|
+
#### 1. 提出需求(草稿與分支隔離)
|
|
375
|
+
開需求時**不搶序號**,直接以語意名稱建立分支與草稿目錄:
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
# 自動建立並切換至 feat/cart-checkout 分支,建立 project/cart-checkout/ 草稿目錄
|
|
379
|
+
sa-spec new --name cart-checkout --branch
|
|
380
|
+
```
|
|
381
|
+
在 VS Code 側邊欄輸入:
|
|
382
|
+
```text
|
|
383
|
+
scp 購物車結帳需求
|
|
384
|
+
```
|
|
385
|
+
* **特點**:目錄名稱為 `project/cart-checkout/`,內部所有檔案皆使用 `{{REQ_ID}}` 佔位符。多人同時提出需求互不干擾、零衝突。即使需求中途放棄,直接刪除分支即可,絕不留下幽靈跳號。
|
|
386
|
+
|
|
387
|
+
#### 2. 分析與衝突檢查(規格基線同步)
|
|
388
|
+
在分支上執行 `spec:analyze`(或 `sca`)前,工具會確認已取得 `master`(或 `origin/master`)的最新規格,確保是以全專案最新基準進行影響評估,避免因分支落後而漏檢權限、API 或資料表衝突。
|
|
389
|
+
|
|
390
|
+
#### 3. 確認歸檔與延遲定號(Late-Binding)
|
|
391
|
+
當需求規格確認無誤,準備歸檔時:
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
# 自動向 master 查詢最新可用序號(例如 REQ-006)
|
|
395
|
+
sa-spec finalize --name cart-checkout
|
|
396
|
+
```
|
|
397
|
+
* **自動化執行**:
|
|
398
|
+
1. 取得最新序號(例如目前最大是 `REQ-005`,自動取得 `REQ-006`)。
|
|
399
|
+
2. 使用 `git mv` 將目錄重命名為 `project/REQ-006-cart-checkout`。
|
|
400
|
+
3. 自動掃描並將目錄內所有檔案的 `{{REQ_ID}}` 批次置換為 `REQ-006`。
|
|
401
|
+
4. 自動執行 `reindex` 重建需求總表。
|
|
402
|
+
|
|
403
|
+
#### 4. 消除索引合併衝突(`sa-spec reindex`)
|
|
404
|
+
`project/index.md` 改為由目錄動態生成的「衍生清單」,不再依賴脆弱的手動 append:
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
sa-spec reindex
|
|
408
|
+
```
|
|
409
|
+
* 任何時候若有 PR 合併產生 index 衝突,只要執行 `sa-spec reindex`,即可依序號重新排序生成乾淨的總表,無需人工手動解 conflict。
|
|
410
|
+
|
|
411
|
+
## 語言與國際化支援(i18n)
|
|
412
|
+
|
|
413
|
+
`sa-spec` 支援 5 種主要語言的系統分析規格產出與規範手冊:
|
|
414
|
+
|
|
415
|
+
| 語系代碼 | 語言名稱 | 內文與風格規範 | 專有名詞處理準則 |
|
|
416
|
+
|---|---|---|---|
|
|
417
|
+
| `zh-TW` | **繁體中文**(預設) | 台灣技術常用語(如:伺服器、支援、專案) | 強制保留原生英文(如 `REST API`, `Webhook`, `JWT`) |
|
|
418
|
+
| `en` | **English** | 100% 標準技術英文,不可出現中/日/韓文字符 | 原生技術英文 |
|
|
419
|
+
| `zh-CN` | **简体中文** | 大陸技術常用語(如:服务器、支持、项目) | 強制保留原生英文(如 `REST API`, `Webhook`, `JWT`) |
|
|
420
|
+
| `ja` | **日本語** | 日語技術文體(です/ます 或 である),外來詞尊重 Katakana 慣例 | 強制保留原生英文(如 `REST API`, `Webhook`, `JWT`) |
|
|
421
|
+
| `ko` | **한국어** | 韓語專業技術文體與規範敬語 | 強制保留原生英文(如 `REST API`, `Webhook`, `JWT`) |
|
|
422
|
+
|
|
423
|
+
### 語言管理核心原則
|
|
424
|
+
|
|
425
|
+
1. **初始化強制鎖定(Init-Time Lock)**:
|
|
426
|
+
* 語言由初始化階段決定,不可模稜兩可。
|
|
427
|
+
* **CLI 指令**:可直接指定語系參數,例如 `sa-spec init --lang en`。
|
|
428
|
+
* **AI 工作流**:在對話中輸入 `spec:init`(或 `sci`)時,若未指定語言,**AI 必須主動列出 5 種語言選單詢問使用者**,選定後寫入 `project/system-spec.md` 作為全專案唯一的真理之源(Single Source of Truth)。
|
|
429
|
+
2. **全專案語言單一純粹(Single-Language Purity)**:
|
|
430
|
+
* 選定語言後,全專案的所有產出(`srs.md`、`openapi.yaml`、`db-schema.md`、`handoff.md`、`history.md` 與 `rules/`)必須 100% 統一使用該語言,禁止在同一專案內出現語言混雜。
|
|
431
|
+
3. **專有名詞保留原生英文(Technical Terms Exception)**:
|
|
432
|
+
* 在非英語系模式(繁中、簡中、日文、韓文)下,業界公認標準之技術術語、協定、資料格式與元件名稱(例如:`REST API`、`Webhook`、`JWT`、`OAuth 2.0`、`Payload`、`Idempotency Key`、`Redis`、`UUID` 等),**一律強制維持原生英文**,嚴禁生硬直譯。
|
|
433
|
+
4. **升級保護(Upgrade Inheritance)**:
|
|
434
|
+
* 執行 `sa-spec upgrade` 時,系統會**自動偵測並繼承現有專案語言**(讀取 `project/system-spec.md`),不會詢問或變更既有專案語言,確保升級零干擾。
|
|
435
|
+
|
|
352
436
|
## 完整範例:番茄時鐘
|
|
353
437
|
|
|
354
438
|
以下示範從空資料夾開始,建立一個「番茄時鐘」需求。
|
|
@@ -597,6 +681,25 @@ git push origin master
|
|
|
597
681
|
|
|
598
682
|
## 更新紀錄
|
|
599
683
|
|
|
684
|
+
### v0.2.1
|
|
685
|
+
- **5 國語言與國際化支援(i18n)**:
|
|
686
|
+
- **支援語系**:繁體中文 (`zh-TW`,台灣常用語)、英文 (`en`)、簡體中文 (`zh-CN`)、日文 (`ja`)、韓文 (`ko`)。
|
|
687
|
+
- **初始化強制鎖定**:`sa-spec init [--lang <lang>]` 或在 AI 對話中輸入 `spec:init`,未指定時主動彈出選單詢問使用者選擇,選定後寫入 `system-spec.md`。
|
|
688
|
+
- **專案語言單一純粹**:全專案規格產出 100% 統一該語言。
|
|
689
|
+
- **技術專有名詞保留原生英文**:非英語系模式下強制保留標準英文術語(如 REST API, Webhook, JWT),嚴禁生硬直譯。
|
|
690
|
+
- **升級保護**:`sa-spec upgrade` 自動偵測並繼承現有專案語言,不變更設定。
|
|
691
|
+
|
|
692
|
+
### v0.2.0
|
|
693
|
+
- **多人協作 Git-Flow 解決方案(延遲綁定序號與零衝突索引)**:
|
|
694
|
+
- **解決痛點**:多人平行開發時,傳統流水號(`REQ-001`)容易在各自分支撞車,且中心化的 `project/index.md` 容易造成 Git merge conflict。
|
|
695
|
+
- **延遲綁定序號(Late-Binding ID)**:開需求時不搶號,在 feature branch 上以語意名稱建立草稿需求(如 `project/cart`),內部使用 `{{REQ_ID}}` 佔位;只有在 `spec:commit` 最終歸檔時,才向 master/遠端確認最新可用序號並定號(如 `REQ-003`),自動批次置換佔位符,徹底避免幽靈跳號與序號競爭。
|
|
696
|
+
- **動態重建需求索引(`sa-spec reindex`)**:將 `project/index.md` 改為根據 `project/REQ-*` 目錄動態掃描並依序號排序生成,消除手動 append 造成的 Git 合併衝突。
|
|
697
|
+
- **CLI 指令升級**:
|
|
698
|
+
- `sa-spec new [--id REQ-001] --name login [--branch]`:支援草稿模式與自動切換 feature 分支。
|
|
699
|
+
- `sa-spec finalize --name login`:自動定號、重命名目錄、置換佔位符並重建索引。
|
|
700
|
+
- `sa-spec reindex`:一鍵掃描並重新排序生成乾淨的 `project/index.md`。
|
|
701
|
+
- **分析前防護**:`spec:analyze` 執行前確保取得 master/遠端最新規格,避免分支規格落後導致漏檢衝突。
|
|
702
|
+
|
|
600
703
|
### v0.1.3
|
|
601
704
|
- **支援指令簡寫別名(同時保留完整指令)**:
|
|
602
705
|
- `sci` = `spec:init`
|
package/src/cli.js
CHANGED
|
@@ -17,6 +17,12 @@ function main() {
|
|
|
17
17
|
case "new":
|
|
18
18
|
createReqTemplate(args.slice(1));
|
|
19
19
|
return;
|
|
20
|
+
case "finalize":
|
|
21
|
+
finalizeReq(args.slice(1));
|
|
22
|
+
return;
|
|
23
|
+
case "reindex":
|
|
24
|
+
reindexProject();
|
|
25
|
+
return;
|
|
20
26
|
case "check":
|
|
21
27
|
runCheck();
|
|
22
28
|
return;
|
|
@@ -32,6 +38,11 @@ function main() {
|
|
|
32
38
|
case "template":
|
|
33
39
|
runTemplate(args.slice(1));
|
|
34
40
|
return;
|
|
41
|
+
case "version":
|
|
42
|
+
case "--version":
|
|
43
|
+
case "-v":
|
|
44
|
+
showVersion();
|
|
45
|
+
return;
|
|
35
46
|
case "help":
|
|
36
47
|
case "--help":
|
|
37
48
|
case "-h":
|
|
@@ -43,29 +54,54 @@ function main() {
|
|
|
43
54
|
}
|
|
44
55
|
}
|
|
45
56
|
|
|
57
|
+
const SUPPORTED_LANGS = {
|
|
58
|
+
"zh-tw": { code: "zh-TW", name: "繁體中文", label: "繁體中文 (zh-TW)" },
|
|
59
|
+
"en": { code: "en", name: "English", label: "English (en)" },
|
|
60
|
+
"zh-cn": { code: "zh-CN", name: "简体中文", label: "简体中文 (zh-CN)" },
|
|
61
|
+
"ja": { code: "ja", name: "日本語", label: "日本語 (ja)" },
|
|
62
|
+
"ko": { code: "ko", name: "한국어", label: "한국어 (ko)" }
|
|
63
|
+
};
|
|
64
|
+
|
|
46
65
|
function initProject(args = []) {
|
|
47
66
|
const includeRules = args.includes("--full");
|
|
67
|
+
const langArg = getOption(args, "--lang");
|
|
68
|
+
let langConfig = SUPPORTED_LANGS["zh-tw"];
|
|
69
|
+
if (langArg) {
|
|
70
|
+
const key = langArg.toLowerCase();
|
|
71
|
+
if (SUPPORTED_LANGS[key]) {
|
|
72
|
+
langConfig = SUPPORTED_LANGS[key];
|
|
73
|
+
} else {
|
|
74
|
+
console.log(`警告: 不支援的語言代碼 "${langArg}",支援語系: zh-TW, en, zh-CN, ja, ko。已預設使用繁體中文。`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const replacements = {
|
|
79
|
+
LANG_LABEL: langConfig.label,
|
|
80
|
+
LANG_CODE: langConfig.code
|
|
81
|
+
};
|
|
82
|
+
|
|
48
83
|
const results = [
|
|
49
84
|
ensureGitRepository(),
|
|
50
85
|
ensureDir("skills"),
|
|
51
86
|
ensureDir(path.join("skills", "system-analysis")),
|
|
52
87
|
ensureDir("project"),
|
|
53
|
-
ensureFileFromTemplate("AGENTS.md", "AGENTS.md"),
|
|
54
|
-
ensureFileFromTemplate("readme.md", "readme.md"),
|
|
55
|
-
ensureFileFromTemplate(path.join("skills", "system-analysis", "SKILL.md"), path.join("skills", "system-analysis", "SKILL.md")),
|
|
56
|
-
ensureFileFromTemplate(path.join("project", "index.md"), path.join("project", "index.md")),
|
|
57
|
-
ensureFileFromTemplate(path.join("project", "system-spec.md"), path.join("project", "system-spec.md"))
|
|
88
|
+
ensureFileFromTemplate("AGENTS.md", "AGENTS.md", replacements),
|
|
89
|
+
ensureFileFromTemplate("readme.md", "readme.md", replacements),
|
|
90
|
+
ensureFileFromTemplate(path.join("skills", "system-analysis", "SKILL.md"), path.join("skills", "system-analysis", "SKILL.md"), replacements),
|
|
91
|
+
ensureFileFromTemplate(path.join("project", "index.md"), path.join("project", "index.md"), replacements),
|
|
92
|
+
ensureFileFromTemplate(path.join("project", "system-spec.md"), path.join("project", "system-spec.md"), replacements)
|
|
58
93
|
];
|
|
59
94
|
|
|
60
95
|
if (includeRules) {
|
|
61
96
|
results.push(ensureDir("rules"));
|
|
62
97
|
for (const file of listFilesRecursive(path.join(templatesDir, "rules"))) {
|
|
63
98
|
const relativePath = path.relative(templatesDir, file);
|
|
64
|
-
results.push(ensureFileFromTemplate(relativePath, relativePath));
|
|
99
|
+
results.push(ensureFileFromTemplate(relativePath, relativePath, replacements));
|
|
65
100
|
}
|
|
66
101
|
}
|
|
67
102
|
|
|
68
103
|
printResultList("init", results);
|
|
104
|
+
console.log(`專案規格主要語言已設定為: ${langConfig.label}`);
|
|
69
105
|
}
|
|
70
106
|
|
|
71
107
|
function ensureGitRepository() {
|
|
@@ -215,12 +251,46 @@ function createReqTemplate(args, label = "new") {
|
|
|
215
251
|
const id = getOption(args, "--id");
|
|
216
252
|
const name = getOption(args, "--name");
|
|
217
253
|
|
|
218
|
-
if (!
|
|
219
|
-
fail("Usage: sa-spec
|
|
254
|
+
if (!name) {
|
|
255
|
+
fail("Usage: sa-spec new [--id REQ-001] --name login [--branch]");
|
|
220
256
|
return;
|
|
221
257
|
}
|
|
222
258
|
|
|
223
259
|
const slug = slugify(name);
|
|
260
|
+
|
|
261
|
+
// 草稿模式(Git-Flow 多人協作):未指定 id 時建立草稿目錄,以 {{REQ_ID}} 佔位
|
|
262
|
+
if (!id) {
|
|
263
|
+
if (args.includes("--branch")) {
|
|
264
|
+
const branchName = `feat/${slug}`;
|
|
265
|
+
const branchRes = spawnSync("git", ["checkout", "-b", branchName], { cwd, encoding: "utf8" });
|
|
266
|
+
if (branchRes.status === 0) {
|
|
267
|
+
console.log(`已建立並切換至分支: ${branchName}`);
|
|
268
|
+
} else {
|
|
269
|
+
console.log(`提示: 分支切換略過 (${(branchRes.stderr || "").trim() || "可能是已有同名分支或未在 git 目錄"})`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const reqDir = path.join("project", slug);
|
|
274
|
+
const umlDir = path.join(reqDir, "uml");
|
|
275
|
+
const replacements = { REQ_ID: "{{REQ_ID}}", REQ_NAME: name };
|
|
276
|
+
const results = [
|
|
277
|
+
ensureDir(reqDir),
|
|
278
|
+
ensureDir(umlDir),
|
|
279
|
+
ensureFileFromTemplate(path.join("project", "req", "srs.md"), path.join(reqDir, "srs.md"), replacements),
|
|
280
|
+
ensureFileFromTemplate(path.join("project", "req", "uml", "main.puml"), path.join(umlDir, "main.puml"), replacements),
|
|
281
|
+
ensureFileFromTemplate(path.join("project", "req", "openapi.yaml"), path.join(reqDir, "openapi.yaml"), replacements),
|
|
282
|
+
ensureFileFromTemplate(path.join("project", "req", "db-schema.md"), path.join(reqDir, "db-schema.md"), replacements),
|
|
283
|
+
ensureFileFromTemplate(path.join("project", "req", "handoff.md"), path.join(reqDir, "handoff.md"), replacements),
|
|
284
|
+
ensureFileFromTemplate(path.join("project", "req", "history.md"), path.join(reqDir, "history.md"), replacements)
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
printResultList(label, results);
|
|
288
|
+
console.log(`\n已建立草稿需求:${reqDir}(內部使用 {{REQ_ID}} 佔位符)`);
|
|
289
|
+
console.log(`分析確認後,可使用 sa-spec finalize --name ${slug} 定號歸檔。`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 傳統模式:指定序號
|
|
224
294
|
const reqDir = path.join("project", `${id}-${slug}`);
|
|
225
295
|
const umlDir = path.join(reqDir, "uml");
|
|
226
296
|
const replacements = { REQ_ID: id, REQ_NAME: name };
|
|
@@ -239,6 +309,166 @@ function createReqTemplate(args, label = "new") {
|
|
|
239
309
|
printResultList(label, results);
|
|
240
310
|
}
|
|
241
311
|
|
|
312
|
+
function finalizeReq(args) {
|
|
313
|
+
const name = getOption(args, "--name") || getOption(args, "--draft") || args[0];
|
|
314
|
+
if (!name) {
|
|
315
|
+
fail("Usage: sa-spec finalize --name <name-or-slug>");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const slug = slugify(name);
|
|
320
|
+
const projectDir = path.join(cwd, "project");
|
|
321
|
+
let draftRelDir = path.join("project", slug);
|
|
322
|
+
let draftAbsDir = path.join(cwd, draftRelDir);
|
|
323
|
+
|
|
324
|
+
if (!fs.existsSync(draftAbsDir)) {
|
|
325
|
+
const altRelDir = path.join("project", `draft-${slug}`);
|
|
326
|
+
if (fs.existsSync(path.join(cwd, altRelDir))) {
|
|
327
|
+
draftRelDir = altRelDir;
|
|
328
|
+
draftAbsDir = path.join(cwd, altRelDir);
|
|
329
|
+
} else {
|
|
330
|
+
fail(`找不到草稿需求目錄: ${draftRelDir}`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 掃描既有最大 REQ 編號
|
|
336
|
+
let maxIdNum = 0;
|
|
337
|
+
if (fs.existsSync(projectDir)) {
|
|
338
|
+
const entries = fs.readdirSync(projectDir, { withFileTypes: true });
|
|
339
|
+
for (const entry of entries) {
|
|
340
|
+
if (entry.isDirectory() && entry.name.startsWith("REQ-")) {
|
|
341
|
+
const m = entry.name.match(/^REQ-(\d+)/i);
|
|
342
|
+
if (m) {
|
|
343
|
+
const num = parseInt(m[1], 10);
|
|
344
|
+
if (num > maxIdNum) {
|
|
345
|
+
maxIdNum = num;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const nextIdNum = maxIdNum + 1;
|
|
353
|
+
const nextId = `REQ-${String(nextIdNum).padStart(3, "0")}`;
|
|
354
|
+
const finalDirName = `${nextId}-${slug}`;
|
|
355
|
+
const finalRelDir = path.join("project", finalDirName);
|
|
356
|
+
const finalAbsDir = path.join(cwd, finalRelDir);
|
|
357
|
+
|
|
358
|
+
if (fs.existsSync(finalAbsDir)) {
|
|
359
|
+
fail(`目標目錄 ${finalRelDir} 已存在,請確認序號或目錄名稱。`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 重命名目錄 (優先嘗試 git mv)
|
|
364
|
+
const gitMv = spawnSync("git", ["mv", draftRelDir, finalRelDir], { cwd, encoding: "utf8" });
|
|
365
|
+
if (gitMv.status === 0) {
|
|
366
|
+
console.log(`[Git] 已將目錄重命名為: ${finalRelDir}`);
|
|
367
|
+
} else {
|
|
368
|
+
fs.renameSync(draftAbsDir, finalAbsDir);
|
|
369
|
+
console.log(`已將目錄重命名為: ${finalRelDir}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 替換目錄內檔案的 {{REQ_ID}} 佔位符
|
|
373
|
+
const files = listFilesRecursive(finalAbsDir);
|
|
374
|
+
for (const filePath of files) {
|
|
375
|
+
try {
|
|
376
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
377
|
+
if (content.includes("{{REQ_ID}}")) {
|
|
378
|
+
const updated = content.split("{{REQ_ID}}").join(nextId);
|
|
379
|
+
fs.writeFileSync(filePath, updated, "utf8");
|
|
380
|
+
}
|
|
381
|
+
} catch (_) {
|
|
382
|
+
// 略過無法以文字讀取之檔案
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 自動重新整理 index.md
|
|
387
|
+
reindexProject();
|
|
388
|
+
|
|
389
|
+
console.log(`\n 需求已成功定號歸檔為: ${nextId} (${finalDirName})`);
|
|
390
|
+
console.log(`建議接下來執行:`);
|
|
391
|
+
console.log(` git add project/`);
|
|
392
|
+
console.log(` git commit -m "docs(${nextId}): ${name} - 規格確認歸檔"`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function reindexProject() {
|
|
396
|
+
const projectDir = path.join(cwd, "project");
|
|
397
|
+
const indexPath = path.join(projectDir, "index.md");
|
|
398
|
+
if (!fs.existsSync(projectDir)) {
|
|
399
|
+
fail("project/ directory not found.");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 讀取現有索引
|
|
404
|
+
const existingMap = new Map();
|
|
405
|
+
if (fs.existsSync(indexPath)) {
|
|
406
|
+
const existingRows = parseIndexRows(fs.readFileSync(indexPath, "utf8"));
|
|
407
|
+
for (const row of existingRows) {
|
|
408
|
+
existingMap.set(row.id, row);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const entries = fs.readdirSync(projectDir, { withFileTypes: true });
|
|
413
|
+
const reqDirs = entries.filter((e) => e.isDirectory() && e.name.startsWith("REQ-"));
|
|
414
|
+
|
|
415
|
+
const rows = [];
|
|
416
|
+
for (const entry of reqDirs) {
|
|
417
|
+
const dirName = entry.name;
|
|
418
|
+
const match = dirName.match(/^(REQ-\d+)(?:-(.*))?$/i);
|
|
419
|
+
if (!match) continue;
|
|
420
|
+
const id = match[1].toUpperCase();
|
|
421
|
+
const fallbackTitle = match[2] || dirName;
|
|
422
|
+
|
|
423
|
+
const reqDirPath = path.join(projectDir, dirName);
|
|
424
|
+
const srsPath = path.join(reqDirPath, "srs.md");
|
|
425
|
+
|
|
426
|
+
let title = fallbackTitle;
|
|
427
|
+
let description = "待補充";
|
|
428
|
+
let status = "待確認";
|
|
429
|
+
let createdAt = formatLocalDateTime(new Date());
|
|
430
|
+
let updatedAt = createdAt;
|
|
431
|
+
|
|
432
|
+
if (existingMap.has(id)) {
|
|
433
|
+
const prev = existingMap.get(id);
|
|
434
|
+
title = prev.title || title;
|
|
435
|
+
description = prev.description || description;
|
|
436
|
+
status = prev.status || status;
|
|
437
|
+
createdAt = prev.createdAt || createdAt;
|
|
438
|
+
updatedAt = prev.updatedAt || updatedAt;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (fs.existsSync(srsPath)) {
|
|
442
|
+
const srsContent = fs.readFileSync(srsPath, "utf8");
|
|
443
|
+
const titleMatch = srsContent.match(/^# 需求規格[::]\s*(?:REQ-\d+\s*)?(.*)$/m);
|
|
444
|
+
if (titleMatch && titleMatch[1].trim()) {
|
|
445
|
+
title = titleMatch[1].trim();
|
|
446
|
+
}
|
|
447
|
+
const statusMatch = srsContent.match(/^-\s*狀態[::]\s*(.+)$/m);
|
|
448
|
+
if (statusMatch && statusMatch[1].trim()) {
|
|
449
|
+
status = statusMatch[1].trim();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const numMatch = id.match(/\d+/);
|
|
454
|
+
const num = numMatch ? parseInt(numMatch[0], 10) : 0;
|
|
455
|
+
|
|
456
|
+
rows.push({ num, id, title, description, status, createdAt, updatedAt });
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 依數字循序排序
|
|
460
|
+
rows.sort((a, b) => a.num - b.num);
|
|
461
|
+
|
|
462
|
+
const header = `# 需求索引\n\n| 編號 | 標題 | 敘述 | 狀態 | 建立時間 | 更新時間 |\n|---|---|---|---|---|---|\n`;
|
|
463
|
+
const tableRows = rows
|
|
464
|
+
.map((r) => `| ${r.id} | ${r.title} | ${r.description} | ${r.status} | ${r.createdAt} | ${r.updatedAt} |`)
|
|
465
|
+
.join("\n");
|
|
466
|
+
const nextContent = rows.length > 0 ? `${header}${tableRows}\n` : `${header}\n`;
|
|
467
|
+
|
|
468
|
+
fs.writeFileSync(indexPath, nextContent, "utf8");
|
|
469
|
+
console.log(`sa-spec reindex: 已重新索引 ${rows.length} 個需求至 project/index.md`);
|
|
470
|
+
}
|
|
471
|
+
|
|
242
472
|
function ensureIndexRow(id, name) {
|
|
243
473
|
const indexPath = path.join(cwd, "project", "index.md");
|
|
244
474
|
if (!fs.existsSync(indexPath)) {
|
|
@@ -324,7 +554,10 @@ function parseIndexRows(content) {
|
|
|
324
554
|
.map((parts) => ({
|
|
325
555
|
id: parts[1],
|
|
326
556
|
title: parts[2],
|
|
327
|
-
|
|
557
|
+
description: parts[3],
|
|
558
|
+
status: parts[4],
|
|
559
|
+
createdAt: parts[5],
|
|
560
|
+
updatedAt: parts[6]
|
|
328
561
|
}));
|
|
329
562
|
}
|
|
330
563
|
|
|
@@ -341,8 +574,33 @@ function slugify(value) {
|
|
|
341
574
|
.replace(/^-+|-+$/g, "");
|
|
342
575
|
}
|
|
343
576
|
|
|
577
|
+
function detectProjectLanguage() {
|
|
578
|
+
const systemSpecPath = path.join(cwd, "project", "system-spec.md");
|
|
579
|
+
if (fs.existsSync(systemSpecPath)) {
|
|
580
|
+
const content = fs.readFileSync(systemSpecPath, "utf8");
|
|
581
|
+
const match = content.match(/^-\s*(?:主要語言|語言|Language)[::]\s*(.+)$/im);
|
|
582
|
+
if (match) {
|
|
583
|
+
const val = match[1].trim();
|
|
584
|
+
for (const key of Object.keys(SUPPORTED_LANGS)) {
|
|
585
|
+
if (
|
|
586
|
+
val.toLowerCase().includes(key) ||
|
|
587
|
+
val.includes(SUPPORTED_LANGS[key].label) ||
|
|
588
|
+
val.includes(SUPPORTED_LANGS[key].name) ||
|
|
589
|
+
val.includes(SUPPORTED_LANGS[key].code)
|
|
590
|
+
) {
|
|
591
|
+
return SUPPORTED_LANGS[key];
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return { code: "custom", label: val };
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return SUPPORTED_LANGS["zh-tw"];
|
|
598
|
+
}
|
|
599
|
+
|
|
344
600
|
function upgradeProject(args = []) {
|
|
345
601
|
const dryRun = args.includes("--dry-run");
|
|
602
|
+
const langConfig = detectProjectLanguage();
|
|
603
|
+
console.log(`專案目前語言: ${langConfig.label}(依現有語言升級,不變更設定)`);
|
|
346
604
|
const results = [];
|
|
347
605
|
|
|
348
606
|
results.push(ensureDir("skills"));
|
|
@@ -476,13 +734,21 @@ function showHelp() {
|
|
|
476
734
|
console.log(`sa-spec
|
|
477
735
|
|
|
478
736
|
Usage:
|
|
479
|
-
sa-spec init [--full]
|
|
480
|
-
sa-spec new --id REQ-001 --name login
|
|
737
|
+
sa-spec init [--full] [--lang zh-TW|en|zh-CN|ja|ko]
|
|
738
|
+
sa-spec new [--id REQ-001] --name login [--branch]
|
|
739
|
+
sa-spec finalize --name login
|
|
740
|
+
sa-spec reindex
|
|
481
741
|
sa-spec check
|
|
482
742
|
sa-spec upgrade [--dry-run]
|
|
743
|
+
sa-spec --version
|
|
483
744
|
`);
|
|
484
745
|
}
|
|
485
746
|
|
|
747
|
+
function showVersion() {
|
|
748
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
|
749
|
+
console.log(pkg.version);
|
|
750
|
+
}
|
|
751
|
+
|
|
486
752
|
function fail(message) {
|
|
487
753
|
console.error(message);
|
|
488
754
|
process.exitCode = 1;
|
package/templates/AGENTS.md
CHANGED
|
@@ -26,7 +26,12 @@
|
|
|
26
26
|
|
|
27
27
|
# 重要限制
|
|
28
28
|
|
|
29
|
-
-
|
|
29
|
+
- 語言與國際化規範(i18n):
|
|
30
|
+
- 支援語系:繁體中文 (`zh-TW`,台灣常用語)、英文 (`en`)、簡體中文 (`zh-CN`)、日文 (`ja`)、韓文 (`ko`) 共 5 種語言。
|
|
31
|
+
- 初始化語言鎖定:執行 `spec:init` (或 `sci`) 時,若使用者未明確指定語言,AI 必須主動詢問使用者選擇上述 5 種語言之一,不得自行預設;選定後將主要語言寫入 `project/system-spec.md`。
|
|
32
|
+
- 全專案語言單一純粹:後續所有需求文件(`srs.md`、`uml/`、`openapi.yaml`、`db-schema.md`、`handoff.md`、`history.md`)必須 100% 統一使用該語言,不得在同一專案內混雜多種語言。
|
|
33
|
+
- 專有名詞保留原生英文:在中文、日文、韓文等非英語系模式下,業界公認標準之技術術語(如 REST API, Webhook, JWT, OAuth 2.0, Payload, Idempotency, Redis 等)強制保留原生英文,嚴禁生硬直譯。
|
|
34
|
+
- 升級保護:執行 `sa-spec upgrade` 時,自動偵測並繼承現有專案語言,不變更語言設定。
|
|
30
35
|
- `spec:init` 必須檢查 Git 狀態:若 Git 未安裝,提醒使用者後續歸檔會使用版本管理但不可中斷初始化;若 Git 已安裝且目前目錄尚未初始化為 Git repository,則自動執行 `git init`;若已是 Git repository,僅回報狀態,不重複初始化。
|
|
31
36
|
- `project/system-spec.md` 由 `sa-spec init` 建立;`spec:analyze` 只能讀取它做衝突檢查,並提出待確認的系統規格更新建議,不得寫入;`spec:commit` 才能依使用者確認後的內容更新 `project/system-spec.md`。
|
|
32
37
|
- 不可自行幻想未確認的業務規則、角色、流程、資料或系統行為。
|
|
@@ -39,6 +44,7 @@
|
|
|
39
44
|
- 交給 OpenSpec 或其他 AI 開發工具時,必須指定它先閱讀 `handoff.md`,並以 `handoff.md` 作為開發入口與限制摘要;其他規格文件作為詳細來源。
|
|
40
45
|
- `spec:commit` 完成文件歸檔後,必須自動建立本機 Git commit;commit message 以需求編號與需求名稱為主,並加上簡短需求說明,格式為 `docs(REQ-001): 需求名稱 - 簡短需求說明`。
|
|
41
46
|
- `spec:commit` 只有在使用者明確要求推送 remote,或專案設定啟用 auto-push 時,才可執行 `git push`;push 失敗時只回報認證或網路問題,不得回滾已完成的文件歸檔與本機 commit。
|
|
47
|
+
- 多人協作與 Git-Flow 模式(延遲綁定序號):支援多人平行開發。提出需求時可於 feature branch(如 `feat/<name>`)建立草稿目錄,內部使用 `{{REQ_ID}}` 佔位;執行 `spec:analyze` 前應先確保取得 master 最新規格以防漏檢衝突;執行 `spec:commit` 歸檔時向 master 確認最新可用序號並定號,自動將目錄定名為正式 `REQ-xxx`、替換內部佔位符、重新生成 `project/index.md`,避免多人合併時的序號競爭與索引衝突。
|
|
42
48
|
- 文件未經使用者確認,不得標示為 `已審核`。
|
|
43
49
|
- `uml/` 內的 UML 圖不限單一 UML 圖型;`spec:analyze` 必須依需求選擇最能說明流程、互動、狀態或資料關係的 UML 圖,例如活動圖、循序圖、使用案例圖、狀態圖或類別圖,不可為了固定格式硬產生活動圖。**嚴禁將多張圖混在同一個檔案中**,必須在需求資料夾下的 `uml/` 目錄拆分成獨立的 `.puml` 檔案(例如 `uml/main.puml`、`uml/sequence.puml`、`uml/state.puml`),且每個檔案只能包含一組 `@startuml` 與 `@enduml`,以避免 PlantUML 渲染或預覽失敗。
|
|
44
50
|
- 一次最多問 3 個待確認問題,優先詢問會影響範圍、資料、權限、API、DB 或驗收的問題。
|
package/templates/readme.md
CHANGED
|
@@ -235,9 +235,10 @@ sa-spec init --full
|
|
|
235
235
|
|
|
236
236
|
| 指令 | 用途 |
|
|
237
237
|
|---|---|
|
|
238
|
-
| `sa-spec init` |
|
|
239
|
-
| `sa-spec
|
|
240
|
-
| `sa-spec
|
|
238
|
+
| `sa-spec init [--full] [--lang zh-TW|en|zh-CN|ja|ko]` | 建立文件骨架與規則文件(支援 5 種語言,預設 `zh-TW`)。 |
|
|
239
|
+
| `sa-spec new [--id REQ-001] --name login [--branch]` | 建立需求資料夾與空白文件模板(未帶 `--id` 時啟用多人草稿模式,可選 `--branch` 自動建立並切換至 `feat/<name>` 分支)。 |
|
|
240
|
+
| `sa-spec finalize --name login` | 將草稿需求定號歸檔為最新序號(自動計算最新號碼、重命名目錄、批次置換 `{{REQ_ID}}` 並重建索引)。 |
|
|
241
|
+
| `sa-spec reindex` | 掃描 `project/REQ-*` 目錄,依序號重新生成乾淨無衝突的 `project/index.md`。 |
|
|
241
242
|
| `sa-spec check` | 檢查基本文件是否存在。 |
|
|
242
243
|
| `sa-spec status` | 顯示系統規格與需求索引狀態。 |
|
|
243
244
|
| `sa-spec doctor` | 檢查必要檔案是否存在。 |
|
|
@@ -349,6 +350,89 @@ docs(REQ-001): 番茄時鐘 - 新增專注與休息計時需求
|
|
|
349
350
|
|
|
350
351
|
只有在使用者明確要求推送 remote,或專案設定啟用 auto-push 時,才會執行 `git push`。如果 push 失敗,只回報認證或網路問題,不會回滾已完成的文件歸檔與本機 commit。
|
|
351
352
|
|
|
353
|
+
## 多人協作解決方案(Git-Flow 與延遲綁定 ID)
|
|
354
|
+
|
|
355
|
+
在單人開發時,循序流水號(`REQ-001`、`REQ-002`)簡單直覺;但在多人平行協作時,容易引發兩大痛點:
|
|
356
|
+
1. **序號競爭撞車**:Alice 與 Bob 同時在各自的分支開發,雙方本機都取了 `REQ-003`,合併回主幹時發生目錄重名衝突。
|
|
357
|
+
2. **中心化檔案衝突**:每次歸檔都要修改 `project/index.md`,導致頻繁的 Git merge conflict。
|
|
358
|
+
|
|
359
|
+
`sa-spec v0.2.0` 引進 **「Feature Branch 隔離 + 延遲綁定序號(Late-Binding ID)」** 完整解決方案:
|
|
360
|
+
|
|
361
|
+
```mermaid
|
|
362
|
+
flowchart TD
|
|
363
|
+
A[開新需求: sa-spec new --name cart --branch] --> B[建立並切換至 feat/cart 分支<br/>以 project/cart/ 建立草稿目錄]
|
|
364
|
+
B --> C[AI 分析與起草: sca<br/>內部使用 {{REQ_ID}} 佔位<br/>自動同步 master 規格防護]
|
|
365
|
+
C --> D{使用者確認通過<br/>執行 scc 或 finalize}
|
|
366
|
+
D --> E[查詢 master 最新序號 N]
|
|
367
|
+
E --> F[重命名為 project/REQ-N+1-cart/<br/>批次置換 {{REQ_ID}} 為正式號碼]
|
|
368
|
+
F --> G[sa-spec reindex: 動態重構 index.md]
|
|
369
|
+
G --> H[合併回 master: 零衝突、序號永遠連續!]
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### 多人協作四步流程
|
|
373
|
+
|
|
374
|
+
#### 1. 提出需求(草稿與分支隔離)
|
|
375
|
+
開需求時**不搶序號**,直接以語意名稱建立分支與草稿目錄:
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
# 自動建立並切換至 feat/cart-checkout 分支,建立 project/cart-checkout/ 草稿目錄
|
|
379
|
+
sa-spec new --name cart-checkout --branch
|
|
380
|
+
```
|
|
381
|
+
在 VS Code 側邊欄輸入:
|
|
382
|
+
```text
|
|
383
|
+
scp 購物車結帳需求
|
|
384
|
+
```
|
|
385
|
+
* **特點**:目錄名稱為 `project/cart-checkout/`,內部所有檔案皆使用 `{{REQ_ID}}` 佔位符。多人同時提出需求互不干擾、零衝突。即使需求中途放棄,直接刪除分支即可,絕不留下幽靈跳號。
|
|
386
|
+
|
|
387
|
+
#### 2. 分析與衝突檢查(規格基線同步)
|
|
388
|
+
在分支上執行 `spec:analyze`(或 `sca`)前,工具會確認已取得 `master`(或 `origin/master`)的最新規格,確保是以全專案最新基準進行影響評估,避免因分支落後而漏檢權限、API 或資料表衝突。
|
|
389
|
+
|
|
390
|
+
#### 3. 確認歸檔與延遲定號(Late-Binding)
|
|
391
|
+
當需求規格確認無誤,準備歸檔時:
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
# 自動向 master 查詢最新可用序號(例如 REQ-006)
|
|
395
|
+
sa-spec finalize --name cart-checkout
|
|
396
|
+
```
|
|
397
|
+
* **自動化執行**:
|
|
398
|
+
1. 取得最新序號(例如目前最大是 `REQ-005`,自動取得 `REQ-006`)。
|
|
399
|
+
2. 使用 `git mv` 將目錄重命名為 `project/REQ-006-cart-checkout`。
|
|
400
|
+
3. 自動掃描並將目錄內所有檔案的 `{{REQ_ID}}` 批次置換為 `REQ-006`。
|
|
401
|
+
4. 自動執行 `reindex` 重建需求總表。
|
|
402
|
+
|
|
403
|
+
#### 4. 消除索引合併衝突(`sa-spec reindex`)
|
|
404
|
+
`project/index.md` 改為由目錄動態生成的「衍生清單」,不再依賴脆弱的手動 append:
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
sa-spec reindex
|
|
408
|
+
```
|
|
409
|
+
* 任何時候若有 PR 合併產生 index 衝突,只要執行 `sa-spec reindex`,即可依序號重新排序生成乾淨的總表,無需人工手動解 conflict。
|
|
410
|
+
|
|
411
|
+
## 語言與國際化支援(i18n)
|
|
412
|
+
|
|
413
|
+
`sa-spec` 支援 5 種主要語言的系統分析規格產出與規範手冊:
|
|
414
|
+
|
|
415
|
+
| 語系代碼 | 語言名稱 | 內文與風格規範 | 專有名詞處理準則 |
|
|
416
|
+
|---|---|---|---|
|
|
417
|
+
| `zh-TW` | **繁體中文**(預設) | 台灣技術常用語(如:伺服器、支援、專案) | 強制保留原生英文(如 `REST API`, `Webhook`, `JWT`) |
|
|
418
|
+
| `en` | **English** | 100% 標準技術英文,不可出現中/日/韓文字符 | 原生技術英文 |
|
|
419
|
+
| `zh-CN` | **简体中文** | 大陸技術常用語(如:服务器、支持、项目) | 強制保留原生英文(如 `REST API`, `Webhook`, `JWT`) |
|
|
420
|
+
| `ja` | **日本語** | 日語技術文體(です/ます 或 である),外來詞尊重 Katakana 慣例 | 強制保留原生英文(如 `REST API`, `Webhook`, `JWT`) |
|
|
421
|
+
| `ko` | **한국어** | 韓語專業技術文體與規範敬語 | 強制保留原生英文(如 `REST API`, `Webhook`, `JWT`) |
|
|
422
|
+
|
|
423
|
+
### 語言管理核心原則
|
|
424
|
+
|
|
425
|
+
1. **初始化強制鎖定(Init-Time Lock)**:
|
|
426
|
+
* 語言由初始化階段決定,不可模稜兩可。
|
|
427
|
+
* **CLI 指令**:可直接指定語系參數,例如 `sa-spec init --lang en`。
|
|
428
|
+
* **AI 工作流**:在對話中輸入 `spec:init`(或 `sci`)時,若未指定語言,**AI 必須主動列出 5 種語言選單詢問使用者**,選定後寫入 `project/system-spec.md` 作為全專案唯一的真理之源(Single Source of Truth)。
|
|
429
|
+
2. **全專案語言單一純粹(Single-Language Purity)**:
|
|
430
|
+
* 選定語言後,全專案的所有產出(`srs.md`、`openapi.yaml`、`db-schema.md`、`handoff.md`、`history.md` 與 `rules/`)必須 100% 統一使用該語言,禁止在同一專案內出現語言混雜。
|
|
431
|
+
3. **專有名詞保留原生英文(Technical Terms Exception)**:
|
|
432
|
+
* 在非英語系模式(繁中、簡中、日文、韓文)下,業界公認標準之技術術語、協定、資料格式與元件名稱(例如:`REST API`、`Webhook`、`JWT`、`OAuth 2.0`、`Payload`、`Idempotency Key`、`Redis`、`UUID` 等),**一律強制維持原生英文**,嚴禁生硬直譯。
|
|
433
|
+
4. **升級保護(Upgrade Inheritance)**:
|
|
434
|
+
* 執行 `sa-spec upgrade` 時,系統會**自動偵測並繼承現有專案語言**(讀取 `project/system-spec.md`),不會詢問或變更既有專案語言,確保升級零干擾。
|
|
435
|
+
|
|
352
436
|
## 完整範例:番茄時鐘
|
|
353
437
|
|
|
354
438
|
以下示範從空資料夾開始,建立一個「番茄時鐘」需求。
|
|
@@ -597,6 +681,25 @@ git push origin master
|
|
|
597
681
|
|
|
598
682
|
## 更新紀錄
|
|
599
683
|
|
|
684
|
+
### v0.2.1
|
|
685
|
+
- **5 國語言與國際化支援(i18n)**:
|
|
686
|
+
- **支援語系**:繁體中文 (`zh-TW`,台灣常用語)、英文 (`en`)、簡體中文 (`zh-CN`)、日文 (`ja`)、韓文 (`ko`)。
|
|
687
|
+
- **初始化強制鎖定**:`sa-spec init [--lang <lang>]` 或在 AI 對話中輸入 `spec:init`,未指定時主動彈出選單詢問使用者選擇,選定後寫入 `system-spec.md`。
|
|
688
|
+
- **專案語言單一純粹**:全專案規格產出 100% 統一該語言。
|
|
689
|
+
- **技術專有名詞保留原生英文**:非英語系模式下強制保留標準英文術語(如 REST API, Webhook, JWT),嚴禁生硬直譯。
|
|
690
|
+
- **升級保護**:`sa-spec upgrade` 自動偵測並繼承現有專案語言,不變更設定。
|
|
691
|
+
|
|
692
|
+
### v0.2.0
|
|
693
|
+
- **多人協作 Git-Flow 解決方案(延遲綁定序號與零衝突索引)**:
|
|
694
|
+
- **解決痛點**:多人平行開發時,傳統流水號(`REQ-001`)容易在各自分支撞車,且中心化的 `project/index.md` 容易造成 Git merge conflict。
|
|
695
|
+
- **延遲綁定序號(Late-Binding ID)**:開需求時不搶號,在 feature branch 上以語意名稱建立草稿需求(如 `project/cart`),內部使用 `{{REQ_ID}}` 佔位;只有在 `spec:commit` 最終歸檔時,才向 master/遠端確認最新可用序號並定號(如 `REQ-003`),自動批次置換佔位符,徹底避免幽靈跳號與序號競爭。
|
|
696
|
+
- **動態重建需求索引(`sa-spec reindex`)**:將 `project/index.md` 改為根據 `project/REQ-*` 目錄動態掃描並依序號排序生成,消除手動 append 造成的 Git 合併衝突。
|
|
697
|
+
- **CLI 指令升級**:
|
|
698
|
+
- `sa-spec new [--id REQ-001] --name login [--branch]`:支援草稿模式與自動切換 feature 分支。
|
|
699
|
+
- `sa-spec finalize --name login`:自動定號、重命名目錄、置換佔位符並重建索引。
|
|
700
|
+
- `sa-spec reindex`:一鍵掃描並重新排序生成乾淨的 `project/index.md`。
|
|
701
|
+
- **分析前防護**:`spec:analyze` 執行前確保取得 master/遠端最新規格,避免分支規格落後導致漏檢衝突。
|
|
702
|
+
|
|
600
703
|
### v0.1.3
|
|
601
704
|
- **支援指令簡寫別名(同時保留完整指令)**:
|
|
602
705
|
- `sci` = `spec:init`
|
|
@@ -27,7 +27,12 @@ description: 用 init、propose、analyze、commit 四段流程整理需求,
|
|
|
27
27
|
|
|
28
28
|
## 硬性限制
|
|
29
29
|
|
|
30
|
-
-
|
|
30
|
+
- 語言與國際化規範(i18n):
|
|
31
|
+
- 支援語系:繁體中文 (`zh-TW`,台灣常用語)、英文 (`en`)、簡體中文 (`zh-CN`)、日文 (`ja`)、韓文 (`ko`) 共 5 種語言。
|
|
32
|
+
- 初始化語言鎖定:執行 `spec:init` (或 `sci`) 時,若使用者未明確指定語言,AI 必須主動詢問使用者選擇上述 5 種語言之一,不得自行預設;選定後將主要語言寫入 `project/system-spec.md`。
|
|
33
|
+
- 全專案語言單一純粹:後續所有需求文件(`srs.md`、`uml/`、`openapi.yaml`、`db-schema.md`、`handoff.md`、`history.md`)必須 100% 統一使用該語言,不得在同一專案內混雜多種語言。
|
|
34
|
+
- 專有名詞保留原生英文:在中文、日文、韓文等非英語系模式下,業界公認標準之技術術語(如 REST API, Webhook, JWT, OAuth 2.0, Payload, Idempotency, Redis 等)強制保留原生英文,嚴禁生硬直譯。
|
|
35
|
+
- 升級保護:執行 `sa-spec upgrade` 時,自動偵測並繼承現有專案語言,不變更語言設定。
|
|
31
36
|
- `spec:init` 必須檢查 Git 狀態:
|
|
32
37
|
- 若 Git 未安裝,提醒使用者後續歸檔會使用版本管理,但不可中斷初始化。
|
|
33
38
|
- 若 Git 已安裝且目前目錄尚未初始化為 Git repository,則自動執行 `git init`。
|
|
@@ -43,6 +48,7 @@ description: 用 init、propose、analyze、commit 四段流程整理需求,
|
|
|
43
48
|
- 交給 OpenSpec 或其他 AI 開發工具時,必須指定它先閱讀 `handoff.md`,並以 `handoff.md` 作為開發入口與限制摘要;其他規格文件作為詳細來源。
|
|
44
49
|
- `spec:commit` 完成文件歸檔後,必須自動建立本機 Git commit;commit message 以需求編號與需求名稱為主,並加上簡短需求說明,格式為 `docs(REQ-001): 需求名稱 - 簡短需求說明`。
|
|
45
50
|
- `spec:commit` 只有在使用者明確要求推送 remote,或專案設定啟用 auto-push 時,才可執行 `git push`;push 失敗時只回報認證或網路問題,不得回滾已完成的文件歸檔與本機 commit。
|
|
51
|
+
- 多人協作與 Git-Flow 模式(延遲綁定序號):支援多人平行開發。提出需求時可於 feature branch(如 `feat/<name>`)建立草稿目錄,內部使用 `{{REQ_ID}}` 佔位;執行 `spec:analyze` 前應先確保取得 master 最新規格以防漏檢衝突;執行 `spec:commit` 歸檔時向 master 確認最新可用序號並定號,自動將目錄定名為正式 `REQ-xxx`、替換內部佔位符、重新生成 `project/index.md`,避免多人合併時的序號競爭與索引衝突。
|
|
46
52
|
- 文件未經使用者確認,不得標示為 `已審核`。
|
|
47
53
|
- `uml/` 內的 UML 圖不限單一 UML 圖型;`spec:analyze` 必須依需求選擇最能說明流程、互動、狀態或資料關係的 UML 圖,例如活動圖、循序圖、使用案例圖、狀態圖或類別圖,不可為了固定格式硬產生活動圖。**嚴禁將多張圖混在同一個檔案中**,必須在需求資料夾下的 `uml/` 目錄拆分成獨立的 `.puml` 檔案(例如 `uml/main.puml`、`uml/sequence.puml`、`uml/state.puml`),且每個檔案只能包含一組 `@startuml` 與 `@enduml`,以避免 PlantUML 渲染或預覽失敗。
|
|
48
54
|
- 一次最多問 3 個待確認問題,優先詢問會影響範圍、資料、權限、API、DB 或驗收的問題。
|