feat/comments-merge/main #52

Merged
admin merged 10 commits from feat/comments-merge/main into master 2026-09-17 09:22:58 +00:00
Member

摘要

本 PR 取代 #49。#49 的 head 分支在解衝突時被重建,Gitea 因此自動關閉了它。
內容相同,另外含把本工作包的修正套到 #48 的 pr-threads 共用結構上的那一段。

/sdlc-sync:把議題留言裡的決策整併回描述,新加入的人不必爬完整串留言就知道現況。
六份流程正本到此全部到齊。

需求議題

#1 — tea-sdlc:以 tea 驅動 SDLC 全流程的跨平台指令組

工作包議題

#15 — 以 sdlc-sync 把留言裡的決策整併回議題描述

變更內容

commit 內容
refactor(lib) / test(pr-create) 讀檔與列留言收進 lib,並讓已整併只認自己打的 +1
fix(pr-comments) / test(pr-comments) 議題也讀得了,不再先打 PR 端點
feat(議題解析) 換掉一個段落的內容,標題與其餘段落一字不動
feat(comments-merge) / test(comments-merge) 把留言裡的決策整併回議題描述並標記
feat(流程正本) / test(流程正本) 新增 sdlc-sync,並讓另外兩份正本整併完自動接回

新增 scripts/comments-merge.js、prompts/sdlc-sync.md;issue-body.js 增加
replaceSection;lib.js 增加 readTextFile/listIssueComments/mergedByMe。

設計重點

  • 局部更新。 只換談好的那一段,標題與其餘段落一字不動。實測:在議題 #1 那份 212 行的
    描述上換掉一段,另外六段逐字未變。
  • 判斷留在人身上。 哪幾則留言是決策、該併進哪一段、併成什麼樣子,都由讀得懂內容的人
    決定並先給使用者點頭;腳本只負責安全地寫回去。
  • 先寫描述再標記。 反過來的話,描述寫失敗時那幾則已經被標成處理過,再也不會被提出來。
  • replaceSection 照同檔既有的規矩做:圍欄裡的假標題不算段落;同名標題出現不只一次時
    交回 ambiguous 而不賭第一個(蓋掉的是一整段,猜錯的代價比 tickLine 更高)。

解決的問題

Code review 抓出四類缺陷。第一個是我自己寫進正本的錯誤斷言:

1. /sdlc-sync 對它唯一的輸入完全跑不起來。 我在正本寫「pr-comments 對議題一樣管用
(Gitea 的 PR 也是 issue)」——這句話本身就反了:每個 PR 都是議題,議題不一定是 PR。
程式也照著反的寫,先打 /pulls/{index}:

$ node scripts/pr-comments.js --repo plugins/tea-sdlc --index 15
{"ok":false,"error":{"code":"PULL_NOT_FOUND","message":"plugins/tea-sdlc 沒有編號 15 的 PR"}}

而 /sdlc-sync 的輸入正是純議題,於是在讀到第一則留言之前就斷了。改成先讀
/issues/{index}(兩種都有),看它有沒有 pull_request 才決定要不要翻 review。

2. 跨工作包的「已整併」定義不一致,後果很安靜。 countUnmergedComments(#9)接受
任何人的 +1,而 pr-comments 的判斷(#14)只認自己的。隊友對一則決策留言按個讚 →
未處理留言數 掉到 0 → analyze/feat 再也不提示 → 那則決策永遠不會被收進描述。
統一成只認自己打的:別人按讚是「我同意」,不是「已經收進去了」。

3. replaceSection 的三個邊界(都實測過):CRLF 的 body 會被混進 LF,讓抽取契約交出的
raw 對不上原文、之後就勾不動那幾行;同名標題靜靜蓋掉第一個;空段落遺失與下一個標題之間
的空行。

4. 四份重複的「讀一個 --xxx-file 或直接失敗」,錯誤碼還有三種拼法
(BODY_FILE_NOT_FOUND/BODY_FILE_MISSING/CONTENT_FILE_NOT_FOUND)。同一種情況要有
同一個碼,呼叫端才分辨得出是哪一步壞了。連同第三份留言分頁一起收進 lib.js。

影響的功能

  • issue-extract/wp-extract 的 未處理留言數 語意收緊為「自己還沒標記過的則數」。
    這是修正,不是行為變更的副作用——舊語意會讓決策被靜靜跳過。
  • pr-create 改用 lib.js 的 readTextFile,錯誤碼由 BODY_FILE_NOT_FOUND 變成
    FILE_NOT_FOUND/FILE_EMPTY。
  • pr-comments 的輸出新增 類型(PR/議題),錯誤碼由 PULL_NOT_FOUND 變成
    ISSUE_NOT_FOUND。
  • sdlc-analyze/sdlc-feat 偵測到未整併留言時,改為直接轉進 /sdlc-sync 並自動接回,
    不再要求使用者重打指令。

驗收標準本身的一個矛盾,請裁示

這兩條放在一起會互相牴觸:

使用者選擇略過的留言保持未標記,下次執行仍會被提出
sdlc-analyze 與 sdlc-feat 開始時偵測到未整併留言即提示先處理

結果是略過一次 = 以後每次 analyze/feat 都會被打斷,而 sync 每次重新提出同一則。
契約裡只有「+1 = 已處理」一種狀態,沒有「看過但決定不併」。

本 PR 依驗收實作(略過就是不標記),並在正本的回報段落要求講明白「這不是沒整併乾淨,
是你選了略過」。真正的解法需要第二種狀態(例如另一種 reaction 表示「看過不併」),
那超出 #15 的驗收範圍。

另外兩件累積中的契約落差

  • pr-reply(#14 交付)不在議題 #1 的腳本清單裡。
  • 議題 #1 的 story 38 說「PR 七段描述」,但列出八個名稱;#13 依列表實作八段。

三件都是議題 #1 的文字,我沒有自行更動契約正本。要我一次改掉的話說一聲。

測試結果

在最新 master(86bdfd90)之上實際執行 npm test:

ℹ tests 735
ℹ suites 0
ℹ pass 735
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0

master 既有 691,本分支新增 44。

局部更新的六種情況(一般段落、最後一段、空段落、CRLF、重複標題、圍欄裡的假標題)各有
覆蓋;標記只給 --merged 列出的那幾則、描述寫失敗就不標記、冪等重跑、--dry-run 在無
變更時不預告 PATCH 也都釘住。

實機驗證:拿議題 #1 那份 212 行的真實描述換掉一段,段落數與順序不變,只有目標那一段的
內容變了
,其餘六段逐字相同;pr-comments 對純議題 #15 的四個端點回應也逐一確認過
(/pulls/15 是 404,/issues/15/comments 與 /issues/15/timeline 是 200)。

🤖 Generated with Claude Code

## 摘要 > 本 PR 取代 #49。#49 的 head 分支在解衝突時被重建,Gitea 因此自動關閉了它。 > 內容相同,另外含把本工作包的修正套到 #48 的 `pr-threads` 共用結構上的那一段。 `/sdlc-sync`:把議題留言裡的決策整併回描述,新加入的人不必爬完整串留言就知道現況。 六份流程正本到此全部到齊。 ## 需求議題 #1 — tea-sdlc:以 tea 驅動 SDLC 全流程的跨平台指令組 ## 工作包議題 #15 — 以 sdlc-sync 把留言裡的決策整併回議題描述 ## 變更內容 | commit | 內容 | | --- | --- | | `refactor(lib)` / `test(pr-create)` | 讀檔與列留言收進 lib,並讓已整併只認自己打的 `+1` | | `fix(pr-comments)` / `test(pr-comments)` | 議題也讀得了,不再先打 PR 端點 | | `feat(議題解析)` | 換掉一個段落的內容,標題與其餘段落一字不動 | | `feat(comments-merge)` / `test(comments-merge)` | 把留言裡的決策整併回議題描述並標記 | | `feat(流程正本)` / `test(流程正本)` | 新增 `sdlc-sync`,並讓另外兩份正本整併完自動接回 | 新增 `scripts/comments-merge.js`、`prompts/sdlc-sync.md`;`issue-body.js` 增加 `replaceSection`;`lib.js` 增加 `readTextFile`/`listIssueComments`/`mergedByMe`。 ## 設計重點 - **局部更新。** 只換談好的那一段,標題與其餘段落一字不動。實測:在議題 #1 那份 212 行的 描述上換掉一段,另外六段逐字未變。 - **判斷留在人身上。** 哪幾則留言是決策、該併進哪一段、併成什麼樣子,都由讀得懂內容的人 決定並先給使用者點頭;腳本只負責安全地寫回去。 - **先寫描述再標記。** 反過來的話,描述寫失敗時那幾則已經被標成處理過,再也不會被提出來。 - **`replaceSection` 照同檔既有的規矩做**:圍欄裡的假標題不算段落;同名標題出現不只一次時 交回 `ambiguous` 而不賭第一個(蓋掉的是一整段,猜錯的代價比 `tickLine` 更高)。 ## 解決的問題 Code review 抓出四類缺陷。第一個是我自己寫進正本的錯誤斷言: **1. `/sdlc-sync` 對它唯一的輸入完全跑不起來。** 我在正本寫「`pr-comments` 對議題一樣管用 (Gitea 的 PR 也是 issue)」——這句話本身就反了:**每個 PR 都是議題,議題不一定是 PR**。 程式也照著反的寫,先打 `/pulls/{index}`: ``` $ node scripts/pr-comments.js --repo plugins/tea-sdlc --index 15 {"ok":false,"error":{"code":"PULL_NOT_FOUND","message":"plugins/tea-sdlc 沒有編號 15 的 PR"}} ``` 而 `/sdlc-sync` 的輸入正是純議題,於是在讀到第一則留言之前就斷了。改成先讀 `/issues/{index}`(兩種都有),看它有沒有 `pull_request` 才決定要不要翻 review。 **2. 跨工作包的「已整併」定義不一致,後果很安靜。** `countUnmergedComments`(#9)接受 **任何人**的 `+1`,而 `pr-comments` 的判斷(#14)只認自己的。隊友對一則決策留言按個讚 → `未處理留言數` 掉到 0 → `analyze`/`feat` 再也不提示 → 那則決策永遠不會被收進描述。 統一成只認自己打的:別人按讚是「我同意」,不是「已經收進去了」。 **3. `replaceSection` 的三個邊界**(都實測過):CRLF 的 body 會被混進 LF,讓抽取契約交出的 `raw` 對不上原文、之後就勾不動那幾行;同名標題靜靜蓋掉第一個;空段落遺失與下一個標題之間 的空行。 **4. 四份重複的「讀一個 `--xxx-file` 或直接失敗」**,錯誤碼還有三種拼法 (`BODY_FILE_NOT_FOUND`/`BODY_FILE_MISSING`/`CONTENT_FILE_NOT_FOUND`)。同一種情況要有 同一個碼,呼叫端才分辨得出是哪一步壞了。連同第三份留言分頁一起收進 `lib.js`。 ## 影響的功能 - `issue-extract`/`wp-extract` 的 `未處理留言數` 語意收緊為「自己還沒標記過的則數」。 這是修正,不是行為變更的副作用——舊語意會讓決策被靜靜跳過。 - `pr-create` 改用 `lib.js` 的 `readTextFile`,錯誤碼由 `BODY_FILE_NOT_FOUND` 變成 `FILE_NOT_FOUND`/`FILE_EMPTY`。 - `pr-comments` 的輸出新增 `類型`(`PR`/`議題`),錯誤碼由 `PULL_NOT_FOUND` 變成 `ISSUE_NOT_FOUND`。 - `sdlc-analyze`/`sdlc-feat` 偵測到未整併留言時,改為直接轉進 `/sdlc-sync` 並自動接回, 不再要求使用者重打指令。 ### 驗收標準本身的一個矛盾,請裁示 這兩條放在一起會互相牴觸: > 使用者選擇略過的留言保持未標記,下次執行仍會被提出 > sdlc-analyze 與 sdlc-feat 開始時偵測到未整併留言即提示先處理 結果是**略過一次 = 以後每次 `analyze`/`feat` 都會被打斷**,而 `sync` 每次重新提出同一則。 契約裡只有「`+1` = 已處理」一種狀態,沒有「看過但決定不併」。 本 PR 依驗收實作(略過就是不標記),並在正本的回報段落要求講明白「這不是沒整併乾淨, 是你選了略過」。真正的解法需要第二種狀態(例如另一種 reaction 表示「看過不併」), 那超出 #15 的驗收範圍。 ### 另外兩件累積中的契約落差 - `pr-reply`(#14 交付)不在議題 #1 的腳本清單裡。 - 議題 #1 的 story 38 說「PR 七段描述」,但列出八個名稱;#13 依列表實作八段。 三件都是議題 #1 的文字,我沒有自行更動契約正本。要我一次改掉的話說一聲。 ## 測試結果 在最新 master(`86bdfd90`)之上實際執行 `npm test`: ``` ℹ tests 735 ℹ suites 0 ℹ pass 735 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ``` master 既有 691,本分支新增 44。 局部更新的六種情況(一般段落、最後一段、空段落、CRLF、重複標題、圍欄裡的假標題)各有 覆蓋;標記只給 `--merged` 列出的那幾則、描述寫失敗就不標記、冪等重跑、`--dry-run` 在無 變更時不預告 PATCH 也都釘住。 實機驗證:拿議題 #1 那份 212 行的真實描述換掉一段,段落數與順序不變,**只有目標那一段的 內容變了**,其餘六段逐字相同;`pr-comments` 對純議題 #15 的四個端點回應也逐一確認過 (`/pulls/15` 是 404,`/issues/15/comments` 與 `/issues/15/timeline` 是 200)。 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jiantw83 added 10 commits 2026-09-17 09:21:38 +00:00
「讀一個 --xxx-file 或直接失敗」原本在四支腳本各寫一份,錯誤碼還有三種拼法
(BODY_FILE_NOT_FOUND/BODY_FILE_MISSING/CONTENT_FILE_NOT_FOUND)。同一種情況要有同一個
碼,呼叫端才分辨得出是哪一步壞了。留言分頁的那段咒語也是第三份,一併收成 listIssueComments。

countUnmergedComments 原本接受任何人的 +1,而讀留言那邊只認自己的——兩端對「已整併」的
定義不一致。後果是隊友對決策留言按個讚,未處理留言數就掉到 0,analyze 與 feat 再也不提示,
那則決策永遠不會被收進描述。統一成只認自己打的:別人按讚是「我同意」,不是「已經收進去了」。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
每個 PR 都是議題,議題不一定是 PR——原本的註解把這句話講反了,程式也照著反過來寫:
先打 /pulls/{index},對純議題回 404,於是 /sdlc-sync 在讀到第一則留言之前就斷了。

改成先讀 /issues/{index}(兩種都有),看它有沒有 pull_request 才決定要不要去翻 review。
輸出加上「類型」讓下游知道拿到的是議題還是 PR。

讀取的共用結構沿用 pr-threads(#48 為了讓 pr-watch 數同一件事而抽出來的):readGeneral
改名 readGeneralComments 並導出,純議題只叫它;pr-threads 自己那份 markedByMe 拿掉,
改用 lib 的 mergedByMe。#48 的檔頭擔心「規則寫兩份遲早會各自演化」,現在三處共用一份。

試跑改印 commonRequests:pr-comments 收得下兩種輸入,而試跑階段還沒讀過議題、不知道是
哪一種。與其假設是 PR 而列出五個(對純議題有三個根本不會發),不如只列一定會發的。
pr-watch 的輸入一定是 PR,繼續用 plannedRequests。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
整併留言裡的決策時用它。不整份重寫的理由跟 upsertLineInSection 一樣,只是代價更大:
重寫會把別人在其他段落的編輯一起蓋掉,而議題的編輯紀錄沒有人會去比對。

三件事照著同檔既有的規矩做:圍欄裡的假標題不算段落;同名標題出現不只一次時交回 ambiguous
而不賭第一個(蓋掉的是一整段,猜錯的代價比 tickLine 更高);換行沿用 body 原本的那一種,
CRLF 的 body 裡混進 LF 會讓抽取契約交出的 raw 對不上原文,之後就勾不動那幾行。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
判斷「哪幾則有決策、該併進哪一段」是讀得懂內容的人的事;這一支只負責把結果安全地寫回去。

先寫描述再標記,描述寫失敗就不標記:反過來的話,那幾則已經被標成處理過,再也不會被提出來。
--merged 只收真的併進去的那幾則,略過的保持未標記。標記之前先核對那幾則確實在這顆議題上——
打錯 id 的 reaction 會落在別顆議題的留言上,而那幾乎不會有人發現。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
挑出哪些留言真的是決策、寫回去之前讓使用者點頭、略過的保持未標記——三件都只有正本做得到。

analyze 與 feat 原本寫「建議先執行 /sdlc-sync,再回來」,那等於要使用者重打指令。改成直接走
sync 的流程、做完自動接回,並在接回前重新抽取一次——接著用舊的那一份做事,這一整段就白做了。

略過的留言會讓未處理留言數停在大於 0,於是 analyze 與 feat 每次都會再停一次。這是驗收標準
本身的兩條放在一起的結果,正本把它講明白,讓使用者分得出「沒整併乾淨」與「我選了略過」。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
admin approved these changes 2026-09-17 09:22:55 +00:00
admin merged commit 643293b21c into master 2026-09-17 09:22:58 +00:00
admin deleted branch feat/comments-merge/main 2026-09-17 09:22:58 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: plugins/tea-sdlc#52