Files
ISEP/scripts/test-ticket-triage.sh
T
Leo 2bbcda2ac3 雲端工人會分診、會組里程碑,組了就照里程碑做(inkstone/ISEP#159)
leo 09-18:「它看到票時要能夠組成 milestone 提出版本,所以不只是看票,還要組票。」
雲端只會撿(ISEP#130),沒有任何機制分診或組里程碑;open 302 張裡 74 張沒 s/*、257 張沒掛里程碑。

- ticket triage:沒 s/* 的票 → s/backlog/s/triage/s/pending+票上一則說明(--why 必填),一個動作;
  --dup-of:說明+close/duplicate+關+在留下的那張留一則指回來(留下的那張若是 leo 的就不寫)
- ticket compose:把已分診的票組成 InkStoneCo 里程碑(開工規約 1–7 條):既有載體+前置相依、
  s/backlog → s/todo、名字不准是版本號、期限 1–60 天、不開新票;先全部讀過才寫,理由一次列齊
- 不碰:Human/human/*、指派給別人(含 Leo)——判準是標籤與 assignee 登入名
- pick --pool:還沒到期的 InkStoneCo 里程碑的前置排在散票之前;可做的都抓盡時回待分診的票,
  每列帶 next;queue-drain-guard 照 next 印(待分診是 triage,不是 claim)
- 全局票池只有一個讀法 pool_issues(),接口留給 ISEP#154(global_index 寫卡驗完後只換這一個函式)
- InkStoneCo 掛在 open 里程碑的票=載體(容器):不進票池、不算待分診

假設(寫在這裡等總管 review):
- 分診只收「沒有 s/*」的票;s/triage 仍是總管/leo 的判斷,工人只能把拿不準的丟進 s/triage
- 載體撈不到時,工人用 ticket where+ticket new 補一張 InkStoneCo User Story 當載體(開工規約第 2 條的「真缺口」)
- 期限上限 60 天(9999 與過去日期都擋)

實測:test-ticket-triage 80/80、test-ticket-pick 84/84、queue-drain-guard 28/28;
真 Gitea 代跑一趟:分診 4 張、建 InkStoneCo milestone 68(載體 InkStoneCo#136,前置 ISEP#47、arcrun-rag#199),
pick --pool 第一組即為它的前置;一次違規三格的 compose 真的零寫入擋下。
待總管定版。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 18:43:07 +08:00

301 lines
22 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# 雲端工人「分診+組里程碑」的測試(inkstone/ISEP#159
#
# 票上的驗收:
# 1. 沒狀態的票數下降,每張被動過的票上都有一則說明(分到哪、為什麼;重複的指向哪張)
# 2. 提出新里程碑:載體票在 InkStoneCo、執行票掛成前置、名字不是版本號、有期限、deliverable=版本
# 3. 不碰 Human、不碰指派給 Leo 的票
# 4. 組了之後照里程碑往下做(pick --pool 先回它的前置票)
#
# 全程離線:api() 換成錄音機餵假池子;TICKET_HOST 指到連不上的位址,漏接一條會當場炸,不會假綠。
cd "$(dirname "$0")/.." || exit 1
T=scripts/ticket
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
export TICKET_HOST=http://127.0.0.1:9 GITEA_TOKEN=x ISEP_MEMBER_BACKOFF=0
export ISEP_COUNTDOWN_STATE_DIR="$TMP/state"; mkdir -p "$ISEP_COUNTDOWN_STATE_DIR"
# 時鐘定在 2026-09-18 00:00 UTC
export ISEP_COUNTDOWN_NOW=$(python3 -c 'import datetime as d;print(int(d.datetime(2026,9,18,0,0,tzinfo=d.timezone.utc).timestamp()))')
unset ISEP_MAINLINE_FILE ISEP_IDENTITY CLAUDE_PROJECT_DIR ISEP_DRAIN
export ISEP_WORKER_LOGIN=claude-code # 抓票者自己是誰(真跑時問 /user;離線測試不打網路)
MAIN="$TMP/mainline.json"
cat > "$MAIN" <<'JSON'
{"ref":"inkstone/InkStoneCo#65","owner":"inkstone","repo":"InkStoneCo","id":65,
"title":"主線 M","due_on":"2026-09-25T23:59:59Z","members":[]}
JSON
cat > "$TMP/pool.py" <<'PY'
import json, os, re, urllib.parse
MS = {"main": {"id": 65, "title": "主線 M", "due_on": "2026-09-25T23:59:59Z", "state": "open"},
"mine": {"id": 70, "title": "組好的 M", "due_on": "2026-09-30T23:59:59Z", "state": "open"},
"sub": {"id": 9, "title": "子 repo 的舊里程碑", "due_on": "2026-10-30T23:59:59Z", "state": "open"}}
def I(repo, n, labels, ms=None, assignees=(), created="2026-09-01T00:00:00Z", pr=False, state="open", title=None):
d = {"number": n, "title": title or f"票 {repo}#{n}", "state": state,
"labels": [{"name": l} for l in labels],
"assignees": [{"login": a} for a in assignees],
"milestone": MS[ms] if ms else None, "created_at": created,
"repository": {"full_name": f"inkstone/{repo}"}}
if pr: d["pull_request"] = {"merged": False}
return d
POOL = {
"InkStoneCo": [I("InkStoneCo", 55, [], None, title="身為一天只看兩眼的人…"), # ★ 載體候選:沒里程碑、沒人
I("InkStoneCo", 132, ["Human", "s/pending"], "main", assignees=("Leo",)), # 主線載體(Leo 的)
I("InkStoneCo", 80, ["s/backlog"], "mine"), # 組好的 M 的載體(有 s/* 也不是任務)
I("InkStoneCo", 81, ["Human"], None), # 掛 Human 的載體候選
I("InkStoneCo", 82, [], "main"),
I("InkStoneCo", 83, [], None, assignees=("Leo",)), # 指派 Leo 的載體候選
I("InkStoneCo", 84, ["s/backlog"], None)], # 已是「組好的 M」載體的前置——不能再當載體 # 已掛在主線里程碑的載體候選
"ISEP": [I("ISEP", 1, [], None, created="2026-08-01T00:00:00Z"), # ★ 待分診(最舊)
I("ISEP", 2, [], None), # 待分診
I("ISEP", 3, ["Human"], None), # 沒 s/*、掛 Human:不碰
I("ISEP", 4, [], None, assignees=("Leo",)), # 沒 s/*、指派 Leo:不碰
I("ISEP", 5, ["s/backlog", "type/bug"], None), # ★ 已分診:可組進里程碑
I("ISEP", 6, ["s/triage"], None), # 還沒決定:不可組
I("ISEP", 7, ["s/backlog"], None), # 已是組好的 M 的前置
I("ISEP", 8, ["s/backlog"], "sub"), # 掛著子 repo 里程碑
I("ISEP", 9, [], None, pr=True), # PR
I("ISEP", 10, [], None, assignees=("claude-code",))], # 只指派給自己:可分診
"Arcrun": [I("Arcrun", 20, ["s/todo"], None), # ★ 已分診:可組進里程碑
I("Arcrun", 21, ["s/todo"], None), # 組好的 M 的前置(pick 用)
I("Arcrun", 22, ["s/backlog", "Human"], None), # Human 前置:不碰
I("Arcrun", 23, ["s/backlog"], None, assignees=("Leo",))], # 指派 Leo 的前置:不碰 # Human 前置:不碰
}
MSN = {"InkStoneCo": ["main", "mine"], "ISEP": ["sub"]}
DEPS = {"InkStoneCo#80": ["ISEP#7", "Arcrun#21", "InkStoneCo#84"]}
def _by_ref(ref):
repo, n = ref.split("#")
return next(i for i in POOL[repo] if i["number"] == int(n))
BLOCKS = {}
for _p, _cs in DEPS.items():
for _c in _cs:
BLOCKS.setdefault(_c, []).append(_p)
LABELS = ["s/triage", "s/backlog", "s/todo", "s/doing", "s/review", "s/pending", "close/duplicate",
"Human", "type/bug", "p/high"]
DROP = set(json.loads(os.environ.get("DROP", "[]")))
rec = []
def fake_api(path, payload=None, method=None):
verb = method or ("POST" if payload is not None else "GET")
rec.append((verb, path, payload))
if path.startswith("/orgs/inkstone/repos"):
return [{"name": r} for r in POOL]
if path == "/user":
return {"login": "claude-code"}
if path.endswith("/labels?limit=100"):
return [{"name": n, "id": k} for k, n in enumerate(LABELS, 1)]
if "/milestones" in path:
repo = path.split("/")[3]
if verb == "POST":
return {"id": 71, "title": payload["title"]}
return [MS[k] for k in MSN.get(repo, [])]
if "/issues?" in path:
repo = path.split("/")[3]
q = urllib.parse.parse_qs(path.split("?", 1)[1])
rows = [i for i in POOL[repo] if f"{repo}#{i['number']}" not in DROP]
if "milestones" in q:
want = q["milestones"][0]
return [i for i in rows if (i["milestone"] or {}).get("title") == want]
rows = [i for i in rows if i["state"] == "open"]
page = int(q.get("page", ["1"])[0]); per = int(q.get("limit", ["50"])[0])
return rows[(page - 1) * per: page * per]
m = re.match(r"/repos/inkstone/([\w-]+)/issues/(\d+)/(dependencies|blocks)(\?.*)?$", path)
if m:
if verb == "POST":
return {}
table = DEPS if m.group(3) == "dependencies" else BLOCKS
return [_by_ref(r) for r in table.get(f"{m.group(1)}#{m.group(2)}", [])]
m = re.match(r"/repos/inkstone/([\w-]+)/issues/(\d+)$", path)
if m and verb == "GET":
return _by_ref(f"{m.group(1)}#{m.group(2)}")
return {}
PY
export POOL_PY="$TMP/pool.py"
probe() {
python3 - "$1" "$2" <<'PY'
import sys, os, json, io, contextlib, importlib.util as u
from importlib.machinery import SourceFileLoader as L
spec = u.spec_from_loader("t", L("t", "scripts/ticket"))
m = u.module_from_spec(spec); spec.loader.exec_module(m)
ns = {}
exec(open(os.environ["POOL_PY"]).read(), ns)
m.api = ns["fake_api"]; rec = ns["rec"]
argv = json.loads(sys.argv[2]) if sys.argv[2] else None
buf = io.StringIO(); rc = None
if argv is not None:
with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
try:
getattr(m, argv[0])(argv[1:])
except SystemExit as e:
rc = e.code
W = [(mm, p, pl) for mm, p, pl in rec if mm in ("POST", "PATCH", "PUT", "DELETE")]
print(eval(sys.argv[1], {"m": m, "rec": rec, "W": W, "rc": rc, "out": buf.getvalue(), "I": ns["I"], "json": json}))
PY
}
p() { # p <說明> <want> <python 片段> [argv json]
got=$(probe "$3" "${4:-}" 2>&1)
if [ "$got" = "$2" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
printf 'want=%s got=%s %s\n' "$2" "$got" "$1"
}
j() { python3 -c 'import json,sys;print(json.dumps(sys.argv[1:],ensure_ascii=False))' "$@"; }
echo "── ① 規則本體(純函式)──"
p "沒 s/*、沒人、沒 Human → 待分診" "True" "m.triageable(I('ISEP',1,[]),'claude-code')[0]"
p "★ 掛 Human → 不碰(驗收 3)" "False" "m.triageable(I('ISEP',3,['Human']),'claude-code')[0]"
p "human/exec 同組 → 不碰" "False" "m.triageable(I('ISEP',3,['human/exec']),'claude-code')[0]"
p "★ 指派給 Leo → 不碰(驗收 3)" "False" "m.triageable(I('ISEP',4,[],assignees=('Leo',)),'claude-code')[0]"
p "只指派給自己 → 可分診" "True" "m.triageable(I('ISEP',10,[],assignees=('claude-code',)),'claude-code')[0]"
p "不知道自己是誰、票有指派 → 不碰(不猜)" "False" "m.triageable(I('ISEP',10,[],assignees=('claude-code',)),None)[0]"
p "已經有 s/*(連 s/triage 也算)→ 不是待分診" "False" "m.triageable(I('ISEP',6,['s/triage']),'claude-code')[0]"
p "PR → 不是票" "False" "m.triageable(I('ISEP',9,[],pr=True),'claude-code')[0]"
p "已關 → 不碰" "False" "m.triageable(I('ISEP',1,[],state='closed'),'claude-code')[0]"
p "InkStoneCo 裡掛在 open 里程碑的票(載體)→ 不是待分診(它是容器)" "False" "m.triageable(I('InkStoneCo',82,[],'main'),'claude-code')[0]"
p "別的 repo 掛著里程碑、沒 s/* → 照樣待分診(載體只認 InkStoneCo)" "True" "m.triageable(I('ISEP',8,[],'sub'),'claude-code')[0]"
p "名字以版本號開頭(1.4.52 雲端零件包)→ 算版本號名字" "True" "m._version_like('1.4.52 雲端零件包')"
p "v 開頭也算" "True" "m._version_like('v0.30 什麼')"
p "名字裡提到版本但不是開頭(封測不再斷,含 1.4 修正)→ 不算(誤攔比漏擋嚴重)" "False" "m._version_like('封測不再斷,含 1.4 修正')"
echo
echo "── ② triage:一個動作=說明+欄位(驗收 1)──"
TR=$(j cmd_triage inkstone/ISEP#1 --to s/backlog --why "還在用、要修" --name isep-hand)
p "★ 先留說明、再改標籤:寫入恰好是 [留言, 改標籤]" "[('POST', '/repos/inkstone/ISEP/issues/1/comments'), ('PUT', '/repos/inkstone/ISEP/issues/1/labels')]" \
"[(mm,p) for mm,p,_ in W]" "$TR"
p "★ 說明第一行是【身份】,內文講得出分到哪、為什麼" "True" \
"(lambda b: b.split(chr(10))[0]=='【身份】isep-handinkstone/ISEP-' and 's/backlog' in b and '還在用、要修' in b)(W[0][2]['body'])" "$TR"
p "標籤換成 s/backlogid 2" "[2]" "W[1][2]['labels']" "$TR"
p "離開碼 0" "None" "rc" "$TR"
TD=$(j cmd_triage inkstone/ISEP#2 --dup-of inkstone/ISEP#5 --why "同一件事" --name isep-hand)
p "★ 重複:留說明→標 close/duplicate→關→在留下的那張留一則指回來" \
"[('POST', '/repos/inkstone/ISEP/issues/2/comments'), ('PUT', '/repos/inkstone/ISEP/issues/2/labels'), ('PATCH', '/repos/inkstone/ISEP/issues/2'), ('POST', '/repos/inkstone/ISEP/issues/5/comments')]" \
"[(mm,p) for mm,p,_ in W]" "$TD"
p "★ 重複那則說明指向留下的那張(驗收 1:重複的指向哪張)" "True" "'inkstone/ISEP#5' in W[0][2]['body']" "$TD"
p "關的是 state=closed、標的是 close/duplicateid 7" "True" "W[2][2]=={'state':'closed'} and W[1][2]['labels']==[7]" "$TD"
TDL=$(j cmd_triage inkstone/ISEP#2 --dup-of inkstone/InkStoneCo#132 --why "同一件事")
p "★ 重複的對象是 Leo 的票 → 這張照關,但**不在 Leo 的票上寫任何字**(驗收 3)" "True" \
"rc is None and not any('/InkStoneCo/' in p for _,p,_ in W) and '沒在' in out" "$TDL"
p "只指派給自己的 ISEP#10 → 分診得了" "None" "rc" "$(j cmd_triage inkstone/ISEP#10 --to s/triage --why 要總管看)"
echo
echo " (該擋的:擋下時一筆都沒寫)"
nb() { # nb <說明> <argv json> [要在訊息裡看到的字]
p "$1 → 離開碼 2、零寫入${3:+、訊息有「$3}" "True" "rc==2 and not W${3:+ and '$3' in out}" "$2"
}
nb "★ 掛 Human 的 ISEP#3(驗收 3" "$(j cmd_triage inkstone/ISEP#3 --to s/backlog --why x)" "掛著 Human"
nb "★ 指派給 Leo 的 ISEP#4(驗收 3" "$(j cmd_triage inkstone/ISEP#4 --to s/backlog --why x)" "指派給 Leo"
nb "已分診過的 ISEP#6" "$(j cmd_triage inkstone/ISEP#6 --to s/backlog --why x)" "已經分診過"
nb "PR" "$(j cmd_triage inkstone/ISEP#9 --to s/backlog --why x)"
p "triage --sign cloud-worker ⇒ 說明內文署 [cloud-worker]" "True" "'[cloud-worker]' in W[0][2]['body']" \
"$(j cmd_triage inkstone/ISEP#1 --to s/backlog --why x --sign cloud-worker)"
nb "沒寫 --why(說明是驗收 1 的本體)" "$(j cmd_triage inkstone/ISEP#1 --to s/backlog)" "--why"
nb "--to s/todo(要經 compose 排進里程碑才給)" "$(j cmd_triage inkstone/ISEP#1 --to s/todo --why x)" "compose"
nb "--to 與 --dup-of 都給" "$(j cmd_triage inkstone/ISEP#1 --to s/backlog --dup-of inkstone/ISEP#5 --why x)"
nb "--dup-of 指到自己" "$(j cmd_triage inkstone/ISEP#1 --dup-of inkstone/ISEP#1 --why x)"
p "參數錯(沒 --why)→ 一通 API 都沒打" "0" "len(rec)" "$(j cmd_triage inkstone/ISEP#1 --to s/backlog)"
p "身份名不在名單上 → 一通 API 都沒打" "True" "rc==2 and len(rec)==0" "$(j cmd_triage inkstone/ISEP#1 --to s/backlog --why x --name 路人)"
echo
echo " (--list:工人拿什麼來判斷)"
p "★ --list 列出待分診的票(InkStoneCo 還沒分診的也算),不列 Human/Leo/已分診/PR" \
"['inkstone/ISEP#1', 'inkstone/ISEP#10', 'inkstone/ISEP#2', 'inkstone/InkStoneCo#55']" \
"[r['ref'] for r in json.loads(out)]" "$(j cmd_triage --list --json)"
p "--list --all 連已分診的 open 票也列(找重複要看全部),並標出哪些可分診" "True" \
"(lambda rows: len(rows)>10 and {r['ref'] for r in rows if r['triageable']}=={'inkstone/ISEP#1','inkstone/ISEP#2','inkstone/ISEP#10','inkstone/InkStoneCo#55'})(json.loads(out))" \
"$(j cmd_triage --list --all --json)"
p "--list 文字版講得出「不碰」的有幾張,並給出下一步那一行" "True" \
"'不碰' in out and ' triage inkstone/ISEP#1 ' in out" "$(j cmd_triage --list)"
p "--list 只讀不寫" "True" "not W" "$(j cmd_triage --list)"
echo
echo "── ③ compose:組里程碑(驗收 2)──"
CP=$(j cmd_compose --title "雲端自己把票池消化成版本" --goal "leo 不必一張張叫它做" --due 2026-10-01 \
--carrier inkstone/InkStoneCo#55 inkstone/ISEP#5 inkstone/Arcrun#20 --name isep-hand)
p "★ 里程碑建在 inkstone/InkStoneCo(開工規約第 7 條)" "True" \
"[p for mm,p,_ in W if mm=='POST' and p.endswith('/milestones')]==['/repos/inkstone/InkStoneCo/milestones']" "$CP"
p "★ 有期限(23:59:59Z),不是 9999" "2026-10-01T23:59:59Z" \
"[pl['due_on'] for mm,p,pl in W if p.endswith('/milestones')][0]" "$CP"
p "★ 描述寫明這一輪要讓什麼變成真的、deliverable=總管驗過的新版本、載體與前置" "True" \
"(lambda d: '這一輪要讓什麼變成真的:leo 不必' in d and 'deliverable' in d and '新版本' in d and 'inkstone/InkStoneCo#55' in d and 'inkstone/ISEP#5' in d)([pl['description'] for mm,p,pl in W if p.endswith('/milestones')][0])" "$CP"
p "★ 載體票掛上新里程碑(id 71" "True" \
"('PATCH','/repos/inkstone/InkStoneCo/issues/55',{'milestone':71}) in W" "$CP"
p "★ 兩張執行票都掛成載體票的前置(/dependencies" "True" \
"[pl for mm,p,pl in W if mm=='POST' and p=='/repos/inkstone/InkStoneCo/issues/55/dependencies']==[{'owner':'inkstone','repo':'ISEP','index':5},{'owner':'inkstone','repo':'Arcrun','index':20}]" "$CP"
p "★ s/backlog → s/todo(已排進里程碑,等人領票),type/bug 留著" "[3, 9]" \
"sorted([pl['labels'] for mm,p,pl in W if mm=='PUT' and p=='/repos/inkstone/ISEP/issues/5/labels'][0])" "$CP"
p "本來就 s/todo 的 Arcrun#20 不重貼標籤" "False" "any(p.endswith('/Arcrun/issues/20/labels') for _,p,_ in W)" "$CP"
p "每張前置與載體都留了一則說明(第一行【身份】)" "True" \
"sorted(p for mm,p,pl in W if mm=='POST' and p.endswith('/comments') and pl['body'].startswith('【身份】'))==['/repos/inkstone/Arcrun/issues/20/comments','/repos/inkstone/ISEP/issues/5/comments','/repos/inkstone/InkStoneCo/issues/55/comments']" "$CP"
p "★ 不開任何新票(開工規約第 2 條)" "False" \
"any(mm=='POST' and p.endswith('/issues') for mm,p,_ in W)" "$CP"
p "離開碼 0,印出里程碑網址" "True" "rc is None and '/inkstone/InkStoneCo/milestone/71' in out" "$CP"
CPS=$(j cmd_compose --title "雲端自己把票池消化成版本" --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#55 inkstone/ISEP#5 --sign cloud-worker)
p "--sign cloud-worker ⇒ 每則留言內文都署 [cloud-worker]--sign 的值不會被當成前置票" "True" \
"rc is None and all('[cloud-worker]' in pl['body'] for mm,p,pl in W if p.endswith('/comments')) and not any('cloud-worker' in str(pl) for mm,p,pl in W if p.endswith('/dependencies'))" "$CPS"
p "沒給 --sign ⇒ 不署一個寫死的名字(誰跑的就署誰)" "False" \
"any('[cloud-worker]' in pl['body'] for mm,p,pl in W if p.endswith('/comments'))" "$CP"
echo
echo " (該擋的:先全部驗過才寫——擋下時一筆都沒寫,而且理由一次列齊)"
cb() { # cb <說明> <要在訊息裡看到的字> <argv...>
what=$1; want=$2; shift 2
p "$what → 離開碼 2、零寫入、訊息有「$want" "True" "rc==2 and not W and '$want' in out" "$(j cmd_compose "$@")"
}
OK=(--goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#55)
cb "★ 名字是版本號(開工規約第 5 條)" "版本號" --title "1.4.52 雲端零件包" "${OK[@]}" inkstone/ISEP#5
cb "沒名字" "--title" "${OK[@]}" inkstone/ISEP#5
cb "★ 沒期限(開工規約第 6 條)" "--due" --title 好名字 --goal g --carrier inkstone/InkStoneCo#55 inkstone/ISEP#5
cb "期限 9999" "天內" --title 好名字 --goal g --due 9999-01-01 --carrier inkstone/InkStoneCo#55 inkstone/ISEP#5
cb "期限在過去" "天內" --title 好名字 --goal g --due 2026-09-01 --carrier inkstone/InkStoneCo#55 inkstone/ISEP#5
cb "載體不在 InkStoneCo(不准在子 repo 建里程碑)" "InkStoneCo" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/ISEP#1 inkstone/ISEP#5
cb "沒有前置" "前置" --title 好名字 "${OK[@]}"
cb "★ 載體掛 Human(驗收 3" "載體 inkstone/InkStoneCo#81:掛著 Human" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#81 inkstone/ISEP#5
cb "★ 載體指派給 Leo(驗收 3" "載體 inkstone/InkStoneCo#83:指派給 Leo" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#83 inkstone/ISEP#5
cb "載體本身是別的 open 里程碑載體的前置(那一輪還在跑)" "那一輪還在跑" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#84 inkstone/ISEP#5
cb "載體已在別的 open 里程碑" "已經掛在里程碑" --title 好名字 --goal g --due 2026-10-01 --carrier inkstone/InkStoneCo#82 inkstone/ISEP#5
cb "同名 open 里程碑已在" "同名" --title "組好的 M" "${OK[@]}" inkstone/ISEP#5
cb "★ 前置掛 Human(驗收 3" "前置 inkstone/Arcrun#22:掛著 Human" --title 好名字 "${OK[@]}" inkstone/Arcrun#22
cb "★ 前置指派給 Leo(驗收 3" "前置 inkstone/Arcrun#23:指派給 Leo" --title 好名字 "${OK[@]}" inkstone/Arcrun#23
cb "前置還沒分診(沒 s/*" "還沒分診" --title 好名字 "${OK[@]}" inkstone/ISEP#1
cb "前置是 s/triage(要不要做還沒決定)" "還沒分診" --title 好名字 "${OK[@]}" inkstone/ISEP#6
cb "前置已經是別的里程碑載體的前置(一張票同時只排一輪)" "只排一輪" --title 好名字 "${OK[@]}" inkstone/ISEP#7
cb "前置掛著子 repo 的里程碑" "已經掛在里程碑" --title 好名字 "${OK[@]}" inkstone/ISEP#8
p "★ 好幾格不對時一次列齊(Human 前置+沒分診前置,兩個理由都在)" "True" \
"rc==2 and not W and 'Arcrun#22:掛著 Human' in out and 'ISEP#1 還沒分診' in out" \
"$(j cmd_compose --title 好名字 "${OK[@]}" inkstone/Arcrun#22 inkstone/ISEP#1)"
p "參數層的錯(版本號名字)在打任何 API 之前就擋" "0" "len(rec)" \
"$(j cmd_compose --title 1.2.3 "${OK[@]}" inkstone/ISEP#5)"
echo
echo "── ④ 組了之後照里程碑往下做(驗收:再照里程碑往下做)──"
POOLA=$(j cmd_pick --mainline "$MAIN" --pool --all --json)
p "★ --pool:還沒到期的 InkStoneCo 里程碑「組好的 M」一組,成員是載體的前置(ISEP#7 的 s/backlog 不抓,Arcrun#21 s/todo 可抓)" \
"['inkstone/Arcrun#21']" "[r['ref'] for r in json.loads(out) if r['group']=='里程碑']" "$POOLA"
p "★ 載體票本身(InkStoneCo#80s/backlog)不當成可做的票——里程碑那組與票池那組都沒有它" "False" \
"any(r['ref']=='inkstone/InkStoneCo#80' for r in json.loads(out))" "$POOLA"
p "里程碑那一組排在票池之前(先照里程碑做,再撿散票)" "True" \
"(lambda g: g.index('里程碑') < g.index('票池'))([r['group'] for r in json.loads(out)])" "$POOLA"
p "★ 待分診那一組最舊的先(ISEP#1 08-01 開),不含 HumanLeo" \
"['inkstone/ISEP#1', 'inkstone/ISEP#10', 'inkstone/ISEP#2', 'inkstone/InkStoneCo#55']" \
"[r['ref'] for r in json.loads(out) if r['group']=='待分診']" "$POOLA"
p "每一列都帶下一步(里程碑那一列是 claim、待分診那一列是 triage" "True" \
"all((' triage ' in r['next']) == (r['group']=='待分診') for r in json.loads(out))" "$POOLA"
p "沒帶 --pool(本機總管)→ 不看還沒到期的里程碑、也不回待分診(誤攔比漏擋嚴重)" "True" \
"rc==1 and '里程碑「組好的' not in out and '待分診' not in out" "$(j cmd_pick --mainline "$MAIN")"
p "pick 只讀不寫" "True" "not W" "$POOLA"
echo
echo "── ⑤ 動詞接得上 ──"
out=$(python3 "$T" usage triage 2>&1); got=$?
if [ "$got" = 0 ] && printf '%s' "$out" | grep -q -- '--dup-of'; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
printf 'ticket usage triage 印得出用法\n'
out=$(python3 "$T" usage compose 2>&1); got=$?
if [ "$got" = 0 ] && printf '%s' "$out" | grep -q -- '--carrier'; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
printf 'ticket usage compose 印得出用法\n'
out=$(python3 "$T" triage inkstone/ISEP#1 --to s/backlog --why x 2>&1); got=$?
if [ "$got" != 0 ] && ! printf '%s' "$out" | grep -q '✅'; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
printf 'triage 連不上 Gitea → 非零離開、不印成功(got=%s\n' "$got"
echo
echo "通過 $PASS 條,失敗 $FAIL"
[ "$FAIL" = 0 ] || exit 1