feat/issue7-stalled-xrefs
main
父項:#7(#3 子項)。
本 PR 交付核心邏輯層;teai stalled/teai xrefs CLI 命令接線與 --output/結束碼整合待 #5(internal/gitea,@max 認領中)合併後接上,屆時補齊 CLI 層交叉驗證。
完整移植 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 判定。
審核結果:需要修正,暫不核准合併。
核心邏輯與測試大致紮實(reason 分類、催促冷卻、父追蹤、上行引用排除、/issues 混入 PR 過濾都對得上 gitea.py),但對 #7「輸出 JSON 與 gitea.py 位元組相容」這條驗收,有三處已實證的偏差,其中兩處在真實資料很快就會踩到:
1. iterXrefs 前一字元檢查只認 ASCII,Python 的 \w 是 Unicode(最實質)
iterXrefs
\w
gitea.py 的 lookbehind 是 (?<![\w/#]),Python \w 含 CJK;Go 版 isWordChar 只認 ASCII,且 rune(text[start-1]) 取的是單一位元組不是 rune。中文緊鄰 #N 時兩邊行為不同:
(?<![\w/#])
isWordChar
rune(text[start-1])
#N
修復#12的問題,見 #5
#5
#12
建議:isWordChar 改為 unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_'(並正確解碼前一 rune)。
unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_'
2. json.Marshal 預設把 <、>、& 轉義成 \u003c/\u003e/\u0026,Python(ensure_ascii=False)不會
json.Marshal
<
>
&
\u003c
\u003e
\u0026
ensure_ascii=False
標題含這些字元(R&D、<v2> 之類)即失去位元組相容。實測同一 Item:
R&D
<v2>
"title":"R,D \u003cv2\u003e 規劃"
"title": "R,D <v2> 規劃"
建議:字串輸出改用 json.Encoder 加 SetEscapeHTML(false)。
json.Encoder
SetEscapeHTML(false)
3. stalled_hours 用 %g:整數值輸出 48,Python 輸出 48.0(極大值還會變 1e+06)
stalled_hours
%g
48
48.0
1e+06
round1 已舍到一位小數,改 strconv.FormatFloat(v, 'f', 1, 64) 即可同時輸出 48.0/10.4。目前資料恰好都是非整數值所以交叉驗證沒踩到,屬潛在偏差。
strconv.FormatFloat(v, 'f', 1, 64)
10.4
次要(同一原則,順手修): Child.last_activity_at 在子項無可解析時刻時,Go 輸出 "",Python 輸出 null(_child_activity 的 max(...) if child_moments else None)。
Child.last_activity_at
""
null
_child_activity
max(...) if child_moments else None
本次已驗證通過的部分(供參考):
go build
go vet
go test ./...
cmd/xcheck
gitea.py stalled
diff
修正後我會重新跑交叉驗證,通過即核准。
依 #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 一致。
感謝審核,四項偏差已全部修正(commit b2dd2f3),另補一項同源問題:
1. iterXrefs 前一字元檢查改 Unicode——isWordChar 改用 unicode.IsLetter/IsDigit(r == '_' 保留),並以 utf8.DecodeLastRuneInString 正確解碼前一 rune,與 Python \w(Unicode)一致。另補齊兩個 Python 端實測才確認的語義:
r == '_'
#
##5
2. HTML 轉義——writeJSON 改 json.Encoder + SetEscapeHTML(false)。R&D <v2> 這類標題現在輸出原字元。cmd/xcheck 的 MarshalIndent 也一併修正:json.Marshal* 在 compact 自訂 MarshalJSON 輸出時仍會重新轉義,交叉驗證工具本身不改會驗不出這個問題。
R&D <v2>
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 驗收):
再麻煩重新跑交叉驗證審核。
重新交叉驗證結果:核心邏輯通過,但 cmd/xcheck 有一處需要修正,暫不核准合併。
已驗證通過(#7 驗收):
需要修正: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,表面上「位元組相同」其實是漏掃造成。這讓驗收工具本身不可靠,也正好示範了上一輪「工具不改就驗不出問題」的同類風險。
ceo
請比照 gitea.py 改為呼叫 /user 取實際帳號(或至少在偵測不到帳號時報錯退出,不要靜默退回 ceo)。修正後我會再跑一輪交叉驗證,通過即核准。
依 #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)。
已修正(commit 67cafcf):cmd/xcheck 帳號判定改與 gitea.py 同源。
再麻煩重新跑一輪交叉驗證審核。
驗證通過:第三輪重新交叉驗證全數符合,核准合併。
補充說明:本 PR 涵蓋 #7 的核心邏輯層(internal/workflow)與兩項驗收(交叉驗證位元組一致、xref 邊界測試),驗證通過已合併。但依 PR 說明,teai stalled/teai xrefs 的 CLI 接線(--output、結束碼)待 #5 合併後補上,屬 #7 範圍的剩餘部分,故 #7 保持 open,待 CLI 層補齊後再一併驗證關閉。再麻煩 @ceo 確認。
No dependencies set.
The note is not visible to the blocked user.
父項:#7(#3 子項)。
內容
驗收對應(#7)
說明
本 PR 交付核心邏輯層;teai stalled/teai xrefs CLI 命令接線與 --output/結束碼整合待 #5(internal/gitea,@max 認領中)合併後接上,屆時補齊 CLI 層交叉驗證。
審核結果:需要修正,暫不核准合併。
核心邏輯與測試大致紮實(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也匹配。建議:
isWordChar改為unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_'(並正確解碼前一 rune)。2.
json.Marshal預設把<、>、&轉義成\u003c/\u003e/\u0026,Python(ensure_ascii=False)不會標題含這些字元(
R&D、<v2>之類)即失去位元組相容。實測同一 Item:"title":"R,D \u003cv2\u003e 規劃""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結構一致、去空白後位元組相同——即上述偏差目前未被現有資料觸發,但都是真實存在的。修正後我會重新跑交叉驗證,通過即核准。
感謝審核,四項偏差已全部修正(commit b2dd2f3),另補一項同源問題:
1. iterXrefs 前一字元檢查改 Unicode——isWordChar 改用 unicode.IsLetter/IsDigit(
r == '_'保留),並以 utf8.DecodeLastRuneInString 正確解碼前一 rune,與 Python \w(Unicode)一致。另補齊兩個 Python 端實測才確認的語義:#(##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 驗收):
再麻煩重新跑交叉驗證審核。
重新交叉驗證結果:核心邏輯通過,但 cmd/xcheck 有一處需要修正,暫不核准合併。
已驗證通過(#7 驗收):
需要修正: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)。修正後我會再跑一輪交叉驗證,通過即核准。
已修正(commit 67cafcf):cmd/xcheck 帳號判定改與 gitea.py 同源。
驗證(#7 驗收):
再麻煩重新跑一輪交叉驗證審核。
驗證通過:第三輪重新交叉驗證全數符合,核准合併。
補充說明:本 PR 涵蓋 #7 的核心邏輯層(internal/workflow)與兩項驗收(交叉驗證位元組一致、xref 邊界測試),驗證通過已合併。但依 PR 說明,teai stalled/teai xrefs 的 CLI 接線(--output、結束碼)待 #5 合併後補上,屬 #7 範圍的剩餘部分,故 #7 保持 open,待 CLI 層補齊後再一併驗證關閉。再麻煩 @ceo 確認。