主線成員讀不到任何一條 /dependencies 時不准印出完整名單:set 不印 並離開碼 3、refresh/adopt 保留舊快取並講出來(inkstone/ISEP#130 → c6973)

總管 09-13 實撞:Gitea 握手逾時,hub 票 InkStoneCo#132 本身讀得到、它的 /dependencies 讀不到,
`mainline set` 仍印「 成員 6 張」,底下三張前置全漏;`refresh --members` 零輸出、名單沒變。
雲端工人靠這份名單抓票,漏收=以為抓盡=收工,等於 v0.30.0 換個入口又停。

- collect_members:清單讀取拿不到就重讀(ISEP_MEMBER_RETRIES 預設 2、退避 2s×n);
  仍讀不到的票記進 info["unread"],info["complete"]=沒有 unread 且沒被上限截斷
- set:不完整時印 ⚠️🔴 成員不完整)+點名讀不到的票,主線檔寫 members_complete/members_unread,
  同一條線保留舊快取成員,離開碼 3(主線本身照樣標定)
- refresh --members:完整才蓋快取並印成員張數;不完整保留舊快取並講出來;讀不到里程碑也講
- adopt:不完整時與舊快取取聯集

重現(改之前跑新測試):F1–F5、F9–F11、F13–F14 共 11 條紅——set 印 、arcrun-rag#104 漏收、
refresh --members 零輸出且把快取蓋成少兩張的名單。
修後:mainline-members-by-dependency 70/71(唯一紅的 ㊽ 在 main 上同樣紅)、repo-mirror 17/17、
gitea-fallback 31/31、focus-guard 24/24、ticket-pick 58/58、debt-worklist 47/47。
真 Gitea 唯讀跑(不寫主線檔):InkStoneCo#65 成員 9 張、complete=True、hub #132 底下三張都收到。
沒動 InkStoneCo 的 system-dev/mainline.json。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 13:57:44 +08:00
parent 0d49c0dcd7
commit c0eec76b52
3 changed files with 122 additions and 13 deletions
+31 -5
View File
@@ -364,18 +364,42 @@ def collect_members(api, ms, cap=MEMBER_WALK_CAP):
回 (members, info)
members 排好序的 `owner/repo#N` 清單(含已關的——進度與 `has` 都要看得到它們)
info {"milestone": [...], "via_deps": [...], "hub_ticket": ref|None,
"reached": bool, "capped": bool}
"reached": bool, "capped": bool, "unread": [...], "complete": bool}
🔴 `reached=False` 連 hub 里程碑那一頁都讀不到 ⇒ 呼叫端**不要拿空清單蓋掉舊快取**
(讀不到 ≠ 沒有成員)。
🔴 `unread` 讀不到 `/dependencies` 的票(重讀過仍失敗)。**只要有一張,名單就不完整**:
那張底下的前置全部沒收到(inkstone/ISEP#130 c6973Gitea 握手逾時,hub 票底下三張全漏,
`set` 卻印 ✅)。`complete`reached 且沒有 unread 且沒被上限截斷——呼叫端只准在它為真時
把名單當成完整的寫出去。
判準只有兩個 Gitea 事實:票的 `milestone.id` 等於錨的 id、以及相依邊。標題只拿來查詢。
"""
import time
import urllib.parse
try:
retries = max(0, int(os.environ.get("ISEP_MEMBER_RETRIES", "2")))
except ValueError:
retries = 2
try:
backoff = max(0.0, float(os.environ.get("ISEP_MEMBER_BACKOFF", "2")))
except ValueError:
backoff = 2.0
def read_list(path):
"""讀一頁清單;拿不到 list 就重讀(握手逾時常常下一次就好)。仍拿不到回 None。"""
for n in range(retries + 1):
got = api(path)
if isinstance(got, list):
return got
if n < retries and backoff:
time.sleep(backoff * (n + 1))
return None
owner, repo, mid = ms.get("owner"), ms.get("repo"), ms.get("id")
title = ms.get("title") or ""
info = {"milestone": [], "via_deps": [], "hub_ticket": hub_ticket(ms),
"reached": False, "capped": False}
"reached": False, "capped": False, "unread": [], "complete": False}
q = urllib.parse.urlencode({"state": "all", "type": "issues", "milestones": title, "limit": 100})
rows = api("/repos/%s/%s/issues?%s" % (owner, repo, q))
rows = read_list("/repos/%s/%s/issues?%s" % (owner, repo, q))
if not isinstance(rows, list):
return [], info
info["reached"] = True
@@ -400,9 +424,10 @@ def collect_members(api, ms, cap=MEMBER_WALK_CAP):
parts = split_ref(cur)
if not parts:
continue
deps = api("/repos/%s/%s/issues/%d/dependencies?limit=100" % parts)
deps = read_list("/repos/%s/%s/issues/%d/dependencies?limit=100" % parts)
if not isinstance(deps, list):
continue # 這一張讀不到:跳過它,不當成「沒有相依」以外的事
info["unread"].append(cur) # 讀不到 ≠ 沒有相依:記下來,名單就不算完整
continue
for d in deps:
r = issue_ref(d)
if not r or r in seen:
@@ -412,6 +437,7 @@ def collect_members(api, ms, cap=MEMBER_WALK_CAP):
break
seen.add(r); out.append(r); queue.append(r)
info["via_deps"].append(r)
info["complete"] = not info["unread"] and not info["capped"]
return sorted(set(out)), info