Skip to content

feat: Complexity 值域封閉化 + 四 consumer 共用 actionability gate (#316) - #318

Open
kiki830621 wants to merge 1 commit into
mainfrom
idd/316-actionability-gate
Open

feat: Complexity 值域封閉化 + 四 consumer 共用 actionability gate (#316)#318
kiki830621 wants to merge 1 commit into
mainfrom
idd/316-actionability-gate

Conversation

@kiki830621

Copy link
Copy Markdown
Member

這個 PR 做什麼

### Complexity 的值域封閉化,並讓四個 routing consumer 共用同一份 actionability 判定。

#298 的修正(PR #309 / #306)只落在 idd-list,其餘三個 consumer 未動;且 idd-list 自身
Step 5 仍文載會截斷的 regex,與新增的 Step 3.7 直接矛盾。本 PR 承接剩餘缺口(#316)。

根因

不是 parser 太窄,是把會變的狀態存進不可變的 artifact### Complexity 活在 append-only
的 Diagnosis comment,但 parked 會變(trigger 成立就該解除)。#136 的 comment 寫 bare
Spectra、body 寫 Spectra when triggered (parking lot) —— 狀態改不動就自己漂到別處去了。

IDD 早就知道正確做法:### Blocking 正因為會變才放在 body 由 idd-update 維護。本 PR 把
同一條推論套用到 parked 上。

主要改動

檔案 改動
scripts/lib/actionability.sh 新增 —— 唯一實作:idd_parse_complexity / idd_actionability_verdict / idd_actionability_group
references/actionability-gate.md 新增 —— canonical 契約,與 parallel-orchestration.md 的 Conflict Class 契約對稱
scripts/tests/actionability-gate/ 新增 —— 66 條斷言 + 15-row fixture
skills/idd-list/SKILL.md 消除 Step 5 與 Step 3.7 的自我矛盾
skills/idd-all/SKILL.md 改用 helper;dispatch table 補「match 到但值非法」的 row
skills/idd-implement/SKILL.md 改用 helper;移除「不確定就當 Simple」的寬鬆預設
skills/idd-plan/SKILL.md 改用 helper;tier 確認不再自行比對字串
skills/idd-diagnose/SKILL.md producer 端宣告封閉值域;明訂不自動貼 parking-lot label
references/ic-r011-checkpoint.md blocker:* 退役,收斂為 parking-lot

三個刻意的取捨

Strategy 的 - [~] 不進 gate。 它的既有 consumer 是 idd-close 的 checklist gate,語意是
per-item 的 close-time disposition。拿它回答 per-issue 的 routing 問題會撞壞既有語意。

migration 只貼 label、不改寫歷史。 回填既有 Diagnosis comment 會 modify-in-place 一個
append-only artifact;而且不必要 —— legacy 值走 unparseable 路徑得到的正是「not-actionable +
surface 原值」,對那批 parked issue 就是正確結果。實測當前 open backlog 域外值 0 筆

producer 不自動貼 label。 限定詞是 diagnose 當下的 AI 判斷,label 是人的裁決,兩者會分岔
(2026-08-10 的 11 筆實測只有 5 筆一致)。由前者推導後者,等於取消「人可以事後 park 一個
tier 明確的 issue」這條路徑。

行為變更(非純重構,請留意)

  • idd-implement 原本 _(missing / unclear)_ → 預設當 Simple,現在改為停止並要求先跑
    /idd-diagnose。那個寬鬆預設正是契約禁止的「代 user 挑 tier」。
  • idd-all / idd-plan 對域外值從未定義行為改為明確 abort + surface 原值。
  • #84 的 Blocked 分組行為不變 —— gate 統一但顯示分兩組,Blocked 的標題/banner/footer
    逐字保留。

驗證

Spectra

change add-actionability-gate —— proposal / design / spec / tasks 皆在本 PR 內。
10/12 tasks 完成,2 個標 - [~](migration 類別在 live backlog 為空集合,附實測理由)。

過程中另外發現、已獨立開案

Refs #298, #310, #316, #317

`### Complexity` 從沒宣告過封閉值域,也沒有 unparseable 契約 —— 而結構相同的姊妹欄位
`### Conflict Class` 兩者都有。於是 producer 寫出 `Simple when triggered` 不違反任何明文
規則,三個 consumer 各自發明互不相容的窄化:idd-list 靜默截成 `Simple`、idd-all 與
idd-implement 得到非法字串。2026-08-10 實測本 repo 的 22-issue backlog,11 個 diagnosed
裡 8 個 parked/deferred 被判成「Actionable now」,其中兩個帶著使用者親自下的 defer 裁決。

根因不是 parser 太窄,是**把會變的狀態存進不可變的 artifact**。Complexity 活在 append-only
的 Diagnosis comment,parked 卻會變(trigger 成立就該解除)。#136 的 comment 寫 bare
`Spectra`、body 寫 `Spectra when triggered (parking lot)` —— 狀態改不動就自己漂到別處。
IDD 早就知道正確做法:`### Blocking` 正因為會變才放在 body 由 idd-update 維護。

  - tier 值域收斂為封閉四值(可帶既有 ` via <來源>` 後綴),延期狀態遷至 parking-lot label
  - 三訊號 gate(域外值 / label / Blocking 區塊),放行需三者皆不成立
  - 解析與判定抽成 scripts/lib/actionability.sh,四個 consumer 全部改為引用 + fail-loud
    guard —— 沒有 guard 的話,「找不到就自己想辦法」的 consumer 會把三方分歧原封帶回來

三個刻意的取捨:

Strategy 的 `- [~]` **不進 gate**。它的既有 consumer 是 idd-close 的 checklist gate,語意是
per-item 的 close-time disposition;拿它回答 per-issue 的 routing 問題會撞壞既有語意。

migration **只貼 label、不改寫歷史**。回填既有 Diagnosis comment 會 modify-in-place 一個
append-only artifact;而且不必要 —— legacy 值走 unparseable 路徑得到的正是「not-actionable
+ surface 原值」,對那批 parked issue 就是正確結果。實測當前 open backlog 域外值 0 筆。

producer **不自動貼 label**。限定詞是 diagnose 當下的判斷,label 是人的裁決,兩者會分岔
(11 筆實測只有 5 筆一致)。由前者推導後者,等於取消「人可以事後 park 一個 tier 明確的
issue」這條路徑。

顯示層仍分兩組:reason 僅為 blocking 者維持 #84 既有的 Blocked 分組(標題/banner/footer
逐字不變),其餘進 Parked 組。gate 統一不等於顯示統一。

ic-r011 的 `blocker:infeasible` / `blocker:waiting` 一併退役 —— 0 個 issue 在用、無任何
consumer 讀,(b)/(c) 的區分保留在 audit 字串(那是說明性資訊,不是機械判定用)。

47/47 suites 綠(新增 actionability-gate 66 條,fixture 含 2026-08-10 事故快照,
斷言 9 筆中只有 #37 為 actionable)。

Refs #298, #310, #317
@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #318

Engine

pai-ensemble 2.20.0 (canonical #207) — 4 IDD lenses + DA + Codex (gpt-5.6-sol), model: opus

Aggregate

FAIL — 2 CRITICAL, 21 HIGH blocking; 28 MEDIUM / 11 LOW follow-up

Frozen snapshot d0ef6d1 — freshness gate passed (HEAD unchanged during review).

Scope coverage

PR refs: #37 #84 #136 #292 #298 #306 #309 #310 #316 #317 — 8 已 CLOSED/MERGED(敘述脈絡)、#317 為獨立 follow-up(PR body 自述不在本次修復範圍)
Verified scope: #316


#316 — bug: #298 的修正只落在 idd-list

Requirements coverage: 部分達成,但兩個 CRITICAL 使核心宣稱未成立。

CRITICAL(blocking,兩者互相獨立)

C1. 三訊號 gate 從未接進四個 consumer — parking-lot / Blocking 仍被繞過

lens: codex · plugins/issue-driven-dev/skills/idd-all/SKILL.md

Codex 的首要發現,也是它給出「建議修改後再合併」的理由:

問題idd_actionability_verdictscripts/lib/actionability.sh(約 file line 90–139)已完整實作,但四個 skill 的新增 bash 片段全部只呼叫 idd_parse_complexity,沒有任何一處讀 labels / ### Blocking 並送進 verdict:

  • idd-all/SKILL.mdTIER=$(idd_parse_complexity "$LATEST_DIAGNOSIS" 2>/dev/null); CEXIT=$? → 「Dispatch 以 (CEXIT, TIER) 為鍵」
  • idd-implement/SKILL.md:「Routing 以 (CEXIT, TIER) 為鍵」
  • idd-plan/SKILL.md:「以 (CEXIT, TIER) 為鍵決定行為」
  • idd-list/SKILL.mdtier=$(idd_parse_complexity "$latest_diagnosis_body"); cexit=$?

對照 spec(specs/actionability-gate/spec.md)明文:「evaluating exactly three signals」。

觸發情境### Complexity 是合法 Spectra(CEXIT=0),issue 帶 parking-lot label 或 ### Blocking 非空,使用者跑 /idd-all/idd-implement/idd-plan

後果:parser 回 exit 0,consumer 照 tier routing 直接執行。fixture 自己描述的 #37 形狀(bare Spectra、事後由人貼 parking-lot)在這三個 consumer 仍會被送進 implementation/Spectra 流程 —— 正是本變更宣稱要消滅的「自動推翻人為 park 裁決」。

建議:(1) 每個 consumer 讀 labels 與 body 的 Blocking section;(2) 一律呼叫 idd_actionability_verdict;(3) idd-list 對 exit 1 繼續列出並分組,另三者對 exit 1 停止 routing 並列出完整 reasons;(4) verdict exit 2 必須當成 API 誤用,不可與 not-actionable 混同;(5) 若「直接呼叫 /idd-implement 可覆寫 parking」是刻意設計,spec 需另立人工 override 契約——目前沒有這條例外。

C2. 封閉值域與本 repo 真實語料不符:90 筆真實 diagnosis 有 38 筆域外,其中 22 筆是舊 parser 路由正確、新契約改成 hard abort 的正常 issue

lens: devils-advocate · plugins/issue-driven-dev/scripts/lib/actionability.sh:73

四位評審都只讀 diff,沒有人拿真實資料跑過這個 helper。我跑了。

方法gh issue list --state closed --limit 120 取本 repo 最近 120 個 closed issue,抽每個 issue 最後一則 ^## Diagnosis comment,逐一餵進 idd_parse_complexity(helper 原檔,未修改)。90 個 issue 有 diagnosis comment。

結果

真實值長這樣(逐字,非我編的):Spectra(opt-out → 直接 propose)(#264 #258 #221)、Plan(Layer P:cross-file 一致性 + 判準應用的 judgment 邊界)(#215)、Simple — 單檔、2 個 1-token 補丁…(#160)、Spectra — 觸及 living spec(idd-verify MODIFIED)…(#205)、Plan —— 七個呼叫點互相依賴同一個述詞…(#293)。

這推翻了本 change 的因果診斷。design/proposal 認定「截斷是 bug」,但在真實語料上截斷 22 次是對的、1 次是錯的(#200 那一類才是錯的)。把截斷整個拿掉、改成「任何後綴文字=域外=停止」,是用一個 24%(22/90)的 false-stop 去換一個 10%(9/90)的 false-go —— 而且 false-stop 是硬停,比原本的靜默誤路由更容易讓整條 pipeline 停擺。design 明文否決的替代方案 (b)「只加寬 parser」,才是符合資料的那一個。

proposal 的 BREAKING 估計也是錯的proposal.md:14/21 寫「既有 9 筆帶限定詞的 diagnosis 值在新契約下為非法,需 migration」。實測光是最近 120 個 closed issue 就有 38 筆非法,是宣稱值的 4 倍以上。9 這個數字來自 2026-08-10「11 個 open diagnosed issue」這個有偏樣本(open backlog 本來就以 parked 為主)。

而且流量端沒有任何強制力:新的 producer 規則只是 idd-diagnose/SKILL.md:613 的一段散文;同一個 producer 在同一份模板下已經把理由寫進同一行 38/90 次,沒有任何 lint/test 檢查 emitted comment 的第一非空行。所以域外值不是歷史殘留,是還在持續產生的常態,consumer 端的 hard abort 會經常性 fire。

重現

. plugins/issue-driven-dev/scripts/lib/actionability.sh
gh issue view 264 --json comments --jq '[.comments[]|select(.body|test("^## Diagnosis";"m"))]|last|.body' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin))' >/tmp/b   # 或直接傳 body
idd_parse_complexity "$(cat /tmp/b)"; echo $?   # → 3, unparseable-complexity: Spectra(opt-out → 直接 propose)

HIGH(blocking)

# Finding Source File
H1 via <source> 後綴不受約束 — 封閉值域的逃生孔 codex actionability.sh:69
H2 awk 不理解 code fence — 散文範例裡的 ### Complexity 會被當成真欄位 codex actionability.sh:47
H3 consumer 的 command substitution 在 set -e 下會提前中止 — 違反「單筆壞資料不得中斷 listing」 codex actionability-gate.md
H4 option 缺 value 時 shift 2 失敗導致無限迴圈,而非契約規定的 exit 2 codex actionability.sh:99
H5 測試完全沒驗證四個 skill 的整合,也沒有 #84 輸出回歸 — false assurance codex test.sh:72
H6 legacy 值成為永久 not-actionable latch — 與「parking 是可變狀態」的設計目標自相矛盾 codex design.md
H7 新增 spec 的核心 requirement「三訊號 gate」沒有任何 consumer — idd_actionability_verdict 全 repo 零呼叫 requirements spec.md:25
H8 spec 要求的 Parked 顯示分組不存在;idd-list Step 5 指向一個沒有被實作的分組 requirements SKILL.md:516
H9 idd-list Step 3.7 仍保有私有值域窄化,且只列三個 tier — SDD-warranted 在同一檔內會得到兩個相反判定 requirements SKILL.md:255
H10 改寫了 live spec 明文規定的 parser 行為(trailing parenthetical 剝除),卻宣告 Modified Capabilities: (none) requirements proposal.md:30
H11 blocker:*parking-lot 收斂只做了 reference 一半:live spec 與 idd-issue SKILL.md 仍規定貼已退役的 label requirements spec.md:40
H12 idd_actionability_verdict hangs in a silent infinite loop when a flag is given without a value logic actionability.sh:101
H13 The three-signal gate and the display-grouping helper have zero consumers — only the parse half was wired up logic actionability.sh:97
H14 idd-list Step 3.7 still contradicts the new closed domain, and the "Parked 組" the new Step 5 routes to does not exist in the output spec logic SKILL.md:255
H15 Routing gate trusts unauthenticated issue comments — public repo, no author check on ## Diagnosis security SKILL.md:522
H16 parking-lot label — the human defer ruling — is never consulted by any consumer that mutates the repo security actionability.sh:97
H17 idd-list Step 3.7/3.9 keep a private Complexity narrowing that now contradicts the shared helper (SDD-warranted and Plan via Layer V get classified as parked) regression SKILL.md:255
H18 type=meeting issues legitimately have no ### Complexity; removing the missing→Simple fallback turns them into hard stops and mislabels them "Parked" regression SKILL.md:399
H19 idd_actionability_verdict hangs forever on a value-less flag — the contract promises exit 2, and four skills now source this lib regression actionability.sh:101
H20 The blocker:*parking-lot convergence updated one copy of the rule and left the prescriptive sibling and the published spec saying the opposite regression ic-r011-checkpoint.md:76
H21 tasks.md 5.1 宣稱的「helper 對真實資料的 end-to-end 驗證」是對空集合成立,零證據 devils-advocate tasks.md:31

MEDIUM / LOW(follow-up 候選)

MEDIUM ×28
  • M1 [codex] 空的 ### Complexity section 被誤報成 complexity-missing(應為 unparseable)
  • M2 [codex] markdown 裝飾剝除是非對稱的 — 未配對符號與 typo 被靜默正規化成合法 tier
  • M3 [codex] verdict 測試不驗 exit code、且輸出解析過度寬鬆
  • M4 [codex] idd-list 同段仍留著相反的「推不出 → /idd-implement (保守 default)」列
  • M5 [codex] migration 敘述在 proposal / design / tasks 三份文件間直接矛盾
  • M6 [codex] label 被定為 parked 唯一 source of truth,卻沒有任何寫入路徑
  • M7 [codex] 第三個訊號 ### Blocking 的抽取與「非空」定義沒有共用 — divergence 風險原封不動
  • M8 [requirements] Tasks 3.2 / 3.3 / 3.4 勾 [x] 但其宣告的驗證方式(測試斷言四個引用點)從未被實作
  • M9 [requirements] design.md 自訂的 feature: /idd-list blocked-state awareness output mode (anti-anxiety surfacing) #84 逐字回歸驗收條件未被任何測試覆蓋
  • M10 [requirements] gate 的四值封閉 reason 值域無法表達 blocked label 訊號,consumer 必須保留私有邏輯
  • M11 [requirements] 使用者可見的行為變更沒有 CHANGELOG 條目、也沒有 plugin 版號 bump
  • M12 [logic] idd_actionability_group silently answers parked for every malformed input and never fails loud
  • M13 [logic] Section-boundary detection only recognises ### headings, so an empty Complexity section reports a value stolen from the next section
  • M14 [logic] First-match-wins with no fenced-code awareness: a quoted example of ### Complexity shadows the real field
  • M15 [logic] The headline acceptance assertion is a fixture tautology — it passes with the idd-list / idd-all routing 讀不到「現在可不可以動」— Complexity 的 when-triggered 限定詞被截斷、parking-lot label 無 consumer(22-issue backlog 實測 8/9 誤路由) #298 bug fully re-introduced
  • M16 [logic] idd-diagnose has two copies of the Complexity template; only the second was updated, and the primary one lost the new rules
  • M17 [logic] idd-list's consumer snippet cannot obtain the raw value it is required to surface, and references an undefined variable
  • M18 [security] First-matching ### Complexity heading wins, including inside fenced code blocks — tier spoofing
  • M19 [security] No CRLF/control-character normalization at the trust boundary — defeats the mandatory-surfacing guarantee
  • M20 [security] Contract mandates echoing untrusted issue text verbatim into agent context — prompt-injection surface
  • M21 [regression] Scope creep: a routing-parser change also converts IC_R011 sister-sweep into an automatic parking-lot producer, contradicting this change's own "producer never derives the label" rule
  • M22 [regression] Acceptance criteria for non-regression of feature: /idd-list blocked-state awareness output mode (anti-anxiety surfacing) #84 and for the four consumer rewrites are marked done but no such test exists
  • M23 [regression] A fifth consumer of ### Complexity keeps its private parse and now feeds out-of-domain values into idd-route stats
  • M24 [regression] No version bump and no CHANGELOG entry — marketplace users stay on 2.108.0 and never receive scripts/lib/actionability.sh
  • M25 [devils-advocate] fixture 自稱 2026-08-10 snapshot,但記錄的 raw 值不是逐字原文——被整理掉的正好是會炸的那個形狀
  • M26 [devils-advocate] gate 唯一的人為裁決通道 fail-open,且用 repo 自己 bug-prevention: #141 discussion-metadata helper must verify label existence + surface UI cache-sync hint #142 protocol 明令禁止的 blind --label 方式施加
  • M27 [devils-advocate] 更正 logic 評審:被改的那份 Complexity 模板才是引入 regression 的一份——它授權 producer 寫 legacy alias
  • M28 [devils-advocate] CRLF diagnosis comment 讓合法 tier 變 unparseable,且『強制 surface』印出的是看不見的 CR
LOW ×11
  • L1 [codex] idd_actionability_group 對格式與未知 reason 過度敏感,會靜默誤分組
  • L2 [codex] diff 內含指示審閱者採用特定評估框架的祈使句(prompt-injection 型內容)
  • L3 [requirements] Step 3.7 內部四訊號表與其下方三項編號清單自相矛盾,本次未一併收斂
  • L4 [requirements] --parked 回訪模式(Step 3.9)未與新契約對齊,仍把 when triggered 當作 live 的 parked 來源
  • L5 [logic] The via <source> suffix is unvalidated, so deferral text smuggled after "via" resolves to a legal, actionable tier
  • L6 [logic] Reason-list comparison in the test depends on locale collation
  • L7 [security] $CLAUDE_PLUGIN_ROOT is unvalidated before source — unset expands to an absolute /scripts/... path
  • L8 [regression] idd-list Step 5 table still carries 推不出 → /idd-implement #N (保守 default), contradicting the prose added directly beneath it
  • L9 [regression] Helper invoked twice per call site, and idd-list's snippet omits how $latest_diagnosis_body is obtained
  • L10 [devils-advocate] 更正 logic 評審:headline assertion 確實是 tautology,但『idd-list / idd-all routing 讀不到「現在可不可以動」— Complexity 的 when-triggered 限定詞被截斷、parking-lot label 無 consumer(22-issue backlog 實測 8/9 誤路由) #298 bug 完全復原也會通過』不成立——我做了 mutation test
  • L11 [devils-advocate] 以『封閉列舉、不得類推』為論旨的 reference,自己把 suffix producer 列漏了一個

INFO ×7

含 4 份獨立的 prompt-injection 掃描結果(皆 clean)。

Scope Check

有 scope creep:M44 指出一個 routing-parser 變更同時改了 IC_R011 sister-sweep 的 label 慣例(blocker:*parking-lot),且該收斂只做了 reference 一半 —— live spec 與 idd-issue SKILL 仍是舊慣例(H13 / H22)。

Lens 統計

lens findings
codex 17
requirements 12
logic 12
regression 12
devils-advocate 8
security 8

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant