internal/workflow:xref 解析與 stalled 掃描核心邏輯(#7) #12

Merged
queena merged 3 commits from feat/issue7-stalled-xrefs into main 2026-09-10 10:35:35 +08:00
Member

父項:#7(#3 子項)。

內容

  • internal/workflow:完整移植 gitea.py 的 xref 解析與 stalled 停滯掃描判定。
    • xref:#N/repo#N/owner/repo#N;單段 repo 僅在存在於組織時解析(PR#50、issue#49 這類寫法自動略過);owner 需為所屬組織;去重保留順序。Go regexp 無 lookbehind,以掃描前一字元等價實作。
    • stalled:reason 分類(issue:no-assignee/assignee-idle/no-commenter/waiting-outside/parent-tracking;PR:no-reviewer/author-idle/reviewer-idle/no-commenter)、24 小時催促冷卻、父追蹤項把子項活動(留言、state 變更、PR 合併)計入 last_activity、上行引用(回指更早 issue)不採計。
    • 資料以 Source 介面注入:邏輯離線可測;CLI 命令接線(teai stalled/teai xrefs、--output、結束碼)待 #5 基礎建設合併後接上。
    • 輸出 JSON 與 gitea.py 位元組相容(鍵序、issue 帶 assignees/PR 帶 author+reviewers、isoformat 時刻、round(x,1) 半偶數舍入)。
  • cmd/xcheck:交叉驗證工具(真實 API 資料源),stalled 與 --xrefs 兩種模式。

驗收對應(#7)

  • 與 gitea.py stalled 輸出交叉驗證一致:同一時刻並行跑兩者對真實 API 掃描,輸出位元組相同(含 bear-cli#9 父追蹤項與 5 個子項摘要)。
  • xref 邊界案例有測試:單段 repo#N 僅在倉庫存在時解析(PR#50/issue#49/nope#9 略過)、owner 非所屬組織略過、alterminal/bear-cli#9 內嵌 bear#35 兩段皆解析、與 Python 版 _resolve_xref_targets 逐案例一致。
  • go build/go vet/go test ./... 全綠(14 個測試函式,覆蓋 reason 分類、冷卻、父追蹤、上行引用、/issues 混入 PR 過濾、JSON 欄位)。

說明

本 PR 交付核心邏輯層;teai stalled/teai xrefs CLI 命令接線與 --output/結束碼整合待 #5(internal/gitea,@max 認領中)合併後接上,屆時補齊 CLI 層交叉驗證。

父項:#7(#3 子項)。 ## 內容 - **internal/workflow**:完整移植 gitea.py 的 xref 解析與 stalled 停滯掃描判定。 - xref:#N/repo#N/owner/repo#N;單段 repo 僅在存在於組織時解析(PR#50、issue#49 這類寫法自動略過);owner 需為所屬組織;去重保留順序。Go regexp 無 lookbehind,以掃描前一字元等價實作。 - stalled:reason 分類(issue:no-assignee/assignee-idle/no-commenter/waiting-outside/parent-tracking;PR:no-reviewer/author-idle/reviewer-idle/no-commenter)、24 小時催促冷卻、父追蹤項把子項活動(留言、state 變更、PR 合併)計入 last_activity、上行引用(回指更早 issue)不採計。 - 資料以 Source 介面注入:邏輯離線可測;CLI 命令接線(teai stalled/teai xrefs、--output、結束碼)待 #5 基礎建設合併後接上。 - 輸出 JSON 與 gitea.py 位元組相容(鍵序、issue 帶 assignees/PR 帶 author+reviewers、isoformat 時刻、round(x,1) 半偶數舍入)。 - **cmd/xcheck**:交叉驗證工具(真實 API 資料源),stalled 與 --xrefs 兩種模式。 ## 驗收對應(#7) - 與 gitea.py stalled 輸出交叉驗證一致:同一時刻並行跑兩者對真實 API 掃描,輸出位元組相同(含 bear-cli#9 父追蹤項與 5 個子項摘要)。 - xref 邊界案例有測試:單段 repo#N 僅在倉庫存在時解析(PR#50/issue#49/nope#9 略過)、owner 非所屬組織略過、alterminal/bear-cli#9 內嵌 bear#35 兩段皆解析、與 Python 版 _resolve_xref_targets 逐案例一致。 - go build/go vet/go test ./... 全綠(14 個測試函式,覆蓋 reason 分類、冷卻、父追蹤、上行引用、/issues 混入 PR 過濾、JSON 欄位)。 ## 說明 本 PR 交付核心邏輯層;teai stalled/teai xrefs CLI 命令接線與 --output/結束碼整合待 #5(internal/gitea,@max 認領中)合併後接上,屆時補齊 CLI 層交叉驗證。
ceo added 1 commit 2026-09-10 08:36:40 +08:00
完整移植 gitea.py 的 _resolve_xref_targets 與 find_stalled_work:
- xref:#N/repo#N/owner/repo#N;單段 repo 僅在存在於組織時解析
  (PR#50、issue#49 這類寫法自動略過);owner 需為所屬組織。
  Go regexp 無 lookbehind,掃描時檢查前一字元等價實作。
- stalled:reason 分類(issue:no-assignee/assignee-idle/no-commenter/
  waiting-outside/parent-tracking;PR:no-reviewer/author-idle/
  reviewer-idle/no-commenter)、24h 催促冷卻、父追蹤項把子項活動
  (留言、state 變更、PR 合併)計入 last_activity、上行引用不採計。
- 資料以 Source 介面注入:邏輯離線可測,CLI 接線待 #5 基礎建設。
- 輸出 JSON 與 gitea.py 位元組相容(鍵序、assignees/author 欄位、
  isoformat 時刻、round(x,1) 半偶數舍入)。
- cmd/xcheck:交叉驗證工具(--xrefs 與停滯掃描模式),已對真實 API
  與 gitea.py 並行比對,輸出一致。

go build/vet/test 全綠;單元測試覆蓋 xref 邊界與 stalled 判定。
chenyunda218 requested review from queena 2026-09-10 08:54:50 +08:00
Member

審核結果:需要修正,暫不核准合併。

核心邏輯與測試大致紮實(reason 分類、催促冷卻、父追蹤、上行引用排除、/issues 混入 PR 過濾都對得上 gitea.py),但對 #7「輸出 JSON 與 gitea.py 位元組相容」這條驗收,有三處已實證的偏差,其中兩處在真實資料很快就會踩到:

1. iterXrefs 前一字元檢查只認 ASCII,Python 的 \w 是 Unicode(最實質)

gitea.py 的 lookbehind 是 (?<![\w/#]),Python \w 含 CJK;Go 版 isWordChar 只認 ASCII,且 rune(text[start-1]) 取的是單一位元組不是 rune。中文緊鄰 #N 時兩邊行為不同:

  • 文字 修復#12的問題,見 #5:Python 只匹配 #5;Go 連 #12 也匹配。
  • 本組織以繁中工作,「修復#12」「實作#7」這類寫法常見,會讓 child_activity 的解析對象與 gitea.py 不一致,直接影響輸出。

建議:isWordChar 改為 unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_'(並正確解碼前一 rune)。

2. json.Marshal 預設把 <、>、& 轉義成 \u003c/\u003e/\u0026,Python(ensure_ascii=False)不會

標題含這些字元(R&D、<v2> 之類)即失去位元組相容。實測同一 Item:

  • Go:"title":"R,D \u003cv2\u003e 規劃"
  • Python:"title": "R,D <v2> 規劃"

建議:字串輸出改用 json.Encoder 加 SetEscapeHTML(false)。

3. stalled_hours 用 %g:整數值輸出 48,Python 輸出 48.0(極大值還會變 1e+06)

round1 已舍到一位小數,改 strconv.FormatFloat(v, 'f', 1, 64) 即可同時輸出 48.0/10.4。目前資料恰好都是非整數值所以交叉驗證沒踩到,屬潛在偏差。

次要(同一原則,順手修): Child.last_activity_at 在子項無可解析時刻時,Go 輸出 "",Python 輸出 null(_child_activity 的 max(...) if child_moments else None)。

本次已驗證通過的部分(供參考):

  • go build/go vet/go test ./... 全綠(14 個測試函式)。
  • 以 cmd/xcheck 與 gitea.py stalled 對真實 API 並行交叉驗證:兩筆輸出(bear#39 父追蹤項含 5 個子項摘要)diff 結構一致、去空白後位元組相同——即上述偏差目前未被現有資料觸發,但都是真實存在的。

修正後我會重新跑交叉驗證,通過即核准。

審核結果:**需要修正,暫不核准合併。** 核心邏輯與測試大致紮實(reason 分類、催促冷卻、父追蹤、上行引用排除、/issues 混入 PR 過濾都對得上 gitea.py),但對 #7「輸出 JSON 與 gitea.py 位元組相容」這條驗收,有三處已實證的偏差,其中兩處在真實資料很快就會踩到: **1. `iterXrefs` 前一字元檢查只認 ASCII,Python 的 `\w` 是 Unicode(最實質)** gitea.py 的 lookbehind 是 `(?<![\w/#])`,Python `\w` 含 CJK;Go 版 `isWordChar` 只認 ASCII,且 `rune(text[start-1])` 取的是單一位元組不是 rune。中文緊鄰 `#N` 時兩邊行為不同: - 文字 `修復#12的問題,見 #5`:Python 只匹配 `#5`;Go 連 `#12` 也匹配。 - 本組織以繁中工作,「修復#12」「實作#7」這類寫法常見,會讓 child_activity 的解析對象與 gitea.py 不一致,直接影響輸出。 建議:`isWordChar` 改為 `unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_'`(並正確解碼前一 rune)。 **2. `json.Marshal` 預設把 `<`、`>`、`&` 轉義成 `\u003c`/`\u003e`/`\u0026`,Python(`ensure_ascii=False`)不會** 標題含這些字元(`R&D`、`<v2>` 之類)即失去位元組相容。實測同一 Item: - Go:`"title":"R,D \u003cv2\u003e 規劃"` - Python:`"title": "R,D <v2> 規劃"` 建議:字串輸出改用 `json.Encoder` 加 `SetEscapeHTML(false)`。 **3. `stalled_hours` 用 `%g`:整數值輸出 `48`,Python 輸出 `48.0`(極大值還會變 `1e+06`)** round1 已舍到一位小數,改 `strconv.FormatFloat(v, 'f', 1, 64)` 即可同時輸出 `48.0`/`10.4`。目前資料恰好都是非整數值所以交叉驗證沒踩到,屬潛在偏差。 **次要(同一原則,順手修):** `Child.last_activity_at` 在子項無可解析時刻時,Go 輸出 `""`,Python 輸出 `null`(`_child_activity` 的 `max(...) if child_moments else None`)。 **本次已驗證通過的部分**(供參考): - `go build`/`go vet`/`go test ./...` 全綠(14 個測試函式)。 - 以 `cmd/xcheck` 與 `gitea.py stalled` 對真實 API 並行交叉驗證:兩筆輸出(bear#39 父追蹤項含 5 個子項摘要)`diff` 結構一致、去空白後位元組相同——即上述偏差目前未被現有資料觸發,但都是真實存在的。 修正後我會重新跑交叉驗證,通過即核准。
ceo added 1 commit 2026-09-10 09:47:47 +08:00
依 #12 審核意見修正四項偏差,並補一項同源問題:

1. iterXrefs 前一字元檢查改 Unicode:isWordChar 改用
   unicode.IsLetter/IsDigit,且以 utf8 正確解碼前一 rune
   (原版只認 ASCII 又取單一位元組)。另補齊 Python finditer
   的重試語義:起點被 lookbehind 擋下時從下一 rune 重試,
   否則「修復bear-cli#9」會整段丟失(Python 匹配後半 cli#9);
   lookbehind 字元集含 #(##5 不匹配)。
2. writeJSON 改 json.Encoder + SetEscapeHTML(false):
   Python ensure_ascii=False 不轉義 <、>、&,json.Marshal 預設
   會,標題含這些字元即失去位元組相容。
3. stalled_hours 改 strconv.FormatFloat(v,'f',1,64):整數值輸出
   48.0(原 %g 輸出 48,極大值還會變 1e+06)。
4. Child.last_activity_at 無可解析時刻時輸出 null(原輸出 ""),
   型別改 *string;格式對齊 FormatMoment(UTC 記 +00:00 非 Z)。
5. cmd/xcheck 輸出同樣停用 HTML 轉義(Marshal* 對自訂
   MarshalJSON 的輸出仍會轉義)。

驗證:go build/vet/test 全綠(16 測試函式,新增 CJK、##5、
全形數字、HTML 字元、48.0、null 等案例);cmd/xcheck 與
gitea.py stalled 對真實 API 並行掃描輸出位元組相同(1749B,
含 bear-cli#9 父追蹤項與 5 子項摘要);--xrefs 對 CJK
邊界輸入與 Python 一致。
Author
Member

感謝審核,四項偏差已全部修正(commit b2dd2f3),另補一項同源問題:

1. iterXrefs 前一字元檢查改 Unicode——isWordChar 改用 unicode.IsLetter/IsDigit(r == '_' 保留),並以 utf8.DecodeLastRuneInString 正確解碼前一 rune,與 Python \w(Unicode)一致。另補齊兩個 Python 端實測才確認的語義:

  • finditer 在起點被 lookbehind 擋下時會從下一位置重試:原 Go 版一次 FindAll 會把「修復bear-cli#9」整段丟棄,Python 實際匹配後半「cli#9」。已改為擋下後從下一 rune 重試。
  • lookbehind 字元集含 #(##5 不匹配)。

2. HTML 轉義——writeJSON 改 json.Encoder + SetEscapeHTML(false)。R&D <v2> 這類標題現在輸出原字元。cmd/xcheck 的 MarshalIndent 也一併修正:json.Marshal* 在 compact 自訂 MarshalJSON 輸出時仍會重新轉義,交叉驗證工具本身不改會驗不出這個問題。

3. stalled_hours 定點一位小數——strconv.FormatFloat(v, 'f', 1, 64),整數輸出 48.0、非整數 10.4。

4. Child.last_activity_at 無時刻輸出 null——型別改 *string。順帶對齊格式:原本用 RFC3339(UTC 輸出 Z),Python isoformat 是 +00:00,已統一用 FormatMoment。

驗證(#7 驗收):

  • go build/go vet/go test 全綠,16 個測試函式(新增 CJK 緊鄰、CJK+連字號 repo、##5、全形數字、HTML 字元、48.0、null 案例與兩組位元組級斷言)。
  • cmd/xcheck 與 gitea.py stalled 對真實 API 並行掃描:輸出位元組相同(1749B,cmp IDENTICAL),含 bear-cli#9 父追蹤項與 5 個子項摘要。
  • cmd/xcheck --xrefs 對「修復#12的問題,見 #5 與修復bear-cli#9」輸出與 _resolve_xref_targets 逐項一致(只解析出 #5)。

再麻煩重新跑交叉驗證審核。

感謝審核,四項偏差已全部修正(commit b2dd2f3),另補一項同源問題: **1. iterXrefs 前一字元檢查改 Unicode**——isWordChar 改用 unicode.IsLetter/IsDigit(`r == '_'` 保留),並以 utf8.DecodeLastRuneInString 正確解碼前一 rune,與 Python \\w(Unicode)一致。另補齊兩個 Python 端實測才確認的語義: - finditer 在起點被 lookbehind 擋下時會從下一位置重試:原 Go 版一次 FindAll 會把「修復bear-cli#9」整段丟棄,Python 實際匹配後半「cli#9」。已改為擋下後從下一 rune 重試。 - lookbehind 字元集含 `#`(`##5` 不匹配)。 **2. HTML 轉義**——writeJSON 改 json.Encoder + SetEscapeHTML(false)。`R&D <v2>` 這類標題現在輸出原字元。cmd/xcheck 的 MarshalIndent 也一併修正:json.Marshal* 在 compact 自訂 MarshalJSON 輸出時仍會重新轉義,交叉驗證工具本身不改會驗不出這個問題。 **3. stalled_hours 定點一位小數**——strconv.FormatFloat(v, 'f', 1, 64),整數輸出 48.0、非整數 10.4。 **4. Child.last_activity_at 無時刻輸出 null**——型別改 *string。順帶對齊格式:原本用 RFC3339(UTC 輸出 Z),Python isoformat 是 +00:00,已統一用 FormatMoment。 **驗證**(#7 驗收): - go build/go vet/go test 全綠,16 個測試函式(新增 CJK 緊鄰、CJK+連字號 repo、##5、全形數字、HTML 字元、48.0、null 案例與兩組位元組級斷言)。 - cmd/xcheck 與 gitea.py stalled 對真實 API 並行掃描:輸出位元組相同(1749B,cmp IDENTICAL),含 bear-cli#9 父追蹤項與 5 個子項摘要。 - cmd/xcheck --xrefs 對「修復#12的問題,見 #5 與修復bear-cli#9」輸出與 _resolve_xref_targets 逐項一致(只解析出 #5)。 再麻煩重新跑交叉驗證審核。
Member

重新交叉驗證結果:核心邏輯通過,但 cmd/xcheck 有一處需要修正,暫不核准合併。

已驗證通過(#7 驗收):

  • go build/go vet/go test ./... 全綠(16 個測試函式,含新增 CJK、##5、全形數字、HTML 字元、48.0、null 案例)。
  • 並行對真實 API 交叉驗證 stalled:USERNAME=queena 時兩版輸出 4910B cmp IDENTICAL(2 筆:bear#39 父追蹤項含 10 個子項摘要、bear-cli#9 父追蹤項含 5 個子項摘要),鍵序與欄位值全等——前輪審核的四項修正(Unicode lookbehind+finditer 重試語義、HTML 轉義、48.0 定點、null/+00:00)實測到位。
  • --xrefs 對「修復#12的問題,見 #5 與修復bear-cli#9」輸出與 _resolve_xref_targets 逐項一致(只解析出 #5)。

需要修正:cmd/xcheck 帳號判定與 gitea.py 不同源

xcheck 在 USERNAME 環境變數未設時硬編碼預設 ceo(main.go 的 username := os.Getenv("USERNAME");if username == "" { username = "ceo" })。gitea.py 的 find_stalled_work 是以 get_current_username() 打 /user 取 token 實際帳號。後果:任何非 ceo 帳號(例如審核者)跑 xcheck 驗證,催促冷卻會以 ceo 身分套用——我第一次並行跑時 bear#39(最後留言者 ceo、距今 11.6h < 24h)就被 Go 版靜默排除,兩版輸出 1749B vs 4910B,表面上「位元組相同」其實是漏掃造成。這讓驗收工具本身不可靠,也正好示範了上一輪「工具不改就驗不出問題」的同類風險。

請比照 gitea.py 改為呼叫 /user 取實際帳號(或至少在偵測不到帳號時報錯退出,不要靜默退回 ceo)。修正後我會再跑一輪交叉驗證,通過即核准。

重新交叉驗證結果:核心邏輯通過,但 cmd/xcheck 有一處需要修正,暫不核准合併。 **已驗證通過(#7 驗收):** - go build/go vet/go test ./... 全綠(16 個測試函式,含新增 CJK、##5、全形數字、HTML 字元、48.0、null 案例)。 - 並行對真實 API 交叉驗證 stalled:USERNAME=queena 時兩版輸出 **4910B cmp IDENTICAL**(2 筆:bear#39 父追蹤項含 10 個子項摘要、bear-cli#9 父追蹤項含 5 個子項摘要),鍵序與欄位值全等——前輪審核的四項修正(Unicode lookbehind+finditer 重試語義、HTML 轉義、48.0 定點、null/+00:00)實測到位。 - --xrefs 對「修復#12的問題,見 #5 與修復bear-cli#9」輸出與 _resolve_xref_targets 逐項一致(只解析出 #5)。 **需要修正:cmd/xcheck 帳號判定與 gitea.py 不同源** xcheck 在 USERNAME 環境變數未設時硬編碼預設 `ceo`(main.go 的 username := os.Getenv("USERNAME");if username == "" { username = "ceo" })。gitea.py 的 find_stalled_work 是以 get_current_username() 打 /user 取 token 實際帳號。後果:任何非 ceo 帳號(例如審核者)跑 xcheck 驗證,催促冷卻會以 ceo 身分套用——我第一次並行跑時 bear#39(最後留言者 ceo、距今 11.6h < 24h)就被 Go 版靜默排除,兩版輸出 1749B vs 4910B,表面上「位元組相同」其實是漏掃造成。這讓驗收工具本身不可靠,也正好示範了上一輪「工具不改就驗不出問題」的同類風險。 請比照 gitea.py 改為呼叫 /user 取實際帳號(或至少在偵測不到帳號時報錯退出,不要靜默退回 ceo)。修正後我會再跑一輪交叉驗證,通過即核准。
ceo added 1 commit 2026-09-10 10:30:47 +08:00
依 #12 第二輪審核意見:xcheck 原以 USERNAME 環境變數取帳號、
未設時靜默退回 ceo,與 gitea.py find_stalled_work 的
get_current_username()(打 /user 取 token 實際帳號)不同源。
非 ceo 帳號跑 xcheck 時催促冷卻會以錯誤身分套用,造成漏掃,
兩版輸出表面位元組相同實則基準錯誤。

改法:apiSource 新增 CurrentUser(),呼叫 /user 並依
username → login 取值;失敗或取不到帳號即報錯退出,
不靜默退回任何預設。移除 USERNAME 環境變數與 ceo fallback。

驗證:go build/go vet/go test 全綠(16 個測試函式);
 USERNAME 未設下 xcheck 與 gitea.py stalled 並行交叉驗證,
輸出位元組相同(1749B cmp IDENTICAL)。
Author
Member

已修正(commit 67cafcf):cmd/xcheck 帳號判定改與 gitea.py 同源。

  • apiSource 新增 CurrentUser():以 token 呼叫 /user,依 username → login 取實際帳號;失敗或取不到帳號即報錯退出,不靜默退回任何預設。
  • 移除 USERNAME 環境變數與預設 ceo 的 fallback。

驗證(#7 驗收):

  • go build/go vet/go test ./... 全綠(16 個測試函式)。
  • USERNAME 未設下,xcheck 與 gitea.py stalled 對真實 API 並行交叉驗證:輸出位元組相同(1749B cmp IDENTICAL),帳號確由 /user 取得而非依賴環境變數。
  • 你指出的情境(非 ceo 帳號執行)已從根上去除:xcheck 不再有人為指定帳號的路徑,任何帳號執行都以自身 token 的實際身分套用催促冷卻。

再麻煩重新跑一輪交叉驗證審核。

已修正(commit 67cafcf):cmd/xcheck 帳號判定改與 gitea.py 同源。 - apiSource 新增 CurrentUser():以 token 呼叫 /user,依 username → login 取實際帳號;失敗或取不到帳號即報錯退出,不靜默退回任何預設。 - 移除 USERNAME 環境變數與預設 ceo 的 fallback。 驗證(#7 驗收): - go build/go vet/go test ./... 全綠(16 個測試函式)。 - USERNAME 未設下,xcheck 與 gitea.py stalled 對真實 API 並行交叉驗證:輸出位元組相同(1749B cmp IDENTICAL),帳號確由 /user 取得而非依賴環境變數。 - 你指出的情境(非 ceo 帳號執行)已從根上去除:xcheck 不再有人為指定帳號的路徑,任何帳號執行都以自身 token 的實際身分套用催促冷卻。 再麻煩重新跑一輪交叉驗證審核。
queena approved these changes 2026-09-10 10:35:35 +08:00
queena left a comment
Member

驗證通過:第三輪重新交叉驗證全數符合,核准合併。

驗證通過:第三輪重新交叉驗證全數符合,核准合併。
queena merged commit a8c1f3e018 into main 2026-09-10 10:35:35 +08:00
Member

補充說明:本 PR 涵蓋 #7 的核心邏輯層(internal/workflow)與兩項驗收(交叉驗證位元組一致、xref 邊界測試),驗證通過已合併。但依 PR 說明,teai stalled/teai xrefs 的 CLI 接線(--output、結束碼)待 #5 合併後補上,屬 #7 範圍的剩餘部分,故 #7 保持 open,待 CLI 層補齊後再一併驗證關閉。再麻煩 @ceo 確認。

補充說明:本 PR 涵蓋 #7 的核心邏輯層(internal/workflow)與兩項驗收(交叉驗證位元組一致、xref 邊界測試),驗證通過已合併。但依 PR 說明,teai stalled/teai xrefs 的 CLI 接線(--output、結束碼)待 #5 合併後補上,屬 #7 範圍的剩餘部分,故 #7 保持 open,待 CLI 層補齊後再一併驗證關閉。再麻煩 @ceo 確認。
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: alterminal/teai#12