Merge pull request 'develop 進 master:CPM 甘特圖修正、工作包 PR 閘門平行化' (#25) from develop into master

Reviewed-on: #25
Reviewed-by: 系統管理員 <1+admin@noreply.localhost>
This commit was merged in pull request #25.
This commit is contained in:
2026-08-26 10:15:07 +00:00
7 changed files with 198 additions and 31 deletions
+3 -3
View File
@@ -22,7 +22,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
| 腳本 | 用途 | | 腳本 | 用途 |
| --- | --- | | --- | --- |
| `tools/wp-gate.sh` | 工作包 PR 閘門,把「PR 沒合併就不開下一包」從內文敘述變成程式判定,共兩個用法。`check {owner}/{repo} {index} [--since {ISO 時間}]` 查 PR:已合併就呼叫 `jsc-hooks` 的 `sdlc-gate.sh wp-unlock` 解鎖並印 `status=merged`;沒合併就印 `status=open` 或 `status=closed-unmerged`(被關掉但沒合併不算完成),接著把 issue 留言、審查評語、行內留言全部逐行印出(`--since` 只印更新的,值取上一輪的 `latest=`),最後一行印 `latest={最新一筆留言的時間戳}` 供寫回分析頁。`lock {owner}/{repo} {index}` 在 PR 開好後轉呼叫 `sdlc-gate.sh wp-lock` 記下未結清。第一行固定 `status=...` 供程式判讀,其後為繁中說明;結束碼 `0`=已合併或無阻擋、`1`=未合併(擋住,呼叫端必須停下來逐筆修留言)、`2`=用法錯誤、`3`=相依工具或 PR 查不到(**查不到就擋,不放行**)。狀態檔由 `jsc-hooks` 管(`$JSC_HOME/wp/{owner}-{repo}.pr`,不綁 session,開新對話照樣擋),本檔只負責查詢與結清 | | `tools/wp-gate.sh` | 工作包 PR 閘門,把「一個工作包的 PR 沒合併,擋的是相依於它的工作包,不是整份分析」從內文敘述變成程式判定,共三個用法。`check {owner}/{repo} {index} [--since {ISO 時間}]` 查一支 PR:已合併就呼叫 `jsc-hooks` 的 `sdlc-gate.sh wp-unlock` 解鎖並印 `status=merged`;沒合併就印 `status=open` 或 `status=closed-unmerged`(被關掉但沒合併不算完成),接著把 issue 留言、審查評語、行內留言全部逐行印出(`--since` 只印更新的,值取上一輪的 `latest=`),最後一行印 `latest={最新一筆留言的時間戳}` 供寫回分析頁。`check-deps {owner}/{repo} {wp-number}` 查某個候選工作包能不能挑:活抓分析頁 WBS 表的相依欄,逐一核對每個相依工作包的狀態與 PR 是否已合併,只認同一張分析頁上的 `WP-NN` 編號,指到別份計畫的文字項目查不了就列出來要求人工確認,不當成擋人的理由。`lock {owner}/{repo} {index}` 在 PR 開好後轉呼叫 `sdlc-gate.sh wp-lock` 記下未結清(一個工作包一支鎖檔,可以同時記好幾筆平行進行的工作包)。第一行固定 `status=...` 供程式判讀,其後為繁中說明;結束碼 `0`=已合併或無阻擋(含 `check-deps` 的 `ready`)、`1`=未合併或 `check-deps` 判定 `blocked`、`2`=用法錯誤、`3`=相依工具或 PR/分析頁查不到(**查不到就擋,不放行**)。狀態檔由 `jsc-hooks` 管(`$JSC_HOME/wp/{owner}-{repo}-{index}.pr`,不綁 session,開新對話照樣擋),本檔只負責查詢與結清 |
| `tools/stage-report.sh` | 階段收尾回報,四個階段共用。`stage-report.sh {plan\|analyze\|implement\|maintain}` 加 `--page TYPE:PAGE`(本階段寫過的每一頁,可重複)產出繁中回報:模型閘門判定(轉述 `sdlc-gate.sh report`,不自評)、工作日誌連結(`--worklog`、`--worklog-heading` 組出導向條目標題的錨點)、所有寫入的 wiki 絕對網址。實作階段再加 `--worktree`、`--source-branch`、`--work-branch`、`--target-branch`、`--pr`,來源分支在不在遠端、工作分支幾個 commit、推送了沒,都由腳本現查。沒有工作日誌時警告使用者檢查,並用 `--pending-file`、`--log-hash` 把內容交給 `jsc-log/tools/worklog-pending.sh` 暫存,下次寫日誌一併寫入。結束碼 `0`=完整、`1`=有警告(**警告不是阻擋**)、`2`=用法錯誤、`3`=相依工具找不到 | | `tools/stage-report.sh` | 階段收尾回報,四個階段共用。`stage-report.sh {plan\|analyze\|implement\|maintain}` 加 `--page TYPE:PAGE`(本階段寫過的每一頁,可重複)產出繁中回報:模型閘門判定(轉述 `sdlc-gate.sh report`,不自評)、工作日誌連結(`--worklog`、`--worklog-heading` 組出導向條目標題的錨點)、所有寫入的 wiki 絕對網址。實作階段再加 `--worktree`、`--source-branch`、`--work-branch`、`--target-branch`、`--pr`,來源分支在不在遠端、工作分支幾個 commit、推送了沒,都由腳本現查。沒有工作日誌時警告使用者檢查,並用 `--pending-file`、`--log-hash` 把內容交給 `jsc-log/tools/worklog-pending.sh` 暫存,下次寫日誌一併寫入。結束碼 `0`=完整、`1`=有警告(**警告不是阻擋**)、`2`=用法錯誤、`3`=相依工具找不到 |
## Skills 目錄 ## Skills 目錄
@@ -41,7 +41,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
### `implement` ### `implement`
實作:先確認來源分支(同時是 PR 目標)→ 產生工作證鎖定「未完成、無相依、無工作證」的工作包(交付工作包優先)→ 交付工作包開工前先確認交付內容(API 文件、由使用者輸入,見 `references/deliver-formats.md`)→ **動程式碼前先從 `origin/{source-branch}` 建立 worktree**(`.worktree/{analysis-HASH}/{repo}`,分支處理依決策樹詢問)→ 在 worktree 內逐項 TDD 實作、每完成一項立即更新 wiki → 程式碼審查 → **每完成一個工作包就 commit、push、PR 回來源分支**(一包一 PR),接著 `tools/wp-gate.sh lock` 上鎖 → **PR 未合併就不開下一包,判定在程式層**:領工作包前先跑 `tools/wp-gate.sh check`,未合併就把全部留言逐筆印出並擋住,以 sub agent 回原 worktree 逐筆修正、推同一條工作分支(不開第二個 PR),修不動或純討論的留言忽略但要在回報裡列出,處理過的時間戳寫回分析頁的 PR 欄位當下一輪的 `--since`;合併後解鎖並移除 worktree → 詢問交付文件格式(`DELIVER_{HASH}` wiki 頁或 Gitea 議題留言)並產出 → 詢問是否加入維護目錄 → 階段回報(`tools/stage-report.sh`,多報工作目錄與來源、工作、目標三條分支)。 實作:先確認來源分支(同時是 PR 目標)→ 領包前先跑 `tools/wp-gate.sh check` 把每個已開 PR 但沒合併的工作包留言逐筆印出並修正(以 sub agent 回原 worktree、推同一條工作分支,不開第二個 PR),**這一步只結清舊 PR 的留言,不擋別的工作包**——修不動或純討論的留言忽略但要在回報裡列出,處理過的時間戳寫回分析頁的 PR 欄位當下一輪的 `--since` → 產生工作證鎖定「未完成、無工作證」的候選工作包(交付工作包優先),**每個候選都跑 `tools/wp-gate.sh check-deps` 判斷能不能挑**:活查它在分析頁上的相依工作包是否都已合併,只有相依於它的包才會被一支未合併的 PR 擋住,跟它無關的工作包可以平行進行 → 交付工作包開工前先確認交付內容(API 文件、由使用者輸入,見 `references/deliver-formats.md`)→ **動程式碼前先從 `origin/{source-branch}` 建立 worktree**(`.worktree/{analysis-HASH}/{wp-number}/{repo}`,一個工作包一個 worktree,分支處理依決策樹詢問)→ 在 worktree 內逐項 TDD 實作、每完成一項立即更新 wiki → 程式碼審查 → **每完成一個工作包就 commit、push、PR 回來源分支**(一包一 PR),接著 `tools/wp-gate.sh lock` 上鎖,合併後解鎖並移除 worktree → 詢問交付文件格式(`DELIVER_{HASH}` wiki 頁或 Gitea 議題留言)並產出 → 詢問是否加入維護目錄 → 階段回報(`tools/stage-report.sh`,多報工作目錄與來源、工作、目標三條分支)。
### `maintain` ### `maintain`
@@ -61,7 +61,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
| `references/stage-report.md` | 階段回報:四階段都要交的三項(模型能力標籤、工作日誌連結、所有寫入的 wiki 連結)、實作階段多交的四項、沒寫日誌時的暫存規則、提前停止也要回報 | | `references/stage-report.md` | 階段回報:四階段都要交的三項(模型能力標籤、工作日誌連結、所有寫入的 wiki 連結)、實作階段多交的四項、沒寫日誌時的暫存規則、提前停止也要回報 |
| `references/model-gate.md` | 模型閘門:執行順序、各階段必要標籤、阻擋與回報的鐵則 | | `references/model-gate.md` | 模型閘門:執行順序、各階段必要標籤、阻擋與回報的鐵則 |
| `references/tdd.md` | 接縫、紅綠循環規則、反模式 | | `references/tdd.md` | 接縫、紅綠循環規則、反模式 |
| `references/branch.md` | 分支規則:**一律以遠端 `origin/{branch}` 為準、動作前先 fetch**、分析前確認來源分支、實作沿用同一條來源分支作為 PR 目標(一包一 PR、PR 未合併不開下一包,閘門分工見該檔)、實作一律在 `.worktree/{HASH}/{repo}` 內進行(建立前問分支、PR 合併後才移除)、判定遠端預設分支、不破壞未提交變更 | | `references/branch.md` | 分支規則:**一律以遠端 `origin/{branch}` 為準、動作前先 fetch**、分析前確認來源分支、實作沿用同一條來源分支作為 PR 目標(一包一 PR,一個工作包的 PR 沒合併只擋相依於它的工作包,不擋整份分析,閘門分工見該檔)、實作一律在 `.worktree/{HASH}/{wp-number}/{repo}` 內進行——一個工作包一個 worktree,讓互不相依的工作包能平行進行不互相搶路徑(建立前問分支、PR 合併後才移除)、判定遠端預設分支、不破壞未提交變更 |
| `references/consensus.md` | 規劃與分析的提問規則:一輪不算問完、共識的兩個判定條件、未決項處理 | | `references/consensus.md` | 規劃與分析的提問規則:一輪不算問完、共識的兩個判定條件、未決項處理 |
| `references/deliver-formats.md` | 交付內容型別:API 文件必備欄位、範例資料優先序、既有端點的新舊參數標示 | | `references/deliver-formats.md` | 交付內容型別:API 文件必備欄位、範例資料優先序、既有端點的新舊參數標示 |
+9 -6
View File
@@ -77,12 +77,13 @@ SDLC 各階段引用的參考分支與來源分支,**一律指遠端的 `origi
### 路徑 ### 路徑
``` ```
{cwd}/.worktree/{analysis-HASH}/{repo} {cwd}/.worktree/{analysis-HASH}/{wp-number}/{repo}
``` ```
- `{analysis-HASH}`:`ANALYZE_{HASH}` 的 HASH 部分,不含 `ANALYZE_` 前綴。 - `{analysis-HASH}`:`ANALYZE_{HASH}` 的 HASH 部分,不含 `ANALYZE_` 前綴。
- `{wp-number}`:這次要做的工作包編號(例如 `WP-06`)。**一定要有這一層**:同一份分析常有好幾個互不相依的工作包平行進行,路徑少了這一層,兩個工作包會搶同一個目錄——第二個到的會被「目標路徑已存在」擋下,因為它不是同一份工作的 worktree。
- `{repo}`:存取庫名稱,不含 owner(`HP/WebService.Buy` → `WebService.Buy`)。不同 owner 的同名存取庫同時出現時,才改用 `{owner}-{repo}` 避免蓋掉,並在輸出中說明。 - `{repo}`:存取庫名稱,不含 owner(`HP/WebService.Buy` → `WebService.Buy`)。不同 owner 的同名存取庫同時出現時,才改用 `{owner}-{repo}` 避免蓋掉,並在輸出中說明。
- 一份分析涉及多個存取庫時,每個存取庫各一個 worktree,並列在同一個 HASH 目錄下。 - 一份分析涉及多個存取庫時,每個存取庫各一個 worktree,並列在同一個 `{wp-number}` 目錄下。
### 建立前先問分支怎麼處理 ### 建立前先問分支怎麼處理
@@ -114,16 +115,18 @@ SDLC 各階段引用的參考分支與來源分支,**一律指遠端的 `origi
- worktree 內還有未提交變更時**不移除**,回報並停止——那些變更沒有進 PR,移除等於丟掉。 - worktree 內還有未提交變更時**不移除**,回報並停止——那些變更沒有進 PR,移除等於丟掉。
- 同一份分析的多個 worktree 全部移除後,若 `.worktree/{HASH}/` 已空就一併刪掉那層目錄。 - 同一份分析的多個 worktree 全部移除後,若 `.worktree/{HASH}/` 已空就一併刪掉那層目錄。
### PR 未合併就不開下一個工作包 ### 一個工作包的 PR 沒合併,擋的是相依於它的包,不是整份分析
一個工作包的 PR 還沒合併,就不得開始下一包。修正一律回原 worktree、推同一條工作分支,**不為同一包開第二個 PR**。 一個工作包的 PR 還沒合併,這一包就沒結清:修正一律回原 worktree、推同一條工作分支,**不為同一包開第二個 PR**。
但「這一包沒結清」不等於「不得開始下一包」——SDLC 實作可能有好幾個互不相依的工作包平行進行,各自的 worktree、各自的 PR。真正被擋住的只有**相依於它的**工作包;跟它無關的工作包不受影響,可以另開一個工作階段(或另一個 worktree)同時進行。
這條規則在程式層強制,不靠內文自律,分工是兩層: 這條規則在程式層強制,不靠內文自律,分工是兩層:
| 層 | 誰執行 | 做什麼 | | 層 | 誰執行 | 做什麼 |
| --- | --- | --- | | --- | --- | --- |
| 工具閘門 | `tools/wp-gate.sh` | 唯一去問 Gitea 的一方,也是唯一有權解鎖的一方。`check` 查合併狀態、印出全部留言、印 `latest=` 時間戳;`lock` 在 PR 開好後記下未結清。結束碼 0 放行、1 擋住、2 用法錯誤、3 查不到(查不到就擋,不放行) | | 工具閘門 | `tools/wp-gate.sh` | 唯一去問 Gitea 的一方,也是唯一有權解鎖的一方。`check` 查合併狀態、印出全部留言、印 `latest=` 時間戳,只管那一支 PR 自己;`check-deps` 查某個候選工作包能不能挑,活抓分析頁的相依欄逐一核對;`lock` 在 PR 開好後記下未結清(一個工作包一支鎖檔)。結束碼 0 放行、1 擋住、2 用法錯誤、3 查不到(查不到就擋,不放行) |
| hook 提醒 | `jsc-hooks` 的 `sdlc-gate.sh wp-check` | 只讀 `$JSC_HOME/wp/` 的狀態檔,不打網路。`prompt` 模式注入提醒但**不擋提示**;`skill` 模式擋掉 `plan`、`analyze`、`maintain`,放行 `implement`——擋住的是「跳去做別的階段」,不是「回來把這一包做完」 | | hook 提醒 | `jsc-hooks` 的 `sdlc-gate.sh wp-check` | 只讀 `$JSC_HOME/wp/` 的狀態檔,不打網路。`prompt` 模式注入提醒但**不擋提示**;`skill` 模式擋掉 `plan`、`analyze`、`maintain`,放行 `implement`——擋住的是「跳去做別的階段」,不是「回來把這一包做完」。這一層是整個存取庫共用的粗粒度提醒,不是逐工作包判斷,細粒度的相依判斷交給 `check-deps` |
狀態檔不綁 session:PR 沒合併就是沒合併,開新對話照樣擋。 狀態檔不綁 session:PR 沒合併就是沒合併,開新對話照樣擋。
+33
View File
@@ -0,0 +1,33 @@
# CPM 甘特圖畫法
`analyze` 步驟 8 畫關鍵路徑圖時,一律照本檔語法;不得自行發明日期算法。
## 事故
`dateFormat X`(Unix 時間戳)配上天數換算出的小數(例:`1.3`、`5.0`)會讓 Gitea 的 mermaid 渲染器丟出 `Invalid date:5.0`,整張圖不顯示。原因是 `X` 格式的日期權杖只吃整數,小數的整數部分之外還剩零頭,解析失敗;即使剩的零頭是 `.0`,字串比對也對不上,一樣判定無效。**小數與 `dateFormat X` 不能同框,沒有例外。**
## 規則
1. **`dateFormat` 固定用 `YYYY-MM-DD`,不用 `X`。** 第一個工作包給一個任意錨點日期(例如 `2000-01-01`),註明「錨點日期非真實排程,只用於相對呈現」。
2. **每個工作包一個 `id`**,格式 `wp01`、`wp02`……跟編號對應,全小寫、不帶連字號(mermaid id 不吃 `-`)。
3. **時長一律用整數小時 `{h}h`,直接取 WBS 表的工時(h) 欄位**,不得把工時換算成天數小數再填進圖裡。天數換算只留在標題與 WBS 表文字,圖本身不出現小數。
4. **除了第一個工作包,其餘一律用 `after {上一個id}`** 串接,不手算累積日期。少算或算錯只會出现在這一步,串接法直接消掉這類錯誤。
5. 關鍵路徑上的工作包標 `crit`;候補(非關鍵路徑)不進這張圖,只留在 WBS 表與候補清單文字。
## 範本
關鍵路徑 `WP-02 → WP-05 → WP-09`,工時依序 10h、13h、8h:
```mermaid
gantt
title 關鍵路徑(單位:小時;錨點日期非真實排程,僅供相對呈現)
dateFormat YYYY-MM-DD
axisFormat %m-%d %H:%M
section 關鍵路徑
WP-02 {名稱} :crit, wp02, 2000-01-01, 10h
WP-05 {名稱} :crit, wp05, after wp02, 13h
WP-09 {名稱} :crit, wp09, after wp05, 8h
```
- 總天數(`= 總工時 / 8`,四捨五入到小數點後 1 位)寫進標題旁的文字或圖下方一行,不進圖本身。
- 完成條件:圖裡每個時長都是整數加 `h`,沒有任何小數;每個工作包都有 `id`;除第一個外全部用 `after`。
+1 -1
View File
@@ -31,7 +31,7 @@ All wiki reads and writes go through `jsc-gitea:wiki`.
Completion condition: every user story has reached consensus under both conditions of `references/consensus.md`, and every reuse decision — reused, or rejected with its reason — is recorded in 複用決策. Completion condition: every user story has reached consensus under both conditions of `references/consensus.md`, and every reuse decision — reused, or rejected with its reason — is recorded in 複用決策.
6. Run a **Work Breakdown Structure (WBS)**: split the user stories into work packages, number them sequentially (`WP-01`, `WP-02`, ...) and mark dependencies. Completion condition: every user story on the plan page maps to at least one numbered work package, and every dependency edge is recorded in the WBS table's 相依 column. 6. Run a **Work Breakdown Structure (WBS)**: split the user stories into work packages, number them sequentially (`WP-01`, `WP-02`, ...) and mark dependencies. Completion condition: every user story on the plan page maps to at least one numbered work package, and every dependency edge is recorded in the WBS table's 相依 column.
7. **`WP-01` is always the delivery/handover work package** — see "Delivery package is WP-01" below. It stands alone, never merged into an implementation package, and every implementation package that consumes its spec depends on it. Completion condition: `WP-01` is marked 交付 `是` and holds only spec-shaped items, and every implementation package that consumes its spec names `WP-01` in its 相依 column — or the no-handover case below is confirmed with the user and its reason is written on the page. 7. **`WP-01` is always the delivery/handover work package** — see "Delivery package is WP-01" below. It stands alone, never merged into an implementation package, and every implementation package that consumes its spec depends on it. Completion condition: `WP-01` is marked 交付 `是` and holds only spec-shaped items, and every implementation package that consumes its spec names `WP-01` in its 相依 column — or the no-handover case below is confirmed with the user and its reason is written on the page.
8. Estimate every work package's effort in hours and days with the **Critical Path Method (CPM)**, and mark the critical path. Completion condition: every work package carries an hours figure and a days figure, and the critical path plus its total days are written on the page. 8. Estimate every work package's effort in hours and days with the **Critical Path Method (CPM)**, and mark the critical path. Draw the critical path as a mermaid gantt chart per `references/cpm-chart.md` — its date/duration rules are mandatory, not a suggestion; skipping them is how the `Invalid date` rendering failure happens. Completion condition: every work package carries an hours figure and a days figure, the critical path plus its total days are written on the page, and the gantt chart follows `references/cpm-chart.md` exactly (integer-hour durations, `after` chaining, no `dateFormat X`).
9. Split every work package into todos with **Test-Driven Development (TDD)**, formatted `[ ]` (open) / `[x]` (done). Each todo is one vertical slice: one seam, one test, one minimal implementation. Seams and anti-patterns: `references/tdd.md`. Completion condition: every work package carries at least one `[ ]` todo, and every todo names its seam and the behaviour its test asserts. 9. Split every work package into todos with **Test-Driven Development (TDD)**, formatted `[ ]` (open) / `[x]` (done). Each todo is one vertical slice: one seam, one test, one minimal implementation. Seams and anti-patterns: `references/tdd.md`. Completion condition: every work package carries at least one `[ ]` todo, and every todo names its seam and the behaviour its test asserts.
10. Apply `templates/analyze-page.md` to create or update the analysis page and write it back via `jsc-gitea:wiki`. The page content is Traditional Chinese, exactly as the template dictates. Completion condition: the page is saved on the wiki and carries every section the template dictates, including the source branch, the head sha and the 未決項 section (「無」 when there is none). 10. Apply `templates/analyze-page.md` to create or update the analysis page and write it back via `jsc-gitea:wiki`. The page content is Traditional Chinese, exactly as the template dictates. Completion condition: the page is saved on the wiki and carries every section the template dictates, including the source branch, the head sha and the 未決項 section (「無」 when there is none).
11. If the analysis page is new: add it to `ANALYZE_CONTENTS` per `templates/analyze-contents.md`, and flip the plan's status in `PLAN_CONTENTS` to the literal 「已分析」. Completion condition: `ANALYZE_CONTENTS` shows the new row and `PLAN_CONTENTS` shows the literal 「已分析」, both saved on the wiki. 11. If the analysis page is new: add it to `ANALYZE_CONTENTS` per `templates/analyze-contents.md`, and flip the plan's status in `PLAN_CONTENTS` to the literal 「已分析」. Completion condition: `ANALYZE_CONTENTS` shows the new row and `PLAN_CONTENTS` shows the literal 「已分析」, both saved on the wiki.
+10 -6
View File
@@ -1,6 +1,6 @@
--- ---
name: implement name: implement
description: SDLC implementation stage. Gate on capability tags enforced in code by sdlc-gate (implement requires coding), then confirm the analysis page's source branch - it is both the worktree base and the PR target. An unmerged work package PR is a hard gate enforced in code by tools/wp-gate.sh: it reads every PR comment, sub agents fix them in the original worktree, and no next package starts until that PR merges. Claim a ready work package from ANALYZE_CONTENTS with a work ticket, confirm a delivery package's content type, then complete its TDD todos one at a time inside a worktree built from origin/{source-branch}, updating the wiki after every item, closing with jsc-review code-review, one PR back to the source branch, the chosen delivery document, an optional MAINTAIN_CONTENTS entry, and a tools/stage-report.sh report covering the model tag verdict, the worklog link, every wiki link written, the worktree and the three branches. Use when analysis is done and code must be written; not for planning or analysis. description: SDLC implementation stage. Gate on capability tags enforced in code by sdlc-gate (implement requires coding), then confirm the analysis page's source branch - it is both the worktree base and the PR target. Every already-open work package's PR comments get triaged via tools/wp-gate.sh check (sub agents fix them in that package's own worktree), but that never blocks a different, unrelated package - independent work packages can proceed in parallel. Picking a candidate is gated per-candidate in code by tools/wp-gate.sh check-deps, which re-reads the analysis page's WBS dependency column and queries Gitea itself: only a candidate whose own declared dependencies are merged may be claimed. Claim a ready work package from ANALYZE_CONTENTS with a work ticket, confirm a delivery package's content type, then complete its TDD todos one at a time inside a worktree built from origin/{source-branch}, updating the wiki after every item, closing with jsc-review code-review, one PR back to the source branch, the chosen delivery document, an optional MAINTAIN_CONTENTS entry, and a tools/stage-report.sh report covering the model tag verdict, the worklog link, every wiki link written, the worktree and the three branches. Use when analysis is done and code must be written; not for planning or analysis.
--- ---
# implement # implement
@@ -17,16 +17,20 @@ All wiki reads and writes go through `jsc-gitea:wiki`.
3. **A source branch missing from the remote is a stop-and-report condition, never a silent fallback.** That rule (section 「來源分支在遠端找不到」), the remote-only basis and the uncommitted-changes rules: `references/branch.md`. 3. **A source branch missing from the remote is a stop-and-report condition, never a silent fallback.** That rule (section 「來源分支在遠端找不到」), the remote-only basis and the uncommitted-changes rules: `references/branch.md`.
4. Completion condition: the user has confirmed the source branch explicitly, and it is recorded on the analysis page next to the work ticket. 4. Completion condition: the user has confirmed the source branch explicitly, and it is recorded on the analysis page next to the work ticket.
3. **Generate a work ticket**: format `TICKET_{yyyyMMdd}_{HHmmss}_{HASH}`. `{HASH}` = the shared wiki hash for `{owner}/{repo}`, computed by `jsc-gitea/tools/hash-id` (see `jsc-gitea:wiki`). Rename the current session to the ticket name; skip the rename only when the CLI exposes no rename command. Completion condition: the ticket string exists, and you have reported it together with which branch applied — renamed, or skipped because this CLI has no rename command. 3. **Generate a work ticket**: format `TICKET_{yyyyMMdd}_{HHmmss}_{HASH}`. `{HASH}` = the shared wiki hash for `{owner}/{repo}`, computed by `jsc-gitea/tools/hash-id` (see `jsc-gitea:wiki`). Rename the current session to the ticket name; skip the rename only when the CLI exposes no rename command. Completion condition: the ticket string exists, and you have reported it together with which branch applied — renamed, or skipped because this CLI has no rename command.
4. **Settle the previous work package's PR before picking anything — the gate lives in code, not in this text**: 4. **Settle every already-open work-package PR's comments — this keeps existing PRs moving, but does not by itself decide which new package may start (that's step 6)**:
1. Read the analysis page's PR column. For every work package holding a PR that is not marked merged, run `jsc-sdlc/tools/wp-gate.sh check {owner}/{repo} {index} --since {the comment timestamp recorded in that PR column}`. Drop `--since` when that package has no recorded timestamp yet. 1. Read the analysis page's PR column. For every work package holding a PR that is not marked merged, run `jsc-sdlc/tools/wp-gate.sh check {owner}/{repo} {index} --since {the comment timestamp recorded in that PR column}`. Drop `--since` when that package has no recorded timestamp yet.
2. Exit 0 (`status=merged`) clears that package: remove its worktree (`references/branch.md`) and mark the package done on the analysis page. 2. Exit 0 (`status=merged`) clears that package: remove its worktree (`references/branch.md`) and mark the package done on the analysis page.
3. Exit 1 (`status=open` or `status=closed-unmerged`) blocks. Fix every comment the script printed — issue comments, review verdicts and inline comments alike. **Each round of fixes MUST run as a sub agent** inside the original worktree, pushing to the same work branch, so the PR updates itself. Never open a second PR for the same package, and never ask whether to fix or wait: the gate already decided. 3. Exit 1 (`status=open` or `status=closed-unmerged`) means that package's own PR is not settled yet. Fix every comment the script printed — issue comments, review verdicts and inline comments alike. **Each round of fixes MUST run as a sub agent** inside that package's own worktree, pushing to the same work branch, so the PR updates itself. Never open a second PR for the same package, and never ask whether to fix or wait: the gate already decided. **This does not block picking a different, unrelated work package** — SDLC implementation can run several independent packages in parallel; an unmerged PR only holds back packages that depend on it (step 6 checks that specifically), never the whole analysis page.
4. Give every printed comment an outcome — fixed, no fix needed, or cannot fix. Ignore what you cannot fix, plus pure discussion and praise: do not reply on the PR and do not stop the flow, and list each ignored comment with its reason in your final report. 4. Give every printed comment an outcome — fixed, no fix needed, or cannot fix. Ignore what you cannot fix, plus pure discussion and praise: do not reply on the PR and do not stop the flow, and list each ignored comment with its reason in your final report.
5. Write the script's `latest=` value into that work package's PR column, appended after the existing PR link as `#{index} 已處理留言 {ISO time}`, and save the page back to the wiki. Next run passes it as `--since`, so handled comments stay handled. Reuse the existing PR column; the analysis page's columns belong to `analyze`. 5. Write the script's `latest=` value into that work package's PR column, appended after the existing PR link as `#{index} 已處理留言 {ISO time}`, and save the page back to the wiki. Next run passes it as `--since`, so handled comments stay handled. Reuse the existing PR column; the analysis page's columns belong to `analyze`.
6. Exit 3 means the gate could not decide (a missing dependency, or the PR could not be found). Report it and stop — an undecidable gate never counts as merged. 6. Exit 3 means the gate could not decide (a missing dependency, or the PR could not be found). Report it and stop — an undecidable gate never counts as merged.
7. Completion condition: no work package holds an unmerged PR; stop here otherwise. 7. Completion condition: every work package holding an unmerged PR has had this run's comments triaged (fixed, no fix needed, or cannot fix) and reported; a package left unmerged after this does not block step 5/6 for packages that do not depend on it.
5. Read `ANALYZE_CONTENTS` via `jsc-gitea:wiki` and list what is unfinished: plan name, HASH, work package number, count of open items. A selectable work package must satisfy all three: **unfinished, dependency-free (or all dependencies done), and not holding a work ticket**. Completion condition: you have listed every selectable work package, or reported that none is selectable and stopped. 5. Read `ANALYZE_CONTENTS` via `jsc-gitea:wiki` and list what is unfinished: plan name, HASH, work package number, count of open items. A selectable work package must satisfy all three: **unfinished, not holding a work ticket, and — verified by step 6's `wp-gate.sh check-deps`, never by eyeballing the 相依 column yourself — dependency-free or all dependencies merged**. Completion condition: you have listed every selectable work package, or reported that none is selectable and stopped.
6. **Step 4's gate comes first: while `wp-gate.sh check` exits 1, no work package may be picked.** Let the user pick a work package per `jsc-ask:ask` rules (options state open-item count and estimated effort). **List delivery packages (交付 `是`) first** — the analysis page makes `WP-01` the standalone delivery package, so keep that order in the options. Write the ticket into that work package's ticket column (the zh-TW field 「工作證」) on the analysis page and save it back to the wiki. Completion condition: the ticket is saved on the wiki; only then may you proceed. 6. **Before picking, the candidate's own dependencies decide — the gate lives in code, not in this text, and it is scoped to that one candidate, never to every open PR on the page**:
1. For the candidate work package the user is about to choose, run `jsc-sdlc/tools/wp-gate.sh check-deps {owner}/{repo} {wp-number}`. It re-reads the analysis page and queries Gitea itself — it does not trust anything you already read or concluded.
2. Exit 0 (`status=ready`) → eligible; proceed to claim it. Exit 1 (`status=blocked`) → not eligible; report which dependency work package's PR is not merged yet, and offer the user the other eligible candidates instead of this one. Exit 3 (`status=missing-dep`) is an undecidable gate — report it and stop; never treat an undecidable result as either ready or blocked.
3. A dependency phrased as text pointing outside this analysis page (another plan, another analysis) cannot be checked by the script; it comes back listed separately as needing manual confirmation. Confirm it with the user before proceeding — an unchecked cross-page dependency is not the same as a cleared one.
4. Let the user pick per `jsc-ask:ask` rules from the eligible candidates only (options state open-item count and estimated effort). **List delivery packages (交付 `是`) first** — the analysis page makes `WP-01` the standalone delivery package, so keep that order in the options. Write the ticket into that work package's ticket column (the zh-TW field 「工作證」) on the analysis page and save it back to the wiki. Completion condition: `check-deps` returned `status=ready` for the picked candidate (any cross-page dependency confirmed with the user), the ticket is saved on the wiki; only then may you proceed.
7. **A delivery/handover package confirms its content before its first todo**: 7. **A delivery/handover package confirms its content before its first todo**:
1. Ask per `jsc-ask:ask` rules what this delivery must contain. The options are fixed: **1. API 文件** and **2. 由使用者輸入**. State the impact scope on each. **Never assume the type, and never skip this — the answer decides what the whole package produces.** 1. Ask per `jsc-ask:ask` rules what this delivery must contain. The options are fixed: **1. API 文件** and **2. 由使用者輸入**. State the impact scope on each. **Never assume the type, and never skip this — the answer decides what the whole package produces.**
2. Required fields, sample-data order and the new-versus-existing parameter marking: `references/deliver-formats.md`. 2. Required fields, sample-data order and the new-versus-existing parameter marking: `references/deliver-formats.md`.
+16 -1
View File
@@ -30,7 +30,8 @@
`WP-01` 固定是交付、交接工作包,獨立成一包,不與實作合併;用到它規格的實作工作包相依於它。 `WP-01` 固定是交付、交接工作包,獨立成一包,不與實作合併;用到它規格的實作工作包相依於它。
交付型別於實作階段開工時確認(API 文件、由使用者輸入)。 交付型別於實作階段開工時確認(API 文件、由使用者輸入)。
資料來源欄記錄範例資料的出處:`真實:{source}` 或 `推論:無來源`。 資料來源欄記錄範例資料的出處:`真實:{source}` 或 `推論:無來源`。
PR 欄記錄該工作包的 PR 連結與編號;PR 未合併前不得開始下一個工作包。 PR 欄記錄該工作包的 PR 連結與編號;一個工作包的 PR 未合併前,擋的是**相依於它**的工作包,不是整份分析——跟它無關的工作包可以平行進行,不必等它合併。
相依欄只寫本頁的 `WP-NN` 編號,多個用頓號分隔(例:`WP-01、WP-03`);沒有相依填 `-`。`implement` 挑包時靠 `wp-gate.sh check-deps` 活查這一欄逐一核對,寫成別的格式(自然語言、別份計畫的敘述)它查不出來,只會被列成「需人工確認」。跨頁的相依(例如依賴另一份計畫的產出)本來就查不了,允許寫成文字,但盡量也帶出可辨識的來源(頁名、HASH)方便人工核對。
| 編號 | 工作包名稱 | 交付 | 交付型別 | 相依 | 工時(h) | 天數 | 資料來源 | 工作證 | PR | 狀態 | | 編號 | 工作包名稱 | 交付 | 交付型別 | 相依 | 工時(h) | 天數 | 資料來源 | 工作證 | PR | 狀態 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
@@ -41,6 +42,20 @@ PR 欄記錄該工作包的 PR 連結與編號;PR 未合併前不得開始下
- 關鍵路徑:{WP-01 → WP-02 → ⋯⋯},總天數 {d} - 關鍵路徑:{WP-01 → WP-02 → ⋯⋯},總天數 {d}
- 實作候補:{WP-02、WP-03、⋯⋯} - 實作候補:{WP-02、WP-03、⋯⋯}
## 關鍵路徑(CPM)
甘特圖語法固定照 `references/cpm-chart.md`:`dateFormat YYYY-MM-DD` 配任意錨點日期、每個工作包一個 `id`、時長用整數小時、除第一個外全部 `after {上一個id}` 串接。**不得用 `dateFormat X` 或把工時換算成小數天數填進圖裡**,會炸出 `Invalid date`。
```mermaid
gantt
title 關鍵路徑(單位:小時;錨點日期非真實排程,僅供相對呈現)
dateFormat YYYY-MM-DD
axisFormat %m-%d %H:%M
section 關鍵路徑
WP-01 {名稱} :crit, wp01, 2000-01-01, {h}h
WP-02 {名稱} :crit, wp02, after wp01, {h}h
```
## 待辦事項(TDD) ## 待辦事項(TDD)
### WP-01 {交付、交接工作包名稱} ### WP-01 {交付、交接工作包名稱}
+126 -14
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# wp-gate.sh — 工作包 PR 閘門:一個工作包的 PR 沒合併,就不准開下一包(供 jsc-sdlc:implement 呼叫)。 # wp-gate.sh — 工作包 PR 閘門:一個工作包的 PR 沒合併,擋的是相依於它的工作包,不是整份分析
# (供 jsc-sdlc:implement 呼叫;check-deps 判斷某個候選包能不能挑,check/lock 管單一 PR 本身)。
# #
# 為什麼要有這支腳本:這條規則原本只寫在技能內文裡,靠模型自律遵守。內文靠不住——換一個 # 為什麼要有這支腳本:這條規則原本只寫在技能內文裡,靠模型自律遵守。內文靠不住——換一個
# 工作階段、換一個模型,或只是上下文被截掉,規則就跟著消失,而且沒有任何徵兆看得出來。 # 工作階段、換一個模型,或只是上下文被截掉,規則就跟著消失,而且沒有任何徵兆看得出來。
@@ -15,19 +16,26 @@
# --since 只印比該時間更新的留言,值用上一輪印出的 latest=(UTC,形如 2026-08-25T10:19:59Z)。 # --since 只印比該時間更新的留言,值用上一輪印出的 latest=(UTC,形如 2026-08-25T10:19:59Z)。
# wp-gate.sh lock {owner}/{repo} {index} # wp-gate.sh lock {owner}/{repo} {index}
# PR 開好之後上鎖,讓閘門跨工作階段有效。轉呼叫 sdlc-gate.sh wp-lock。 # PR 開好之後上鎖,讓閘門跨工作階段有效。轉呼叫 sdlc-gate.sh wp-lock。
# wp-gate.sh check-deps {owner}/{repo} {wp-number}
# 候選工作包能不能挑:活抓分析頁 WBS 表的相依欄,逐一核對每個相依工作包的狀態欄與
# PR 欄(PR 有給就活查 Gitea 是否已合併),不吃 implement 技能自己讀到的任何快取或判斷。
# 只認同一張分析頁上的 WP-NN 編號;相依欄裡指到別份計畫或別頁分析的文字項目查不了,
# 照樣放行但會在說明裡列出來,要求人工確認。
# #
# 輸出: 第一行固定為 `status=...`(供程式判讀),其後為人類可讀的繁中說明。 # 輸出: 第一行固定為 `status=...`(供程式判讀),其後為人類可讀的繁中說明。
# status=merged 已合併,鎖已解除,可以挑下一個工作包 # status=merged 已合併,鎖已解除,相依於它的工作包現在可以挑了
# status=open PR 還開著,沒有合併 # status=open PR 還開著,沒有合併
# status=closed-unmerged PR 被關掉但沒有合併——這不算完成 # status=closed-unmerged PR 被關掉但沒有合併——這不算完成
# status=locked 已記下這筆未結清的 PR # status=locked 已記下這筆未結清的 PR
# status=ready check-deps 專用:相依的工作包都已結清,可以挑
# status=blocked check-deps 專用:至少一個相依工作包還沒結清,不得挑
# status=usage 用法錯誤 # status=usage 用法錯誤
# status=missing-dep 相依腳本找不到,或這支 PR 查不到 # status=missing-dep 相依腳本找不到、PR 查不到,或分析頁/WBS 列查不到
# check 未合併時,最後一行固定為 `latest={最新一筆留言的時間戳}`(一筆留言都沒有就是 `latest=`), # check 未合併時,最後一行固定為 `latest={最新一筆留言的時間戳}`(一筆留言都沒有就是 `latest=`),
# 供呼叫端寫回分析頁,下一輪拿它當 --since,已處理過的留言就不會再處理一遍。 # 供呼叫端寫回分析頁,下一輪拿它當 --since,已處理過的留言就不會再處理一遍。
# #
# 結束碼: 0=已合併或無阻擋 1=未合併(擋住,呼叫端必須停下來逐筆修留言) # 結束碼: 0=已合併或無阻擋(含 check-deps 的 ready) 1=未合併,或 check-deps 判定 blocked
# 2=用法錯誤 3=相依工具或 PR 查不到 # 2=用法錯誤 3=相依工具或 PR/分析頁查不到
# #
# 陷阱: # 陷阱:
# - 「state=closed 但 merged=false」最容易被當成完成:那是 PR 被關掉、程式碼沒進去, # - 「state=closed 但 merged=false」最容易被當成完成:那是 PR 被關掉、程式碼沒進去,
@@ -39,8 +47,12 @@
# 餵進帶 +08:00 這類偏移的值會比錯,所以只餵上一輪的 latest=。 # 餵進帶 +08:00 這類偏移的值會比錯,所以只餵上一輪的 latest=。
# - latest= 取的是「全部留言」裡最新的一筆,不是過濾後那幾筆。取過濾後的會在沒有新留言時 # - latest= 取的是「全部留言」裡最新的一筆,不是過濾後那幾筆。取過濾後的會在沒有新留言時
# 倒退回舊時間戳,下一輪又把處理過的留言全部翻出來。 # 倒退回舊時間戳,下一輪又把處理過的留言全部翻出來。
# - 已合併時會呼叫 sdlc-gate.sh wp-unlock 解鎖;解鎖失敗照樣回報 merged 並 exit 0,但會多印 # - 已合併時會呼叫 sdlc-gate.sh wp-unlock {owner}/{repo} {index} 解鎖這一包;解鎖失敗照樣
# 一行警示與手動指令——鎖沒清掉,hook 會一直提醒下去。 # 回報 merged 並 exit 0,但會多印一行警示與手動指令——鎖沒清掉,hook 會一直提醒下去。
# - check-deps 只核對「同一張分析頁上的 WP-NN 編號」,不是「相依欄裡的每一個字」:相依欄
# 若寫的是另一份計畫或另一張分析頁的敘述(例如「節點建置」這種跨頁文字依賴),這支腳本
# 沒有能力去查那邊的狀態,只能原樣列出來要求人工確認,不會拿它當擋人的理由——結構上就
# 查不到的東西當成擋人的理由,跟前一條「查不到就擋」矛盾,會讓那個工作包永遠挑不到。
set -u set -u
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
@@ -50,8 +62,9 @@ usage() {
cat >&2 <<'EOF' cat >&2 <<'EOF'
用法: 用法:
wp-gate.sh check {owner}/{repo} {index} [--since {ISO 時間}] 查 PR:合併就解鎖放行,沒合併就印出留言並擋住 wp-gate.sh check {owner}/{repo} {index} [--since {ISO 時間}] 查 PR:合併就解鎖放行,沒合併就印出留言並擋住
wp-gate.sh check-deps {owner}/{repo} {wp-number} 候選工作包能不能挑:核對它在分析頁上的相依工作包是否都已結清
wp-gate.sh lock {owner}/{repo} {index} 開完 PR 後上鎖,讓閘門跨工作階段有效 wp-gate.sh lock {owner}/{repo} {index} 開完 PR 後上鎖,讓閘門跨工作階段有效
結束碼: 0=已合併或無阻擋 1=未合併(擋住) 2=用法錯誤 3=相依工具或 PR 查不到 結束碼: 0=已合併或無阻擋(含 check-deps 的 ready) 1=未合併,或 check-deps 判定 blocked 2=用法錯誤 3=相依工具或 PR/分析頁查不到
EOF EOF
} }
@@ -153,13 +166,14 @@ case "$sub" in
if [ "$merged" = 'true' ]; then if [ "$merged" = 'true' ]; then
echo 'status=merged' echo 'status=merged'
echo "$repo 第 $index 號 PR 已合併,這一包結清了,可以挑下一個工作包。" echo "$repo 第 $index 號 PR 已合併,這一包結清了,相依於它的工作包現在可以挑了。"
gate=$(sdlc_gate_sh) || missing_dep "找不到 jsc-hooks 的 hooks/sdlc-gate.sh,鎖解不掉。並排版面請確認 {workspace}/hooks 存在,或設定 JSC_HOOKS_DIR 指向它的 hooks 目錄。" gate=$(sdlc_gate_sh) || missing_dep "找不到 jsc-hooks 的 hooks/sdlc-gate.sh,鎖解不掉。並排版面請確認 {workspace}/hooks 存在,或設定 JSC_HOOKS_DIR 指向它的 hooks 目錄。"
# stdin 一定要關掉:sdlc-gate.sh 的 hook 模式會讀標準輸入,管線沒人關閉時整支卡死。 # stdin 一定要關掉:sdlc-gate.sh 的 hook 模式會讀標準輸入,管線沒人關閉時整支卡死。
if sh "$gate" wp-unlock "$repo" >/dev/null 2>&1 </dev/null; then # 鎖檔一個工作包一支,解鎖要帶 index,只解掉這一包;其餘平行進行的工作包不受影響。
echo "鎖已解除($repo)。" if sh "$gate" wp-unlock "$repo" "$index" >/dev/null 2>&1 </dev/null; then
echo "鎖已解除($repo 第 $index 號)。"
else else
echo "[jsc][工作包閘門][WARN]:鎖解不掉($repo)。PR 確實已合併,但狀態檔還在,hook 會一直提醒。請手動執行:sdlc-gate.sh wp-unlock $repo" >&2 echo "[jsc][工作包閘門][WARN]:鎖解不掉($repo 第 $index 號)。PR 確實已合併,但狀態檔還在,hook 會一直提醒。請手動執行:sdlc-gate.sh wp-unlock $repo $index" >&2
fi fi
exit 0 exit 0
fi fi
@@ -167,11 +181,12 @@ case "$sub" in
if [ "$state" = 'closed' ]; then if [ "$state" = 'closed' ]; then
echo 'status=closed-unmerged' echo 'status=closed-unmerged'
echo "$repo 第 $index 號 PR 被關掉了,但沒有合併——程式碼沒進到來源分支,這不算完成。" echo "$repo 第 $index 號 PR 被關掉了,但沒有合併——程式碼沒進到來源分支,這不算完成。"
echo '要嘛重開這支 PR 並把留言修完,要嘛請使用者裁決;在那之前不得挑下一個工作包。' echo '要嘛重開這支 PR 並把留言修完,要嘛請使用者裁決;在那之前這一包不算結清。'
else else
echo 'status=open' echo 'status=open'
echo "$repo 第 $index 號 PR 還開著,沒有合併。這一包還沒結清,不得挑下一個工作包。" echo "$repo 第 $index 號 PR 還開著,沒有合併。這一包還沒結清。"
fi fi
echo '這一包沒結清,只表示相依於它的工作包不能開始;其餘互不相依的工作包能不能挑,見 wp-gate.sh check-deps。'
echo '下列每一筆留言都要有結果(已修、不需修、修不動)。修不動或純討論、讚美的留言直接忽略:' echo '下列每一筆留言都要有結果(已修、不需修、修不動)。修不動或純討論、讚美的留言直接忽略:'
echo '不在 PR 上回覆、也不因此停下流程,但要在最終回報裡逐筆列出被忽略的留言與理由。' echo '不在 PR 上回覆、也不因此停下流程,但要在最終回報裡逐筆列出被忽略的留言與理由。'
if [ -n "$since" ]; then if [ -n "$since" ]; then
@@ -196,6 +211,103 @@ case "$sub" in
rm -f "$tmp" rm -f "$tmp"
exit 1 ;; exit 1 ;;
check-deps)
repo="${1:-}"; wp_number="${2:-}"
valid_repo "$repo" || { echo 'status=usage'; usage; exit 2; }
valid_index "$wp_number" || { echo 'status=usage'; usage; exit 2; }
[ "$#" -le 2 ] || { echo 'status=usage'; usage; exit 2; }
gitea=$(gitea_sh) || missing_dep "找不到 jsc-gitea 的 tools/gitea.sh。並排版面請確認 {workspace}/gitea 存在,已安裝版面請確認 jsc-gitea plugin 已安裝,或設定 JSC_GITEA_TOOLS 指向它的 tools 目錄。"
hash=$(sh "$gitea" hash-id "$repo" 2>/dev/null)
[ -n "$hash" ] || missing_dep "算不出 $repo 的 wiki HASH。"
wiki_repo=$(sh "$gitea" wiki-repo ANALYZE 2>/dev/null)
[ -n "$wiki_repo" ] || missing_dep "解析不出 ANALYZE 頁的 wiki 存取庫(JSC_WIKI_REPO_ANALYZE 與 JSC_WIKI_REPO 都未設定)。"
page="ANALYZE_${hash}"
content=$(sh "$gitea" wiki-get "$wiki_repo" "$page" 2>/dev/null)
[ -n "$content" ] || missing_dep "讀不到分析頁 $wiki_repo 的 $page。"
# WBS 表欄位(以 | 分隔,$1 是第一個 | 之前的空字串):
# $2=編號 $6=相依 $11=PR $12=狀態。
# 找列一律比數值,不比字串:範本固定兩位數補零(WP-08),但呼叫端或相依欄裡的寫法
# 不一定補零(WP-8);比字串會讓兩種寫法各自找不到對方那一列,誤判成「查不到」。
find_wp_row() { # $1=WP 編號(可帶或不帶前導零) -> 印出該列,找不到印空字串
_n=$(printf '%s' "$1" | sed 's/^0*//'); [ -n "$_n" ] || _n=0
printf '%s\n' "$content" | awk -F'|' -v n="$_n" '
{ w = $2; gsub(/^[ \t]+|[ \t]+$/, "", w)
if (w ~ /^WP-[0-9]+$/) {
wn = w; sub(/^WP-0*/, "", wn); if (wn == "") wn = "0"
if (wn + 0 == n + 0) { print; exit }
}
}'
}
row=$(find_wp_row "$wp_number")
[ -n "$row" ] || missing_dep "分析頁 $page 上找不到 WP-$wp_number 這一列,核對不了相依。"
self_wp=$(printf '%s' "$row" | awk -F'|' '{ w = $2; gsub(/^[ \t]+|[ \t]+$/, "", w); print w }')
deps=$(printf '%s' "$row" | awk -F'|' '{ d = $6; gsub(/^[ \t]+|[ \t]+$/, "", d); print d }')
case "$deps" in
''|-|無)
echo 'status=ready'
echo "$self_wp 沒有相依,可以挑。"
exit 0 ;;
esac
unmet=''
unknown=''
# 用 sed 把分隔符號換成換行、靠 IFS 只切換行:頓號是多位元組字元,tr 逐位元組處理
# 這種字元集合會把緊鄰的中文字元切壞(例如「節點建置」被腰斬成亂碼)。
_old_ifs=$IFS
IFS='
'
for dep in $(printf '%s\n' "$deps" | sed 's/、/\
/g; s/,/\
/g; s/,/\
/g'); do
IFS=$_old_ifs
dep_idx=$(printf '%s' "$dep" | sed -n 's/.*WP-\([0-9][0-9]*\).*/\1/p')
if [ -z "$dep_idx" ]; then
# 不是本頁的 WP-NN 編號(例如指到別份計畫、別張分析頁的文字敘述):查不了,
# 但不能因為查不了就一律擋住——那會讓這種工作包永遠挑不到。列出來要求人工確認。
[ -n "$dep" ] && unknown="${unknown}${unknown:+、}$dep"
continue
fi
dep_row=$(find_wp_row "$dep_idx")
if [ -z "$dep_row" ]; then
unmet="${unmet}${unmet:+、}WP-$dep_idx(分析頁上查不到這一列)"
continue
fi
dep_wp=$(printf '%s' "$dep_row" | awk -F'|' '{ w = $2; gsub(/^[ \t]+|[ \t]+$/, "", w); print w }')
dep_status=$(printf '%s' "$dep_row" | awk -F'|' '{ s = $12; gsub(/^[ \t]+|[ \t]+$/, "", s); print s }')
if [ "$dep_status" = '已完成' ]; then
continue
fi
dep_pr=$(printf '%s' "$dep_row" | awk -F'|' '{ print $11 }')
pr_index=$(printf '%s' "$dep_pr" | sed -n 's#.*/pulls/\([0-9][0-9]*\).*#\1#p' | head -n1)
if [ -z "$pr_index" ]; then
unmet="${unmet}${unmet:+、}${dep_wp}(狀態「${dep_status:-未完成}」,還沒開 PR)"
continue
fi
st=$(sh "$gitea" pr-status "$repo" "$pr_index" 2>/dev/null) || st=''
merged=$(printf '%s' "$st" | awk '{print $2}')
if [ "$merged" != 'true' ]; then
unmet="${unmet}${unmet:+、}${dep_wp}(第 $pr_index 號 PR 未合併)"
fi
done
if [ -n "$unmet" ]; then
echo 'status=blocked'
echo "$self_wp 相依 $unmet 尚未結清,不得挑選。"
[ -n "$unknown" ] && echo "另有查不了的相依項目(非本頁工作包編號,需人工確認):$unknown"
exit 1
fi
echo 'status=ready'
echo "$self_wp 在本頁上的相依工作包全部已結清,可以挑。"
[ -n "$unknown" ] && echo "有查不了的相依項目(非本頁工作包編號,需人工確認是否已完成):$unknown"
exit 0 ;;
lock) lock)
repo="${1:-}"; index="${2:-}" repo="${1:-}"; index="${2:-}"
valid_repo "$repo" || { echo 'status=usage'; usage; exit 2; } valid_repo "$repo" || { echo 'status=usage'; usage; exit 2; }