Files
jiantw83 de7962a8c1 fix(gate): 閘門說明與實際行為對齊
兩份參考文件跟實際判定互相矛盾。照著做會漏掉收尾,或是擋錯階段。

- 模型閘門非零退出時,該回合仍要跑階段回報。提前停下來也要留紀錄,不然使用者看不到停在哪裡。
- 工作包閘門只擋分析與維護:規劃只印提醒就放行,實作一律放行。實作是結清那支 PR 的唯一路徑,擋了閘門就自己鎖死。
- 順帶把指向實作技能的步驟引用改成新的順序。
2026-08-31 11:10:30 +08:00

164 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 分支規則 — SDLC 各階段動工前的分支確認
分析要讀對分支的程式碼,實作要寫進對的分支。兩件事都不能從「目前 checkout 的分支」推定使用者的意圖。
## 總則:一律以遠端為準,不吃本地分支
SDLC 各階段引用的參考分支與來源分支,**一律指遠端的 `origin/{branch}`**。本地分支不可作為基準。
理由:本地分支可能落後遠端、可能有沒推上去的 commit、也可能是別的工作留下的狀態。以本地為基準,分析會對著過時的程式碼做,實作會從錯誤的起點長出來,而且錯了不會有任何徵兆。
| 項目 | 規則 |
| --- | --- |
| 動作之前 | 先 `git fetch --prune origin`。**沒 fetch 就用 `origin/{branch}` 等於用快取**,遠端追蹤參照可能已經過時,連分支被刪掉都看不出來 |
| 列分支 | `git branch -r`,不看 `git branch` |
| 基準與比對 | 一律寫 `origin/{branch}`,例如 `git rev-list --count origin/master..origin/develop` |
| 建立 worktree | 從 `origin/{source-branch}` 建,不從本地同名分支建 |
| 判定預設分支 | `refs/remotes/origin/HEAD`(見〔判定遠端預設分支〕) |
**本地與遠端不一致時一律停下回報**,不自行 `pull`、不自行 `reset`、不自行切換。要不要同步是使用者的決定。
`origin` 以外的遠端名稱(例如 `upstream`):先問使用者以哪個遠端為準,不臆測。
### 來源分支在遠端找不到:停下回報,不退回預設分支
已指定的來源分支,`origin/{source-branch}` 在遠端不存在時,**回報並停止**。不改用 `develop`、不改用 `master`、不套用任何後備分支,也不自行建立同名分支。這條沒有例外。
理由:來源分支是那份分析的功能分支。悄悄退回 `develop`,單一工作包就會越過自己的功能分支直接進 `develop`,而且沒有任何徵兆看得出來。
**這是停止條件,不是後備條件。**〔判定遠端預設分支〕那節的後備順序只用在「還沒有指定分支、要挑一條基準」的情況;指定過的來源分支找不到,一律套用本節。
## 先確認,再動工
| 階段 | 要確認的分支 | 確認時機 |
| --- | --- | --- |
| `analyze` | **來源分支**——哪條分支的程式碼算現況 | 讀任何程式碼之前 |
| `implement` | **來源分支**——worktree 的基準,也是每個工作包的 PR 目標 | 寫任何檔案之前 |
確認方式:
1. 先 `git fetch --prune origin`,再把事實攤開:目前分支、工作區是否乾淨、遠端有哪些分支(`git branch -r`,不看本地)、下節判定出的遠端預設分支。
2. 依 `jsc-ask:ask` 的決策樹詢問,每個選項都要標明影響範圍。分析錯分支會產出對不上程式碼的工作包;來源分支錯了則 worktree 從錯的起點長出來,PR 也會開到錯的地方。
3. 把確認結果寫進產出(分析頁的來源分支欄),後續步驟一律沿用同一個答案,不再自行改判。
4. 使用者沒回答就**不預設** `develop`/`master`;預設值只在使用者明確同意後才成立。
## 判定遠端預設分支
1. 先用 `git symbolic-ref --quiet refs/remotes/origin/HEAD`,結果形如 `refs/remotes/origin/<預設分支>`。
2. 取不到時退而用 `git remote show origin`,找輸出中的 `HEAD branch:` 那行。
3. 兩者都取不到 → 回報並停止,**不臆測** `master`/`main`。
還沒有指定分支、需要挑一條基準或後備分支時(例如維護階段要落腳的分支):`origin/develop` 存在就用 `develop`;否則 `origin/master` 存在就用 `master`;兩者皆無則回報並停止。
這條後備順序**不適用於已指定的來源分支**。指定過的來源分支在遠端找不到,一律回報並停止,見總則的〔來源分支在遠端找不到〕。
## 只讀階段不動工作區
`analyze` 是 logic-only 階段:
- **工作目錄的 HEAD 必須與 `origin/{source-branch}` 指向同一個 commit**。落後、超前或分歧都代表讀到的不是遠端現況——停下來回報差距(`git rev-list --left-right --count origin/{source-branch}...HEAD`),請使用者自己處理。
- 需要換分支才能讀到正確現況時,**停下來請使用者自己切換**。
- 不代為 `switch`、不 `stash`、不動工作區、不建分支。
## 分支階梯與 base 推導
階梯表本身(哪一種類型往上接哪一條、`{子功能}` 怎麼組、base 為什麼一律由 `jsc-git/tools/base-branch.sh --derive` 推導)的唯一來源,在 `jsc-meta` 的 [`references/guidelines.md`](https://gitea.jsc.idv.tw/plugins/meta/src/branch/develop/references/guidelines.md)「PR 分支階梯」那一節。本檔不抄第二份,只寫 sdlc 拿到腳本回應之後要做什麼:
| 腳本回應 | 這個階段要做的事 |
| --- | --- |
| 印出一條分支名(結束碼 `0`) | 拿它當 `jsc-git:pr` 的 base,不再自行改判 |
| 結束碼 `7`(推不出唯一合法基底) | **中止並問使用者**,不退回 `develop`——退回 `develop` 就是讓子功能越級直接進主線 |
| stderr 印出「已建立功能主幹」 | 在階段回報裡講明建了哪一條主幹,那是這次實作多長出來的一條分支 |
| 結束碼 `2`(分支名含非 ASCII) | 先把中文簡述翻成英文短語,過 `jsc-git/tools/slugify.sh {類型} {英文短語}` 再重組分支名 |
## 實作階段的分支選擇
實作在 worktree 內進行,工作分支從 `origin/{source-branch}` 長出來,做完再 PR 回同一條來源分支。
- **一個工作包一個 PR**,目標一律是 `origin/{source-branch}`。不把兩個完成的工作包併成一個 PR,也不把完成的工作包留到下一包一起送——拆工作包的意義就是各自能獨立審查。
- 呼叫 `jsc-git:pr` 時**必須明確帶入來源分支當 base**。該 skill 在沒收到 base 時會自行退回 `develop`,那會讓單一工作包越過它所屬的功能分支直接進 develop。
- 來源分支整條後續要進 `develop` 時,那是另一個獨立的 PR,不在本階段範圍。
- 新分支名稱要可讀且不覆蓋既有分支;本地或遠端已存在同名分支時,換一個時間戳或短 hash。
- 切換或建立分支屬不可忽略的狀態變更,要在輸出中講清楚原因與結果分支名稱。
## 實作一律在 worktree 內進行
`implement` 動任何程式碼之前,先 `git fetch --prune origin`,再從**分析頁記錄的來源分支的遠端版本**(`origin/{source-branch}`)建立 git worktree。所有修改都在 worktree 內,主工作目錄的分支與工作區完全不動。
### 路徑
```
{cwd}/.worktree/{analysis-HASH}/{wp-number}/{repo}
```
- `{analysis-HASH}`:`ANALYZE_{HASH}` 的 HASH 部分,不含 `ANALYZE_` 前綴。
- `{wp-number}`:這次要做的工作包編號(例如 `WP-06`)。**一定要有這一層**:同一份分析常有好幾個互不相依的工作包平行進行,路徑少了這一層,兩個工作包會搶同一個目錄——第二個到的會被「目標路徑已存在」擋下,因為它不是同一份工作的 worktree。
- `{repo}`:存取庫名稱,不含 owner(`HP/WebService.Buy` → `WebService.Buy`)。不同 owner 的同名存取庫同時出現時,才改用 `{owner}-{repo}` 避免蓋掉,並在輸出中說明。
- 一份分析涉及多個存取庫時,每個存取庫各一個 worktree,並列在同一個 `{wp-number}` 目錄下。
### 建立前先問分支怎麼處理
依 `jsc-ask:ask` 的決策樹詢問,每個選項標明影響範圍。固定兩個選項:
| 選項 | 指令 | 影響 |
| --- | --- | --- |
| 以來源分支為基準開新工作分支 | `git worktree add -b {work-branch} {path} "origin/{source-branch}"` | commit 落在新分支,來源分支不動 |
| 直接簽出來源分支 | `git worktree add --track -b {source-branch} {path} "origin/{source-branch}"` | 建立追蹤遠端的本地分支再簽出;本地已存在同名分支時指令會失敗,此時先確認它與遠端一致才可改用 `git worktree add {path} {source-branch}`,不一致就停下回報 |
**不得自行預設**,也不得跳過詢問。
### 建立時的鐵則
- **分支名與路徑一律加引號**:既有的來源分支可能含空白、多層斜線或非 ASCII 字元(例如 `feat/addr-lookup/query/full/P2` 這種多層命名,或更舊的分支帶空白),不加引號會被切斷。
- **jsc 自己開的分支只用 ASCII**(`a-z0-9` 與 `/`、`-`)。中文標題先翻譯成英文短語,再 slug 化當分支名。
- 找不到該存取庫的本地 clone → **停下來問使用者路徑**,不自行 clone、不臆測位置。
- 目標路徑已存在 → 不覆蓋。先確認它是不是同一份工作的 worktree(`git worktree list`),是就沿用,不是就回報並停止。
- 把 `.worktree/` 加進該存取庫的 `.git/info/exclude`(不動使用者的 `.gitignore`,那是專案共用檔)。
- 建立後在輸出中明確列出:worktree 路徑、簽出的分支、來源分支,以及 `origin/{source-branch}` 當下的 commit sha——那是這次實作的起點,要能事後追。
### 移除時機
**PR 合併之後才移除**:`git worktree remove {path}`,接著 `git worktree prune`。
不是 PR 建立後就移除——審查留言可能要求修改,而修改要回到同一個 worktree、推到同一條工作分支(PR 會自己更新,不開第二個 PR)。先移除就得為了改一行重建整個 worktree。
- PR 尚未合併 → 保留。PR 建立失敗 → 保留,讓使用者接手處理。
- worktree 內還有未提交變更時**不移除**,回報並停止——那些變更沒有進 PR,移除等於丟掉。
- 同一份分析的多個 worktree 全部移除後,若 `.worktree/{HASH}/` 已空就一併刪掉那層目錄。
### 一個工作包的 PR 沒合併,擋的是相依於它的包,不是整份分析
一個工作包的 PR 還沒合併,這一包就沒結清:修正一律回原 worktree、推同一條工作分支,**不為同一包開第二個 PR**。
但「這一包沒結清」不等於「不得開始下一包」——SDLC 實作可能有好幾個互不相依的工作包平行進行,各自的 worktree、各自的 PR。真正被擋住的只有**相依於它的**工作包;跟它無關的工作包不受影響,可以另開一個工作階段(或另一個 worktree)同時進行。
這條規則在程式層強制,不靠內文自律,分工是兩層:
| 層 | 誰執行 | 做什麼 |
| --- | --- | --- |
| 工具閘門 | `tools/wp-gate.sh` | 唯一去問 Gitea 的一方,也是唯一有權解鎖的一方。`check` 查合併狀態、印出全部留言、印 `latest=` 時間戳,只管那一支 PR 自己;`check-deps` 查某個候選工作包能不能挑,活抓分析頁的相依欄逐一核對;`claim` 在領包當下登錄歸屬;`lock` 在 PR 開好後記下未結清並把 PR 掛到工作包名下;`owns` 比對某支 PR 是不是自己這一包的。狀態檔的寫入一律轉呼叫 `sdlc-gate.sh`。結束碼 0 放行、1 擋住、2 用法錯誤、3 查不到(查不到就擋,不放行) |
| hook 提醒 | `jsc-hooks` 的 `sdlc-gate.sh wp-check` | 只讀 `$JSC_HOME/wp/` 的狀態檔,不打網路。`prompt` 模式注入提醒但**不擋提示**;`skill` 模式擋掉 `analyze` 與 `maintain`,對 `plan` 只印提醒就放行,對 `implement` 一律放行——擋住的是「跳去做別的階段」,不是「回來把這一包做完」。`implement` 放行是閘門不自鎖的關鍵:結清那支 PR 的唯一路徑就是它,擋了就沒有人解得開。`plan` 降成提醒之後,放棄的是「手上工作包沒結清就別開新計畫」這道在製品上限,`analyze` 那道仍在,上限只是晚一個階段才生效。這一層是整個存取庫共用的粗粒度提醒,不是逐工作包判斷,細粒度的相依判斷交給 `check-deps`,歸屬比對交給 `owns` |
狀態檔不綁 session:PR 沒合併就是沒合併,開新對話照樣擋。
### 工作包隔離:一個工作階段只碰自己領的那一包
同一份分析常有好幾包平行進行,每包各自的 worktree 與 PR。動任何一支 PR 之前先確認它是自己這一包的,靠的是狀態檔而不是記憶——記憶跨不了工作階段。
| 項目 | 規則 |
| --- | --- |
| 歸屬依據 | **分析頁上的工作包代號**。不看分支名、不看 worktree 目錄名,那兩個都可能被改,改了也沒有徵兆。`WP-03`、`WP-3`、`3` 視為同一包 |
| 狀態檔 | 領取檔 `$JSC_HOME/wp/{owner}-{repo}.claim`(欄位 `repo`、`wp`、`pr`、`analyze`、`claimed`,一個存取庫一支)與鎖檔 `$JSC_HOME/wp/{owner}-{repo}-{index}.pr`(欄位 `repo`、`index`、`wp`、`locked`)。兩者都是純文字 key=value,一行一欄 |
| 誰寫 | 只有 `jsc-hooks` 的 `sdlc-gate.sh`:領包時 `wp-claim`、開完 PR 時 `wp-lock` 的第四個參數帶工作包代號、結清或交回時 `wp-unclaim` 與 `wp-unlock`。`wp-gate.sh` 一律轉呼叫,**不自己拼檔案**——兩邊各拼各的,格式一改就對不上,而且誰寫壞了看不出來 |
| 誰讀 | `wp-gate.sh owns` 與 `sdlc-gate.sh wp-check`,兩邊都只讀不改 |
| 查無歸屬 | **放行只提醒**:沒有分析頁、查不到工作包代號、領取檔不存在、工作包還沒掛上 PR,一律 `status=unowned` 並 exit 0。這跟「查不到就擋」不衝突——那條講的是查得到卻查失敗,這條講的是根本還沒有歸屬可查,拿不存在的歸屬擋人會讓沒登錄過的工作包全部動不了 |
| 逃生門 | `JSC_WP_GATE=off`,由 hook 那一層認 |
查驗程序的細節(何時帶 `--since`、怎麼等 PR 合併、留言逐筆的處理結果、修不動的留言怎麼辦、時間戳寫回哪裡)由 `skills/implement/SKILL.md` 步驟 2 與步驟 11 擁有,本檔不重複。
## 不破壞既有工作
- 工作區有未提交變更時,先提醒使用者 commit 或備份,**絕不**強制丟棄。
- 未提交變更導致切換分支或 pull 失敗 → 停止並回報,請使用者處理。
- **絕不** `reset --hard`/`checkout -f`/`clean`。