Repository navigation
七軸(A1/CV/SS/KG/M/IN/RT)跨頁和諧整合:共用 handoff 契約 + 共享狀態列 + 誠實接收端重驗 - #293
Conversation
新增 spec-to-done 用整合設計:跨頁 handoff 契約、共享狀態/證據列、 A1↔Review Room 既有交握延伸;不合併路由、不新增後端、不重裁 A1 3D 架構。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CyhkWS4Y4SZr1PGakMEKyF
依 superpowers writing-plans 規格,由 spec 2026-07-03-seven-axis-cross-page-harmony-design.md 產出逐 task TDD 實作計畫:共用 handoff.ts(CrossAxisHandoff build/parse)+ SharedStatusProvider 單一輪詢 + SharedStatusRail + 七軸 cross-link chip + CV 轉檔歷史 panel(GET /api/dev/conversions) + KG 真 session 聚合 + Review Room 候選 seed + browser E2E。零新後端、零新路由、零新 production dependency,diff 全落 web-viewer-sample/src/console/ 與 e2e/。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…a1-conv-link、Task7 覆蓋)
依 reviewer 發現逐項修入 plan(只改 plan 文件,不動其他檔;不刪需求):
- [buildability/completeness] Task 6 Step 1 測試改用 DOMParser 逐鈕查 `disabled`,
取代 `/data-testid="a1-link-minio"[^>]*disabled/` 正則。Btn(components.tsx:113)
屬性序 disabled 先於 data-testid 且 data-testid 為最後一個屬性,原正則永不 match
(即使鈕真的 disabled);Step 5「Expected: PASS」矛盾一併消解。
- [spec-alignment] §4.3 A1 → CV 列標「既有連結,補帶 source/id」但 plan 零覆蓋:
Task 6 新增 Step 4b 把 pages.tsx:604 的 a1-conv-link(`#/conv`)改成
`buildHandoff("conv",{source:"a1",job_id:convJobId??undefined})`,並補測 href 斷言。
- [spec-alignment] §4.2(硬性)/§12/§13 要求接收端一律以 ID 重驗、查無誠實 not-found、
禁靜默 fallback,但 plan 只做發送端。新增 Task 14「Receiver-side handoff
re-verification」:共用 useIncomingHandoff+IncomingHandoffBanner,對 M/A1/CV/SS/KG
五接收頁以已抓的權威資料重驗(零新後端 N2/N4);原 E2E 順延為 Task 15。
另於 Task 13 補測 parseReviewRoomHandoff 接受 conv/sessions/intake/runtime 非 a1 source。
- [completeness] Task 15(原 14)E2E 的 `a1-embedded-viewer` testid 全庫不存在,
toHaveCount(0) 真空通過、對 N3 零防護。改以真實存在的 review-room-viewer-host
(ReviewSessionViewerPane.tsx:313,僅 Review Room 渲染)做差異式閘,並註明 exhaustive
防線仍是 A1ViewerEmbed.test.tsx;另補 M→CV 落地後 conv-incoming-handoff 接收端斷言。
- [task-decomposition] Task 7 三加法只測 history panel,ledger/job 三個 chip testid
無斷言即進 commit。補一輪 chip 斷言(conv-ledger-minio/conv-job-session/conv-job-review
存在+導頁),Step 7 預期改 3 tests;並註明可等價拆兩 task。
同步更新 File Structure(新增 incomingHandoff.tsx)、Self-Review Checklist 與 OQ4 的
Task 14/15 交叉引用。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
修 reviewer 三項 blocker/major: Task 14(接收端重驗,completeness + task-decomposition major): - Step 4 把 A1/CV/SS/KG 四頁的散文指引改為可直接貼上的 code block, 每頁綁真實 in-memory state var(A1=minioObjects、CV=jobs/records、 SS=rt?.sessions.items、KG=shared.sessionsById)並附精確 render 行號錨點, M 也補上 <h1>(1608) 錨點;連 §4.3 硬性重驗鐵律於五頁一致。 - Step 1 receiving-pages 測試新增 A1(verified)/CV(verified)/KG(not_found) 三個 per-page 斷線斷言,補齊 imports;五頁均可獨立驗證,接錯 state var 或漏接即 npm test -- incomingHandoff.test.tsx 失敗(原本只斷言 M/SS)。 Task 15(Browser E2E,spec-alignment major): - 新增一支 §8 stitched walk-through 測試(M→IN→CV→A1→Review Room→回 A1 issue), 各 infra-heavy leg 用誠實 test.skip(同既有 chip 測試 pattern),不偽造。 - 修正 Self-Review Checklist 過度宣稱;補「§8 coverage & honesty」註記。 - 誠實查證:reviewer 建議引用的 VG-01(viewer-embed-a1-highlight.spec.ts) 與 a1-minio-governance-3d.spec.ts 於 a334e49 解耦後已 stale(斷言已移除的 a1-first-frame-evidence/a1-stage-matched/a1-highlight-3d),不得當覆蓋依據; 四格深度證據由 Review Room 產出、需 live Kit session、CI 誠實 skip(skip != pass)。 只動 plan 文件;testids 全經 pages.tsx / ReviewSessionViewerPane.tsx 查證為真。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…off) - 新增 handoff.ts:AxisKey/CrossAxisHandoff 型別、isAxisKey/buildHandoff/parseHandoff - buildHandoff target 收斂為 string(而非 AxisKey):既有 A1→Review Room pattern 用 "review" 當 target,屬 alias route 非七軸之一(spec N1),AxisKey 過窄會與 自身測試矛盾,故放寬(僅型別註記,行為不變) - PAYLOAD_KEYS 型別排除 "source" 避免 parseHandoff 寫入時的 TS2322 - 4 個 vitest 全綠;tsc --noEmit 維持 baseline(僅 1 個既有無關檔案錯誤,無新增) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rapper(GET /api/dev/conversions) 薄封裝既有 GET /api/dev/conversions(conversion service 側 job 歷史,與 coordinator ledger getConversionRecords 不同源);後端不改動(N2/N4)。新增 DevConversionRecord pass-through 型別 供 Task 7 消費。TDD:先寫失敗測試(getConversionsHistory is not a function)→ 補最小實作 → 通過; 順手修正測試檔內 afterEach(() => vi.restoreAllMocks()) 的隱式回傳型別(VitestUtils 非 void)觸發 的 tsc 錯誤,改用與既有 coordinatorClient.test.ts 一致的花括號寫法(行為不變)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… hook(§5) 新增七軸跨頁狀態共享層:useSharedStatus.ts 定義 SharedSessionEntry / SharedStatusSnapshot 型 別、EMPTY_SHARED_STATUS 預設值、SharedStatusContext 與 useSharedStatus() hook; SharedStatusProvider.tsx 是全 console 唯一對 GET /api/runtime/status 做 5000ms 定時輪詢的 地方(既有各頁自己的 mount-once fetch 不受影響),並用 getConversionRecords(100) 補轉檔佇列 深度(status ∈ detected/queued/converting 才計入)。GPU 節點欄位依 OQ3 恆為 null(未取得,非 偽造 0/0);stage_matched 依 §5.2 設計恆為 null。輪詢失敗時標記 stale=true、health="unknown", 不假裝資料仍新鮮。支援 value prop 供測試注入快照(跳過輪詢,測試 seam)。 TDD:先寫 SharedStatusProvider.test.tsx(3 案例:輪詢映射/失敗降級/注入不輪詢)→ 確認因缺模組 失敗 → 補最小實作 → 3 測試轉綠。驗證:npm run verify(build + 437 vitest + 10 struct-log 全過); 另用暫移新檔驗證 windowParentMessage.dom.test.tsx 既有 TS6133 warning 為 baseline 既存、非本次 引入。GitNexus detect_changes(staged) 確認 changed_files=3、risk_level=low,範圍與預期一致。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gap1(漏做,spec §5.2/§5.4):stale 原本只在 poll reject 時翻真,缺「超過 2× 間隔」的時間 watchdog。請求掛起不 reject 時(背景分頁節流/連線卡住,jsonGet 無 AbortController/timeout)poll 停在 pending 不進 catch,stale 會永遠停在上次 成功值 false,把過期資料當新鮮,違反 §5.4。新增以 updatedAt/Date.now() 為基礎、 獨立於 await 是否 settle 的 setInterval watchdog:last-known-good 超過 2×pollMs 即翻 stale=true(已 stale 或尚無成功 poll 則回傳 prev 不 churn),cleanup 一併 clearInterval。 gap2(測試未覆蓋既有分支):新增測試涵蓋 runtimeStatus 成功但 getConversionRecords 失敗的 inner catch,證實 conversionQueue 誠實降為 null、 外層 poll 不崩、runtimeStatus 半邊仍正確映射。 驗證:SharedStatusProvider.test.tsx 5/5 綠(watchdog 先紅後綠、以預期原因失敗); 全套 vitest 36 檔 439 測全綠;tsc 我方檔案 0 error(僅既有 windowParentMessage 無關 baseline TS6133)。 未動 coordinatorClient.jsonGet 的 fetch timeout:跨所有端點、屬前端凍結契約、 blast radius 大,且 watchdog 已足以滿足 spec stale 語意,故列為 advisory 不在本 task 動它。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
新增跨頁共享狀態列元件,讀 useSharedStatus() 單一真相來源,顯示 Active sessions/GPU/Health/轉檔佇列/資料時間五項指標。GPU 與轉檔佇列在值為 null 時一律渲染「未取得」(非假綠燈);health="unknown" 顯灰、不與 ok/fail 混淆;stale=true 時整列變暗且顯示「資料過期」,不把舊值當即時呈現。點擊 GPU/Health/Active sessions 指標經 buildHandoff() 導向對應權威頁 (#instances/#runtime/#sessions),沿用既有七軸 handoff 慣例。 CSS 僅視覺(.ec-statusrail 區塊,附加於 edge-console.css 檔尾),測試不依賴 樣式,一律鎖 data-testid。 驗證:SharedStatusRail.test.tsx 5/5 綠(先紅:找不到模組 ./SharedStatusRail →後綠);全套 vitest 37 檔 444 測全綠;tsc 僅既有 windowParentMessage.dom .test.tsx pre-existing 錯誤(未觸碰檔案,非本次引入)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Important #1:conversionQueue 在 ledger 超過回傳窗上限(後端 parseListLimit 100) 時,原本直接對被截斷的 recs.items 算佇列狀態筆數,會靜默低報真實佇列深度,操作員 會把低估值當真。比照 pages.tsx ledgerChipStatus 的 recordsIncomplete 模式: recs.count > recs.items.length(截斷)時退 conversionQueue=null(未取得),不臆測 (誠實鐵律 / §5.4)。 Important #2:aliveRef 原為跨 effect 世代共用的 useRef。effect 因 pollMs/value 變動 重跑時,新 effect 會把同一個 ref 撥回 true,使舊 effect 卡在 await 的 poll resume 後 誤判自己仍存活,重新 setTimeout 排下一輪——這條孤兒輪詢鏈只存在舊 closure、新 cleanup 清不到,與正確迴圈並存重複打 API(違反 spec §12「只建立一條輪詢」)。改成 effect-local 的 let cancelled=false 閉包旗標,每個 effect 世代各自一份;一併移除不再 使用的 useRef import。 新增兩個回歸測試(截斷窗退 null、effect 重跑不產生孤兒 timer),先紅後綠;既有 5 測 試維持綠,tsc 僅既有 baseline TS6133(windowParentMessage.dom.test.tsx,未觸及)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
原 afterEach 只做 removeChild(container) + restoreAllMocks + useRealTimers, 從未 unmount;且各 it() 用 const root 宣告在區塊內,afterEach 根本拿不到。 React 18 createRoot 的 effect cleanup 只在 unmount / deps 變動時觸發,單純把 容器移出 DOM 不會清,SharedStatusProvider 的 setInterval watchdog + setTimeout 下一輪 poll(spec §5.1 的 5000ms 自動輪詢)因此殘留在 worker event loop; watch 模式重跑會累加,且 restoreAllMocks 後殘留 timer 的下一輪會打到真的 coordinatorClient.runtimeStatus()。 比照同目錄 EmbeddedViewer.test.tsx 慣例修正:root 提升到 describe 作用域、 beforeEach 重置為 null、afterEach 改 async 先 await act(() => root.unmount()) 再 removeChild,在還原 mock / real timer 之前清掉 effect 註冊的 timer; render 呼叫比照慣例用 root! 非空斷言。 驗證:npx vitest run SharedStatusProvider.test.tsx 7/7 綠;npx tsc --noEmit 僅 剩既有 baseline windowParentMessage.dom.test.tsx:292 TS6133(不在本 diff); eslint 該檔 0 warning/error。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SharedStatusProvider 的 watchdog(輪詢掛起、從未 resolve/reject 的路徑)原本只把 snapshot.stale 撥 true,卻沿用上一輪成功的 health(例如 "ok")。SharedStatusRail 的 health 徽章文字與 health-* CSS class 都純由 s.health 推導,於是在資料已過期時仍渲染 綠字級別的 "ok",違反 spec §5.4「stale=true 不得呈現 last-known-good 為 fresh; 來源沉默 = unknown」。 修法:watchdog flip stale 時於同一次 setState 一併把 health 降為 "unknown",與 catch 分支及 EMPTY_SHARED_STATUS(兩者皆 stale=true 配 health="unknown")對齊;文字與色階 一次修正。 先補回歸測試(既有 watchdog 測試只斷言 stale,未斷言 health):斷言掛起超過 2× 間隔 (3100ms)後 health === "unknown",紅→綠。 驗證:SharedStatusProvider + Rail 12/12;full vitest 37 檔 446 綠;tsc 僅既有 baseline TS6133(windowParentMessage.dom.test.tsx,與本 diff 無關);eslint 3 檔 0;build 綠。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Important #1(SharedStatusProvider):jsonGet 無 AbortController/timeout,單次請求永久 掛起會讓 poll() 卡在 await、finally(唯一排下一輪的地方)永不執行,整條輪詢迴圈死亡 直到手動重整。watchdog 增設 liveness 復活:pollGen 標記每輪、inFlightSince 記錄當前 請求起始時間,in-flight 超過 2× 間隔即判定 wedged,disown 舊 poll(gen 不符→丟棄,不 重複排程 §12)並重啟新 poll,後端恢復後迴圈自癒。既有 watchdog honesty 與 orphan-loop 測試不動即綠。 Important #2(SharedStatusRail):健康三態(ok/degraded/unknown)原本缺 degraded 的回歸 測試。補 health="degraded" 斷言 health-degraded class 與字面值,防日後誤接 t() 翻譯或誤 併入 unknown 分支。 驗證:vitest 全 37 檔 448 test 綠(+2 新測);tsc 僅剩既有 baseline TS6133;eslint 改動檔 0 error。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
原「revives the poll loop after a request wedges」測試只驗 watchdog 持續重試 (statusSpy call count 增加),沒驗復活後 snapshot 真的恢復健康;原 mock 鏈 mockResolvedValueOnce(rt(1)).mockReturnValue(hang) 之後永遠回傳不 settle 的 hang。 改為 .mockReturnValueOnce(hang).mockResolvedValue(rt(2)),讓某次 watchdog 重啟的 poll 真正成功,前進數個間隔後新增斷言 stale===false / health==="ok" / activeSessions===2,把「復活後端已收到 fresh 資料」這半也鎖進回歸測試。 純測試改動,不動 production code(§5.1 Important #1)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…edStatusRail 七軸每頁共用同一份 runtime 真相(spec §5):EdgeConsole 頂層包一層 SharedStatusProvider(全 console 唯一輪詢),並在 ec-mainhead 的 FlowBar 之後插入 SharedStatusRail,依目前 hash 頁面算出 railAxis(七軸直接對映; #gpu/#review 併入 runtime;其餘非七軸頁預設 a1)供狀態列高亮脈絡。 現有 header/nav/main/aside/footer JSX 內容不變,僅做外層包裹與單行插入。 新增 EdgeConsole.sharedstatus.test.tsx:驗證 shared-status-rail 出現在 DOM 中,且整個 console 只有一條 runtime/status 輪詢(不因多頁掛載而 重複打 API)。 驗證:npx vitest run(EdgeConsole.sharedstatus 1/1;console 全套 33 檔 407/407 綠,含既有 EdgeConsole SSR smoke 無回歸)、npm run build 成功、npx tsc --noEmit 僅餘與本次改動無關的既有 windowParentMessage.dom .test.tsx TS6133(已用 git stash 比對 baseline 確認為既有問題)、npm run test:struct-log 10/10 綠。GitNexus impact(EdgeConsole,upstream)=LOW risk、0 upstream caller;detect_changes(staged) 僅回報 EdgeConsole.tsx 三個符號,經 diff 核對 usePageHash/read 為新增 import 造成的行號位移 雜訊、非真實行為變更。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- A1 交付面板補兩顆 chip:回看 MinIO 來源物件(帶 minio_key)、跳
Session 管理檢視此 session(帶 session id);目標 id 不存在時誠實
disabled,不製造無效跳轉。
- 既有 a1-conv-link 錨點升級為 buildHandoff("conv", {source:"a1",
job_id}) 產生的 #conv?source=a1[&job_id=...],取代舊的裸 #/conv
(spec §4.3 A1→CV 列:既有連結,補帶 source/id)。
- 新增 A1CrossLinks.test.tsx:SSR smoke test(renderToString +
DOMParser,無 mock),驗證兩顆 chip 存在且未選取時 disabled、
a1-conv-link href 帶 source=a1。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
review 發現:A1CrossLinks.test.tsx 只驗證未選取時 disabled 與 caption
文案,從未驗證「已選取 → 點擊 → window.location.hash 內容正確」的核心
行為,無法擋 buildHandoff 目標軸字串打錯(如 "sessions" 誤打成單數
"session",EdgeConsole 無此 case 只會靜默 fallback 到 HomePage)或
minio_key / session 兩參數寫反。
在 A1ViewerEmbed.test.tsx 復用既有 renderA1 / selectSession / act /
flush harness,新增一個 client render 情境:選 MinIO 物件 + 選 review
session 後分別點擊 a1-link-minio / a1-link-sessions,解析 hash 斷言
且未交叉洩漏。已以暫時注入單數 "session" typo 驗證此測試會 fail
(line 210 startsWith("#sessions?"))確認有回歸擋防力後還原。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ink chips
- 新增 conv-history-panel:讀既有 GET /api/dev/conversions
(coordinatorClient.getConversionsHistory,Task 2 已建),呈現
conversion service 側 job 歷史 pass-through;prov="artifact"(形狀非
本專案定義);載入失敗誠實顯示「未取得」而非假空表;空陣列另顯「非
錯誤」提示。history/historyErr 與既有 records/recErr 各自獨立
useEffect,不污染既有 ledger/ifc-ready 載入時序。
- Ledger 列 Control cell 補 conv-ledger-minio-<idem> chip:object_key
存在才掛(evidence-typed),導到
buildHandoff("minio",{source:"conv",minio_key,conversion_id});與既
有「觸發轉檔」鈕互不排斥、可同列並存。
- Ifc-ready job 列 session cell 補 conv-job-session-<jobid> /
conv-job-review-<jobid> 兩顆 chip:review_session_id 存在才掛,分別
導到 #sessions?source=conv&session= 與
#review?source=conv&session=;接收端依 spec §4.2 重驗 id,不靜默
fallback。
- 新增 ConversionHistory.test.tsx(3 tests):history panel 正常/失敗
兩態、ledger+job 列三顆 chip 存在性與點擊導覽(hash 含
source=conv&session=)。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
created_at 經 Task7 實作查證,後端 GET /api/dev/conversions 三條回應組裝 分支結構性從不回傳(僅存在內部 job dict 供排序),對真實資料恆為 undefined。 使用者裁決:換成 source_ifc_filename(setdefault 保證存在,顯示轉檔的是哪個 IFC 檔,較 created_at 更有用)。同步修正 Task2 段落的 interface 說明, 避免文件與程式碼不一致。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
轉檔歷史 panel(GET /api/dev/conversions pass-through)先前實作沿用 DevConversionRecord.created_at 顯示第三欄,但後端 _conversion_result_list_item (conversion_authority.py:661-679)結構性從不對此欄位賦值——只存在內部 job dict 供排序;序列化進 API 回應的只有 setdefault 保證存在的 source_ifc_filename。故 created_at 對任何真實資料恆為 undefined,畫面永遠 顯示「—」。plan 檔已在 770a10a 訂正描述,本 commit 補上對應的程式碼修正。 - coordinatorClient.ts:DevConversionRecord.created_at → source_ifc_filename - pages.tsx:轉檔歷史表頭與儲存格改讀 source_ifc_filename - ConversionHistory.test.tsx:mock 加入 source_ifc_filename 值並斷言渲染 (先跑此測試確認因讀 created_at 而失敗,套用修正後再轉綠) 驗證:ConversionHistory.test.tsx(3)+ ConversionSchedulingPage.test.tsx (39)+ coordinatorClient.conversions-history.test.ts(1)全綠;全套 vitest 40 檔 455 測試全綠;tsc --noEmit 僅餘既有無關檔案(不在本次改動 範圍)1 個 pre-existing 錯誤;vite build 成功。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
quality 發現:ConversionHistory.test.tsx 只驗 conv-job-review 一顆 chip 的 click→hash,另兩顆(conv-ledger-minio / conv-job-session)只驗 DOM 存在;且 spec §12「目標 ID 缺 → chip 消失」方向零覆蓋,日後若把 truthy gate 寫反無測試可抓。 - 正向:三顆 chip 各驗一次 click→hash(minio_key 經 parseHandoff round-trip 斷言 CJK/斜線精確還原;session 帶 source=conv)。 - 反向(新測):object_key / review_session_id 皆 null 時三顆 chip 皆不 render, 且列本身仍 render(證明是 chip 條件隱藏、非整列消失),與正向配對夾住條件。 純測試改動不動 production;vitest 40 檔 456 綠、tsc 僅餘既有無關錯誤、 eslint 改動檔 0 error。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ink chips SessionManagementPage 的 Active sessions 動作欄在既有「結束 session」鈕旁 補三顆證據型 chip(session-link-instances-<id> / session-link-review-<id> / session-link-a1-<id>),各自用 buildHandoff 帶 source=sessions + session id 導向 #instances / #review / #a1。SS 頁維持自己 mount-once runtimeStatus 抓取不變(N6),不耦合 useSharedStatus;接收端依既有 §4.2 規則自行重驗 session id。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nk chip KG(#instances)原本 100% 靜態、無任何 fetch;現在讀 useSharedStatus() 呈現真 session 聚合(asbuilt,data-testid=kg-live-aggregate,含每個真 session 的 #sessions 導覽鈕), 並在 demo Node snapshot 表首列補一顆導向 #sessions?source=instances 的 chip (data-testid=kg-demo-link-sessions)。demo 表本身維持 prov="demo" 不動、不假裝接真 (N5);GPU per-node 遙測仍未取得(OQ3)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sessionsById(spec §5.2)是全量 session 表(不分狀態,供跨頁 ID 查找重用), 且 coordinator 從不刪除 session(只 active→closing→closed,永遠保留)。原 liveIds = Object.keys(sessionsById) 未過濾狀態,把 closed/closing 的過期 session 在標題「即時 session 聚合(真實)」、prov="asbuilt" 的區塊渲染成可點的 kg-session-link 導覽鈕,且緊鄰只算 active 的「使用中 session 數」,並蓋掉誠實 空狀態文案——把過期 session 假裝成真實可操作(違反 N5 誠實鐵律)。 改為只取 status==='active' 的 session 當即時連結,與相鄰 activeSessions 聚合 一致;closed 過期 session 回到誠實空狀態。新增兩個回歸測試涵蓋 activeSessions=0 卻殘留 closed session、以及 active/closed 混雜兩情境。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_key 往返) M 頁(MinioDataPage)obj.role === "source_ifc" 列在既有觸發轉檔鈕後補兩顆 evidence-typed chip:minio-link-conv-<idk> 導向 #conv?source=minio&minio_key=...、 minio-link-a1-<idk> 導向 #a1?source=minio&minio_key=...,皆帶編碼後的 obj.key (可能含中文,如 270專案/建築/v07/模型.ifc)。接收端(CV/A1)依 §4.2 重驗 minio_key。 新測試 MinioCrossLinks.test.tsx 驗證中文 key 經 buildHandoff → URL hash → parseHandoff 完整往返不失真(OQ4 決定性 spike)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MinioCrossLinks.test.tsx 第一個測試(navigates to #conv)原本只斷言 parseHandoff
解出的 source / minio_key,未驗目的地路由前綴。因 parseHandoff 只解析 query、不看
target 字串,若 onClick 誤寫成 buildHandoff("a1", ...) 此測試仍會通過,測試名稱與
實際斷言不對稱。補一行 window.location.hash.startsWith("#conv?") 斷言,與同檔 #a1
測試(line 58)及 Session/KitGpuFleet 姊妹測試家族的目的地前綴檢查對齊。
生產碼本身正確(pages.tsx:1844 buildHandoff("conv", ...)),純測試嚴謹度補強,
非修 live bug。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IntakePage 的 intake job 表新增「跨頁」欄:intake-link-conv-<jobid> 永遠顯示, 導向 #conv?source=intake&job_id=...;intake-link-review-<jobid> 僅在 j.review_session_id 存在時顯示,導向 #review?source=intake&session=... (無 session 時不畫假 nav,符合 §4.2 誠實鐵律)。皆用既有 buildHandoff 產生 hash,接收端(CV / Review Room)依既有規則自行重驗 ID。 新測試 IntakeCrossLinks.test.tsx:驗證兩顆 chip 皆出現且點擊後 hash 正確; 以及無 review_session_id 時 review chip 不渲染(no fake nav)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IntakeCrossLinks.test.tsx 原本只斷言 intake-link-review-job_1 存在於 DOM (not.toBeNull),從未點擊、從未驗證產生的 hash;#review chip 的 onClick (session=…)因此無任何行為防護,若複製貼上失手把 session 換成 job_id,CI 會 靜默通過。改為比照同檔 #conv chip 與 sibling ConversionHistory.test.tsx 的 既有 pattern,逐一 click→斷言 hash 含 #review?source=intake 與 session=review_session_a。 已 red-verify:暫時把 pages.tsx 的 session 改成 job_id,新斷言如期以 「expected '#review?source=intake&job_id=job_1' to contain 'session=review_session_a'」失敗,還原後 2/2 綠。 Verify:npx vitest run src/console(39 files / 428 tests 綠)、tsc --noEmit 本 diff 0 新增錯誤、eslint 該檔 exit 0、vite build 成功、git diff --check 乾淨。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RT 值班視圖(#runtime)新增「跨頁 session 連結」Panel,列出 rt.sessions.items 每個 session 並提供三顆證據型 chip: - rt-link-sessions-<id> → #sessions?source=runtime&session=<id> - rt-link-review-<id> → #review?source=runtime&session=<id> - rt-link-instances-<id> → #instances?source=runtime&session=<id> 複用既有 buildHandoff(Task 1)與 CoordinatorPage 既有 rt state, 不改 CoordinatorGovernanceTabs(endpoint/role-keyed rows 加 chip 不安全), 不動四分頁路由(D2-A′維持)。RT 的共享狀態消費由全域 rail(Task 5) 已滿足,本 Panel 只補 session 層級的跨頁導航。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CoordinatorPage 的「跨頁 session 連結」Panel(prov="asbuilt")原對 GET /api/runtime/status 回傳的全量 rt.sessions.items 直接 .map(),不分 active/closing/closed 都渲染三顆滿血可點鈕。 coordinator 從不刪除 session(只 active→closing→closed,永遠保留),此表隨時間無界成長, 把已結束 session 假裝成即時可操作——與同分支前一 commit 0860a54(task#8)剛修的 KG 即時 聚合同型 N5 違規。 比照 0860a54:以 live = s.status === "active" 判定,對「在 Review Room 開此 session」 「Kit / GPU 機隊」兩顆即時操作型鈕在非 active 時 disabled + 誠實 caption/title(session 已結束); 「Session 管理」是 lifecycle 全量治理視圖,對已結束 session 給連結語意合理,保留 enabled。 新增兩個回歸測試:closed-only(Review/KG disabled、SS enabled)與 active+closed 混雜 (只 active 列可操作);既有 active 導航測試不變。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 15 對抗複驗兩項未閉合 finding(皆為測試檔字串/斷言修正,不動測試邏輯): - A1→Review Room leg 的 test.skip 理由與 §8 COVERAGE & HONESTY NOTE 原本把不可達 歸因成缺真實 IFC rule-run 環境/fixture,但這是結構性死路:a1-open-review-room 需 selectedSession 非空且 state.failed[0] 存在(a1ReviewRoomHandoffReason, pages.tsx:256-262;state 為純前端 useReducer, pages.tsx:283,306),本測試從不驅動 選 session 或 rule-run,故無論接什麼環境都 100% skip。改成準確描述為測試範圍限制, 非環境/fixture 限制(依原意不新增瀏覽器操作步驟,避免最後一個 task 增加自動化脆弱點)。 - 三處 data-handoff-status 斷言(M→CV 及 §8 M→CV/IN→CV leg)原本只接 /verified|not_found/,未算進 indeterminate。CV verify predicate(pages.tsx:908-922) 在 ledger 超過 getConversionRecords(50) 查詢窗時誠實回傳 indeterminate (incomingHandoff.tsx:9,49),屬誠實鐵律正確第三態;長期 ledger 累積超過 50 筆會使 斷言間歇 flake。三處改為 /verified|not_found|indeterminate/ 並加註原因。 驗證:npx playwright test --list 解析成功(5 tests);detect_changes(staged)=0 changed symbols / 0 affected processes / low risk;git diff --cached --check 乾淨。未跑瀏覽器 E2E(需 live coordinator,本次僅測試檔字串/斷言修正)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gap 1(必修):Step 2「services up 後跑一次」對 HEAD 從未執行過(前三個修改測試邏輯的 commit 皆自述未跑真 E2E,僅做 eslint / playwright --list 等靜態檢查)。本次對分支隔離 coordinator(:8005)補跑: - 以 VITE_COORDINATOR_API_BASE=http://127.0.0.1:8005 重建 dist-ui,起隔離 branch coordinator(PORT=8005,不碰部署區 :8004)。 - E2E_COORDINATOR_BASE_URL=http://127.0.0.1:8005 playwright test cross-axis-handoff.spec.ts → 2 passed(shared-rail axis sweep、A1 no-embed / Review Room not-started) + 3 honest skip(M→CV、SS→Review、§8 walk-through;空 MinIO / 無 active session = not observed,skip != pass)。 - screenshot(cross-axis-rail-runtime.png、cross-axis-review-not-started.png)落 artifacts/e2e/。 - 已知界線:HEAD 的 CV load-race waitForResponse 守門與 indeterminate 斷言,只有在具真 MinIO fixture + ledger >50 筆時才會被實際走到;本環境無,屬 not observed,非本 additive spec 範圍(誠實鐵律,未偽造)。 Gap 2(次要):line 55 註解引用 ReviewSessionViewerPane.tsx:313 過時,該 testid (review-room-viewer-host)實際落在 :329(自 7b4f6a6 起即有偏移,非本次退化),改正行號。 驗證:npm run verify 綠(build + 504 unit + 10 struct-log);eslint 該檔 0 warning; detect_changes(staged) changed_symbols=[] risk=low(僅註解,無 symbol 變更)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…畫面,非斷言嚴謹度) quality Important #1:三處 data-handoff-status 斷言用 /verified|not_found|indeterminate/ 寬鬆 regex, 接受 CV 載入前的 transient not_found flash(records=[] & recordsTruncated=false → verify 回 false → not_found;pages.tsx:865,904,908-922 + incomingHandoff.tsx:29-30)。因此 `await recordsSettled` 不改變 toHaveAttribute 的 pass/fail——真正效果是讓後面的 screenshot 拍到 CV 終態、而非 pre-load flash。 前次 commit(7d1b467/3a72b46/08b8327)措辭主張此守門是在「保護斷言讀終態不誤過」,實際只保護 screenshot 穩定畫面,會誤導後續維護者高估斷言的迴歸保護力。本次改動: - 只改註解、不動斷言 regex(寬鬆 regex 在 fixture-less 環境是誠實的三態終值集合;收斂成 verified 會 flaky/說謊)。 - 校正 arm-site 與三處斷言周邊註解:明講 wait 買的是 screenshot frame、非更嚴斷言;標明此斷言為 wiring smoke test(banner 掛上 + CV 重驗成非-none 誠實態),per-input 鑑別(含 truncation→indeterminate)由 incomingHandoff.test.tsx 單元測試負責。 - 移除會漂移的絕對行號自我引用,改用符號式指涉。 驗證:npx eslint(EXIT=0)、npx playwright test --list(5 tests 全編譯)與 baseline 一致;git diff --cached --check 乾淨;detect_changes(staged) changed_symbols=[] risk=low(comment-only,無 production symbol)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… A1 leg 不可達死碼) Important #1:page.waitForResponse 在 HTTP response(header 收到)當下 resolve,不等 body 讀取 + setRecords + React 重渲染;且 CV 的 transient 與 terminal 皆可能是 not_found、無 DOM signal 可辨別, 故 expect.poll 不可行、waitForTimeout 屬 anti-pattern。改採誠實作法:把 4 處「screenshot 呈現終態」 的過度承諾改弱為 best-effort/likely,明說不保證重渲染已入 DOM(isolated M→CV arm+inline、s8 M→CV arm、IN leg)。斷言與 pass/fail 完全不變。 Important #2:a1-open-review-room 為 disabled={Boolean(a1ReviewRoomHandoffReason(...))}, selectedSession="" 時 reason 恆非空 → canOpen 恆 false → 原 test.skip(!canOpen) 之後的 183-203 行 為不可達死碼(任何環境、任何 fixture 皆到不了)。依「刪 code 拿到一樣結果視為 win」移除死碼,改以可達 且確定的 toBeVisible + toBeDisabled 斷言作 A1 終點(present-but-disabled 為真實可驗狀態),並同步修正 §8 誠實註解、測試標題與 screenshot 名稱,避免死碼冒充活路徑。 驗證:eslint 0 warning;playwright --list 仍 5 tests;isolated tsc(shim node global)exit 0; detect_changes(staged) changed_symbols=0 / risk=low(純測試檔);git diff --cached --check 乾淨。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- [task15-r2-important1] §8 walk-through 的 M leg 由 test.skip(!mHasConv) 改為軟性
if (mHasConv){...}(比照同測試 IN leg 的 pattern)。Playwright 在 test body 內呼叫
test.skip 且條件成立時會立即中止整支 test,原寫法在無 MinIO fixture(常態)時會於
M leg 就整支中止,後段 A1 Review-Room CTA 斷言(toBeVisible/toBeDisabled)從未執行,
與同測試 coverage note「present-but-DISABLED … holds in ANY environment」矛盾。改軟性
if 後 A1 leg 在任何環境皆可達;同步把兩處 coverage note 的 test.skip 措辭更新為
soft-gate,避免註解與程式碼互相矛盾。
- [task15-r2-important2] 在 A1 CTA 兩條斷言前補誠實註解:此斷言邏輯已對 pages.tsx 逐行
核對(交付/Deliverables Panel 無條件渲染 pages.tsx:651、selectedSession 初值 ""
pages.tsx:306 且不從後端還原 → a1ReviewRoomHandoffReason 恆非空 → disabled),但尚未
經真實瀏覽器對運行中 coordinator 執行;live 驗證留待 spec-to-done P4 browser evidence
階段,不在此重複搭分支 coordinator。未新增或修改任何測試邏輯。
僅改動測試檔(單一測試控制流 + 註解),未動 production symbol;eslint 與
playwright test --list 皆綠。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…A gate 必修 gap:§8 stitched walk-through 先前把 A1 leg 之後的 Review Room 與回 A1 建 Issue 兩段整段砍除(commit 5c0bb46),只斷言 a1-open-review-room present-but-disabled 就結束, 未涵蓋需求 Key Coverage Point #6 明列的 M→IN→CV→A1→Review Room→A1 issue 六段;需求僅把 deep Kit evidence(first_frame/stage_matched/datachannel/highlight)列為無 live session 可 誠實跳過的例外,並未豁免 Review Room 可達狀態與 A1 issue 段落。 修法: - §8 walk-through 現在真正走完 reachable spine:A1 CTA→(容錯)→Review Room kit-not-started→回 A1 斷言 a1-step-issues present。Review Room 段經 CTA 自身的 fixture-less target #review?source=a1(pages.tsx:711)到達,任何環境可達、不偽造 rule-run。 - 修正脆弱無條件 toBeDisabled:改成本檔其他 leg 一致的 waitFor→branch 容錯 pattern—— CTA enabled(部署餵入 rule-run fixture)則點擊走真 A1→Review handoff、斷言 #review?source=a1 URL;disabled(fixture-less)則斷言 present-but-disabled 再續走 stitch, 不再會因未來 CTA 變 enabled 而硬 FAIL。 - deep Kit evidence 四點移到專用 test,以 waitFor→test.skip 提供誠實 not-observed runtime 訊號(skip != pass, N7),對齊需求 Steps「honest test.skip for non-observed cases」, 不再只靠測試標題字串/註解揭露。 驗證:eslint exit 0;playwright --list = 6 tests(原 5+1);對 live coordinator :8004 跑兩支 §8 測試:reachable spine PASSED(21.1s,實走 A1 CTA disabled→review-room-kit-not-started→ a1-step-issues,截圖 s8-02/03/04)、deep Kit SKIPPED(誠實 N7 訊號);detect_changes(staged) changed_symbols=0 / changed_files=1 / risk=low(純測試檔);git diff --cached --check 乾淨。 已知風險::8004 為部署區(main)build,缺本分支新 testid(shared-status-rail/a1-link-sessions) 故同檔 Test 1/3 於該環境失敗——與本次改動無關;§8 兩支用的是 pre-#286 已部署 testid 故有效受測。 分支隔離 stack(:8005)完整 live run + 真 rule-run 使 CTA enabled 之 handoff 串接仍屬 P4。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…view 補重驗斷言) Important #1(§8 deep Kit evidence chain 永久死 skip): 原測試 goto #review?source=a1 後直接等 review-room-viewer-host,但該 host 只在 activePrimaryLease 為真時掛載,而 lease 唯一寫入點是 claimPrimary()、唯一呼叫處是手動啟動鈕 onClick(ReviewSessionViewerPane.tsx:185-211,295,324,329)。無 session、無點擊 → 永遠逾時 → test.skip 在任何環境恆觸發,即便接上真 Kit GPU 也永不亮。改為從 #sessions 取真 active session(帶 session-terminate 鈕的列,pages.tsx:1497)→ 手動 attach → 逐關 honest N7 skip (無 active session / 鈕 disabled / 未掛載)→ 真 WebRTC frame 落地才斷言 first_frame + datachannel_ready(onFirstFrame)。stage_matched / highlight_ack 誠實標為需 A1 rule-run handoff payload、本路徑不帶,僅截圖。 Important #2(SS→Review 標題稱 re-verified 但未驗重驗): 原測試只驗 URL + review-room-kit-not-started,而後者只看 !activePrimaryLease (ReviewSessionViewerPane.tsx:324),任何 id 恆顯示、無法證明接收端重驗。改為選 active session、由 URL 擷取真 session id,斷言 runtime-evidence 區塊回填該 id(擋空字串/參數打錯 wiring regression)且 runtime session=observed(sessionObserved 真源, ReviewSessionViewerPane.tsx:119,314)。 兩處改用 active(非 terminating)列,因 store.list() 回傳含 closed 全量、依 updated_at desc(sessionStore.ts:70-79 / app.ts:2753 未過濾),.first() 常抓到剛關閉的列,Review Room 會誠實回 not_listed 而假失敗。 驗證:eslint 0 / playwright --list 6 tests / 隔離 tsc --noEmit(process shim)0。 未跑:live coordinator 真瀏覽器(本環境無 live Kit GPU,測試 honest-skip,非失敗)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…primary lease 收尾) Important #1(啟用競態假性 skip):deep Kit 測試以一次性 .isEnabled() 快照判斷 review-room-manual- start 可否 attach,但其 disabled gate(!viewerOrigin || !sessionObserved)依賴元件 on-mount 只跑一次、 不輪詢的 runtime/status fetch(ReviewSessionViewerPane.tsx:289,124-142)。按鈕無條件掛載、toBeVisible 幾乎立即通過而不等 fetch resolve,快照可能讀到 fetch 前的 disabled,把真有 live Kit session 的情境假性 skip 掉。比照本檔 M/SS leg 既有慣例,改用會自動重試的 expect().toBeEnabled({timeout}).then(true/false)。 Important #2(佔用真實 session primary lease 無收尾):測試對 live Sessions 表挑到的「真實 active session」claimPrimary() 佔其 primary viewer lease,全程無釋放;最壞情況真人操作員近 45s(lease TTL, viewerLeaseStore.ts:78)無法 claim 自己 session 的 primary 檢視。新增 describe 級 test.afterEach,用 獨立 request fixture 主動 POST release endpoint 歸還 lease(比照 VG-01 收尾模式;但只 release lease、 不 close 這條真實共享 session,以免破壞真人 session — VG-01 明列此跨套件干擾陷阱)。lease_id/lease_token 於點擊 manual-start 當下自 claim response 擷取(token 僅 claim 回應 includeToken 暴露, app.ts:1185)。 僅改單一測試檔 web-viewer-sample/e2e/cross-axis-handoff.spec.ts,無 production code 變動。 驗證:npx eslint(exit 0)、npx playwright test --list(6 tests 正確解析)、gitnexus detect_changes (scope=staged:1 檔、0 production symbol、0 affected process、risk low)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- waitForResponse(?limit=50) 三處(recordsSettled/convRecordsSettled/inRecordsSettled)補 .catch(() => null):讓註解宣稱的 best-effort 名實相符,15s 內無回應不再拋例外阻斷後面的 banner/attribute 斷言(比照同檔 claimSettled 既有寫法) - §8 lifecycle walk-through 補 test.setTimeout(180_000):fixture 齊全時測試體會疊加約 10 個網路等待,逼近/超過 playwright.config.ts 的 60s 全域上限 - §8 deep Kit test.setTimeout 300_000 → 450_000:逐步 timeout 加總約 310_000ms 原本零緩衝甚至超標,比照 viewer-embed-a1-highlight.spec.ts:46 的 2 倍緩衝慣例留出餘裕 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
三處 CV 接收端斷言原本只驗「有 handoff 且非 none」(toHaveURL + banner 可見 +
data-handoff-status 三態),未驗 chip 真的把 id 帶到 URL、receiver 真的 surface
出該 id;與同檔 SS→Review 已加固的模式不一致。因 minio_key/job_id 在 handoff.ts
為 optional(buildHandoff:39 丟空值、parseHandoff:51-53),chip 漏帶時仍得非 null
handoff,CV verify 落到 not_found(pages.tsx:917/909,921),與真實 ledger miss
外觀相同,原斷言完全偵測不到此 wiring 迴歸。
比照 SS→Review 既有 idiom(new URLSearchParams(...).get(k) + not.toBe(""))加固:
- M→CV(獨立)/§8 M leg:擷取 URL hash 的 minio_key 斷言非空 + banner toContainText(minioKey)
- §8 IN leg:擷取 job_id 斷言非空 + banner toContainText(jobId)
- §8 A1 CTA canOpen 分支:擷取 session 斷言非空(sender half;此分支目前 dead-in-practice,
receiver 端重驗由 SS→Review 測試的 evidence 斷言擁有)
banner toContainText 依 handoffIdText(incomingHandoff.tsx:35)surface id,與 verify
狀態正交(not_found banner 仍含 id),不影響既有三態斷言。純新增斷言與註解,無 production
code 變動。
驗證:npx eslint e2e/cross-axis-handoff.spec.ts --max-warnings 0(exit 0)、
npx playwright test --list(6 tests 全解析)、git diff --cached --check(乾淨)、
detect_changes staged(1 file、0 production symbols、low risk)。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
reviewer Important 發現:cross-axis-handoff.spec.ts 行176 的 test.setTimeout(180_000)
註解宣稱「~10 個循序 network waits、no measurement needed、3× headroom」,但實測
fixture-full 情境下該 test body 有 14 個有限逾時的循序 blocking waits:
M leg 70_000 + IN leg 70_000 + A1/Review-Room/A1-issue 70_000 = 210_000ms,
已超過原 180_000ms 預算(方向性低估);且 5 個 page.goto(config use{} 未設
navigationTimeout)與 3 張全頁截圖的時間未計入,retries:0 下一次「慢但會過」的
run 就會硬 flake——正是此 timeout 想防卻沒防足的失敗類型。
修法:比照同檔 deep-Kit test(行311)的實測預算做法,將 test.setTimeout 上調至
360_000(約 summed caps 的 1.7×,對齊 viewer-embed-a1-highlight.spec.ts:46 的
360_000 既有慣例),並改寫註解逐項列出 14 個 waits 的加總依據,移除
「no measurement needed」。純測試檔註解+字面值變更,無 production symbol 異動。
驗證:npx playwright test cross-axis-handoff.spec.ts --list(6 tests 正常解析);
git diff --cached --check 無 trailing whitespace;gitnexus detect_changes
(scope=staged)= 0 changed symbols / 0 affected processes / low risk。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
四項對抗複驗 findings: 1. p5-critic honesty regression(CRITICAL):接收頁 verify 對「handoff 缺本軸 欄位(sender 本來就不帶)」與「查過、真的沒有」不分,一律 fall through 回 false→假 not_found(警示紅字「查無」)。新增第三態 not_applicable(中性、 不掛 ec-warn-note、不說查無),修正三條已重現路徑:SS→A1 帶真實 active session(最嚴重,原本對使用中 session 假報查無)、a1-conv-link 預設無 job_id、KG demo-row 無 session;KG/M 同類 fall-through 一併對齊為 not_applicable。 新增 4 條回歸測試(1 hook 探針 + 3 頁面路徑)鎖住此行為。 2. p5-e2:補 CV 頁 minio_key 接收端重驗單元測試(verified 含中文 key / not_found / indeterminate),比照 A1 既有模式,鎖住原本零覆蓋的回歸風險。 3. p5-e3:deep-Kit E2E first_frame 180s 逾時後多讀一次 runtime/status 的 kit_instance_bindings status,若該 session 的 binding=failed 則 skip 訊息 標「Kit 已配置但 WebRTC/stage pipeline failed」,別於「環境無 Kit」。純加法 讀取既有欄位、best-effort try/catch,未動 ReviewSessionViewerPane.tsx。 4. p5-spec-drift:A1→M「MinIO 來源」chip as-built 送 minio_key(key-level 更 精確)而非 spec §4.3 範例的 prefix。於 spec §4.3 表下註、A1 chip、M prefix 分支文件化此既知差異(minio_key 本列於型別,屬合規選擇);prefix 收件分支 +其單元測試保留為未來「純資料夾回看」能力。 驗證:web-viewer-sample vitest 全 511 passed(含新增 7 條);tsc --noEmit 改 動檔零錯;Finding 1 走 red→green;e2e 經 playwright --list 編譯通過(deep-Kit 本環境無 live Kit,誠實 skip,未跑到 enriched 分支)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
incomingHandoff.test.tsx 三支 CV minio_key 測試改用 waitFor 輪詢取代固定 2-tick: - verified(中文 key,p5r2-critic-cv-miniokey-test-flaky):loadRecords 是與 load 獨立的 useEffect async 鏈,2-tick 排不乾時載入中的空 records 也顯 not_found,會閃 AssertionError(expected not_found to be verified)。改輪詢等 banner settle 成 verified。 - not_found(p5r2-critic-cv-miniokey-notfound-weak-assertion):先輪詢等 ledger 列 (idempotency_key mw_r)反映到 render 再斷言,證明 loadRecords 接線完整+已反映,區隔 「已載入且查無」與「fetch 從未發生/未反映」,修掉「兩種都會過」的弱斷言(實測:severing loadRecords 後本測試由通過轉為失敗,前為通過)。 - indeterminate(truncated)姊妹測試:同款輪詢,避免固定 tick 誤讀載入中的 not_found。 純測試改動,未動 production code(pages.tsx 驗證後回復 pristine)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rebase 到 origin/main 後,main 的 A1 3D decouple 工作(a334e49)為 ReviewRoomHandoff 新增 mappingInformationStatus/mappingIssueCode/ mappingIssueCount 三個欄位。本分支 task#12 新增的 ReviewSessionViewerPane.crosslinks.test.tsx 建構的 handoff stub 物件 未帶這三個欄位,vitest 因不做完整型別檢查而未攔到,tsc --noEmit 才抓到。 補三個 null 值(與物件內其餘診斷相關欄位一致,此測試不涉及 mapping 診斷情境)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
真實隔離 coordinator(:8005)+ 真實 IFC 轉檔(洲際好宅/給水,job stream_conv_20260703090757_89c87388)+ 真實 seeded review session (review_session_0e4ee1079ee1)取得的 vertical slice 證據:4 passed、 2 honest skip(MinIO 憑證缺席、deep-Kit GPU 影格未觀測)、0 failed。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughWalkthroughThis PR implements a frontend-only seven-axis cross-page handoff system: URL-hash encoding/decoding, shared runtime status polling and display, incoming-link re-verification, and cross-link chips across the console pages, with specs, tests, and Playwright evidence. ChangesSeven-Axis Cross-Page Harmony
Estimated code review effort: 4 (Complex) | ~75 minutes Sequence Diagram(s)sequenceDiagram
participant SourcePage
participant URLHash
participant TargetPage
participant coordinatorClient
participant SharedStatusProvider
SourcePage->>URLHash: buildHandoff(target, {source, ids...})
URLHash->>TargetPage: navigate to `#target`?source=...&id=...
TargetPage->>TargetPage: parseHandoff(hash)
TargetPage->>coordinatorClient: fetch authoritative data
coordinatorClient-->>TargetPage: data / loading / error
TargetPage->>TargetPage: useIncomingHandoff verify(data)
SharedStatusProvider->>coordinatorClient: runtimeStatus() / getConversionRecords()
coordinatorClient-->>SharedStatusProvider: snapshot inputs
TargetPage-->>SourcePage: IncomingHandoffBanner or cross-link navigation state
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Review Agent Summary
Blockers
Warnings
Validation Commands
Checks
Human Review Notes
|
There was a problem hiding this comment.
Pull request overview
This PR adds a purely frontend (N6) cross-page integration layer to the web-viewer-sample console so the seven axes (A1/CV/SS/KG/M/IN/RT) share a common navigation and status contract. It introduces a reusable handoff contract, a single-source shared status rail, evidence-typed cross-link chips per axis, and an honest receiver-side re-verification state machine (verified / not_found / indeterminate / not_applicable). No backend services, routes, or the A1/Review-Room 3D adjudication are changed.
Changes:
- New shared primitives:
handoff.ts(encode/decode#target?source=…hashes),useSharedStatus/SharedStatusProvider/SharedStatusRail(single 5s poll of/api/runtime/statuswith a self-healing watchdog and honest未取得/stale rendering), andincomingHandoff.tsx(four-state receiver re-verification against real backend data). - Per-axis cross-link chips and receiver predicates added across
pages.tsx(A1/CV/SS/KG/M/IN/RT), plus a conversion-history panel backed by the existingGET /api/dev/conversions(newgetConversionsHistory+ looseDevConversionRecordtype incoordinatorClient.ts). EdgeConsole.tsxwraps the app inSharedStatusProviderand mounts the rail; comprehensive vitest suites and a Playwright E2E (cross-axis-handoff.spec.ts) with honest soft-gating accompany the change.
Reviewed changes
Copilot reviewed 28 out of 33 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
web-viewer-sample/src/console/handoff.ts |
New cross-axis handoff build/parse utility with AxisKey typing. |
web-viewer-sample/src/console/useSharedStatus.ts |
Shared status context, snapshot types, and EMPTY_SHARED_STATUS. |
web-viewer-sample/src/console/SharedStatusProvider.tsx |
Single 5s polling loop + watchdog self-heal; maps runtime/conversion data honestly. |
web-viewer-sample/src/console/SharedStatusRail.tsx |
Top-level rail UI; renders null→未取得, stale→dim, health verbatim. |
web-viewer-sample/src/console/incomingHandoff.tsx |
Receiver hook + banner implementing the four-state re-verify contract. |
web-viewer-sample/src/console/pages.tsx |
Adds per-axis cross-link chips, receiver predicates, and conversion-history panel. |
web-viewer-sample/src/console/coordinatorClient.ts |
Adds DevConversionRecord type and getConversionsHistory for /api/dev/conversions. |
web-viewer-sample/src/console/EdgeConsole.tsx |
Wraps app in provider, mounts rail, computes railAxis source. |
web-viewer-sample/e2e/cross-axis-handoff.spec.ts |
End-to-end walkthrough with honest skip/soft-gating for unobserved paths. |
web-viewer-sample/src/console/*.test.tsx / *.test.ts |
Unit tests for rail, provider, handoff, receiver, and per-axis cross-links. |
docs/evidence/seven-axis-cross-page-harmony/* |
E2E evidence (screenshots + summary.json). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (5)
web-viewer-sample/e2e/cross-axis-handoff.spec.ts (1)
70-70: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional: extract the repeated hash-param parsing into a small helper.
The idiom
new URLSearchParams(new URL(page.url()).hash.split("?")[1] ?? "").get("<k>") ?? ""is duplicated at Lines 70, 132, 202, 237, 273, and 375. A single local helper would cut the noise and keep the parsing consistent if the hash format ever changes.♻️ Example helper
const hashParam = (page: import("`@playwright/test`").Page, key: string): string => new URLSearchParams(new URL(page.url()).hash.split("?")[1] ?? "").get(key) ?? "";🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-viewer-sample/e2e/cross-axis-handoff.spec.ts` at line 70, The hash query parsing for `minio_key` and the other repeated keys is duplicated throughout `cross-axis-handoff.spec.ts`, making the tests noisy and harder to keep consistent. Extract the repeated `new URLSearchParams(new URL(page.url()).hash.split("?")[1] ?? "").get(...) ?? ""` logic into a small local helper, such as a `hashParam` function near the affected Playwright test cases, and replace each inline occurrence with that helper so all callers share the same parsing behavior.web-viewer-sample/src/console/SharedStatusProvider.tsx (1)
33-47: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winParallelize the two independent poll requests.
runtimeStatus()andgetConversionRecords(100)are independent reads but are awaited sequentially, adding their latencies together on every cycle. Given the watchdog's wedge/staleness detection is tuned to2 × pollMs, this avoidable extra latency works against the freshness goal the rest of this file is carefully protecting.⚡ Proposed fix to run both calls concurrently
const poll = async (gen: number) => { inFlightSince = Date.now(); try { - const rt = await coordinatorClient.runtimeStatus(); - let conversionQueue: number | null = null; - try { - const recs = await coordinatorClient.getConversionRecords(100); - conversionQueue = recs.count > recs.items.length - ? null - : recs.items.filter((r) => QUEUE_STATUSES.has(r.status)).length; - } catch { - conversionQueue = null; // records unavailable → 未取得, do not guess - } + const [rtResult, recsResult] = await Promise.allSettled([ + coordinatorClient.runtimeStatus(), + coordinatorClient.getConversionRecords(100), + ]); + if (rtResult.status === "rejected") throw rtResult.reason; + const rt = rtResult.value; + const conversionQueue = recsResult.status === "fulfilled" + ? (recsResult.value.count > recsResult.value.items.length + ? null + : recsResult.value.items.filter((r) => QUEUE_STATUSES.has(r.status)).length) + : null; // records unavailable → 未取得, do not guess if (cancelled || gen !== pollGen) return;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-viewer-sample/src/console/SharedStatusProvider.tsx` around lines 33 - 47, In SharedStatusProvider.tsx, the poll path currently awaits runtimeStatus() and then getConversionRecords(100) sequentially even though they are independent reads. Update the SharedStatusProvider polling logic to start both requests concurrently (for example in the same block before awaiting either) and then consume both results together, while keeping the existing conversionQueue truncation handling and fallback-to-null behavior unchanged.web-viewer-sample/src/console/edge-console.css (1)
485-494: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winDegraded health has no distinct visual style.
Only
.health-unknowngets dimmed styling;.health-degradedshares the default bold/full-opacity look with.health-ok, so operators can't visually distinguish "degraded" from "ok" at a glance — only by reading the text label. This runs against the stated intent of rendering health "honestly" for degraded/unknown states.💄 Suggested addition
.ec-statusrail-v.muted { opacity: 0.6; font-weight: 400; } .ec-statusrail-v.health-unknown { opacity: 0.6; } +.ec-statusrail-v.health-degraded { color: var(--ec-amb, orange); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-viewer-sample/src/console/edge-console.css` around lines 485 - 494, The status rail styling in edge-console.css does not visually distinguish degraded health from ok health because only the .health-unknown state is dimmed. Update the existing .ec-statusrail-v health styles so .health-degraded has its own muted/less-prominent appearance, consistent with the intent of rendering health states honestly, while keeping .health-ok visually distinct from degraded and unknown.web-viewer-sample/src/console/pages.tsx (1)
741-763: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider extracting a shared cross-link chip helper.
The
onClick={() => { window.location.hash = buildHandoff(target, {...}); }}+ evidence-baseddisabledpattern repeats near-identically at every axis (A1, CV, SS, KG, M, RT, IN — e.g. lines 1246-1254, 1324-1327, 1526-1532, 1594-1595, 1611, 1973-1979, 3151-3160, 3212-3218). A smallCrossLinkBtn({ target, payload, testId, caption, label, disabled })wrapper aroundBtnwould remove this boilerplate without changing behavior.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-viewer-sample/src/console/pages.tsx` around lines 741 - 763, The cross-link chip logic is duplicated across multiple axis sections, with the same evidence-based disabled state and window.location.hash handoff pattern repeated in the Btn handlers. Extract a shared helper such as CrossLinkBtn that wraps Btn and accepts target, payload, testId, caption, label, and disabled, then replace the repeated inline onClick/disabled/caption wiring in the cross-link blocks with that helper while preserving the existing behavior.web-viewer-sample/src/console/MinioCrossLinks.test.tsx (1)
16-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a negative-direction test for chip suppression.
This suite only exercises the positive path (chip present + navigates). Sibling suites in this same PR (
ConversionHistory.test.tsx,IntakeCrossLinks.test.tsx) pair each positive chip test with a negative one asserting the chip is hidden when its gating field is absent (e.g., object withrole !== "source_ifc"or missingidempotency_key). Consider adding the same here to guard against an inverted truthy gate regression, consistent with the rest of this PR's "evidence-typed, no invalid jump" pattern.🧪 Suggested additional test
+ it("hides both cross-link chips for a non-.ifc object (role !== source_ifc)", async () => { + const nonIfcFolder: MinioFolderListing = { bucket: "bim-control", prefix: "", folders: [], count: 1, objects: [ + { key: "270專案/建築/v07/other.txt", etag: "e2", role: "other", project_id: "270", project_display_name: "270", category: "建築", version: "v07", idempotency_key: "mw_other" }, + ] }; + vi.spyOn(coordinatorClient, "getMinioFolder").mockResolvedValue(nonIfcFolder); + vi.spyOn(coordinatorClient, "getConversionRecords").mockResolvedValue({ count: 0, items: [] }); + const root = createRoot(container); + await act(async () => { root.render(<MinioDataPage />); }); + await act(async () => { await Promise.resolve(); await Promise.resolve(); }); + + expect(container.querySelector('[data-testid="minio-link-conv-mw_other"]')).toBeNull(); + expect(container.querySelector('[data-testid="minio-link-a1-mw_other"]')).toBeNull(); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-viewer-sample/src/console/MinioCrossLinks.test.tsx` around lines 16 - 61, Add a negative-path test in MinioCrossLinks.test.tsx to verify chip suppression, not just the successful navigation cases. Mirror the pattern used in the sibling cross-link suites by rendering MinioDataPage with a folder/item that lacks the gating field for each target link, then assert the corresponding chip query (for the same data-testid naming used in the existing tests) returns null. This will protect the chip-rendering logic against inverted truthy gate regressions in the MinioDataPage / cross-link chip conditions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/specs/2026-07-03-seven-axis-cross-page-harmony-design.md`:
- Around line 1-9: The document-nature label in the spec header uses a
non-standard phrase (“cross-page integration spec”) instead of the required
fixed vocabulary. Update the header in this spec to use the canonical document
nature label “spec design” so it matches the docs/**/*.md classification rules.
Keep the rest of the status/scope text intact and ensure the visible label is
consistent with the document’s role and with other specs for tooling/parsing.
In `@web-viewer-sample/src/console/EdgeConsole.sharedstatus.test.tsx`:
- Around line 19-36: The test in EdgeConsole.sharedstatus.test.tsx leaves the
React root mounted, so SharedStatusProvider cleanup never runs and its real
polling timers can leak past the test. Update the test setup/teardown around
createRoot and EdgeConsole to unmount the root in afterEach (before restoring
mocks) so the provider’s timeout/interval are cleared, and keep the
runtimeStatus assertion bounded in a way that still verifies the expected
polling occurs without allowing it to silently stop.
In `@web-viewer-sample/src/console/pages.tsx`:
- Around line 1804-1822: The `prefix` handling inside
`useIncomingHandoff("minio", ...)` is missing the same not-yet-loaded guard
already used by the `minio_key` branch. Update the `if (h.prefix)` path so that
`folder === null` returns `"indeterminate"` instead of falling through to
`false/not_found`, then keep the existing `folder.prefix === h.prefix` and
non-empty contents check for loaded folders. Make the `prefix` and `minio_key`
branches consistent so future prefix-based verification behaves correctly while
the folder is still loading.
- Around line 1520-1533: The session cross-link buttons in the sessions table
are missing the same liveness guard used elsewhere, so closed or non-active
sessions can still open KG/Review. Update the button rendering in the sessions
row block that uses `session-link-instances-*` and `session-link-review-*` to
disable or block those actions unless `s.status === "active"`, mirroring the
gating already implemented in `CoordinatorPage`. Keep `session-link-a1-*`
unchanged if it is intended to remain available, and reuse the same user-facing
disabled reason/message for consistency.
- Around line 920-944: The incoming handoff check in useIncomingHandoff
currently treats empty jobs/records with false truncation flags as not_found
before the initial fetch completes, causing a premature red banner. Gate the
verifier in pages.tsx on first-load completion by tracking whether the jobs and
records requests have resolved, and make the incoming callback return
indeterminate until the relevant source has finished loading; only evaluate
against jobsTruncated/recordsTruncated after that, while keeping the existing
job_id, conversion_id, and minio_key matching logic intact.
---
Nitpick comments:
In `@web-viewer-sample/e2e/cross-axis-handoff.spec.ts`:
- Line 70: The hash query parsing for `minio_key` and the other repeated keys is
duplicated throughout `cross-axis-handoff.spec.ts`, making the tests noisy and
harder to keep consistent. Extract the repeated `new URLSearchParams(new
URL(page.url()).hash.split("?")[1] ?? "").get(...) ?? ""` logic into a small
local helper, such as a `hashParam` function near the affected Playwright test
cases, and replace each inline occurrence with that helper so all callers share
the same parsing behavior.
In `@web-viewer-sample/src/console/edge-console.css`:
- Around line 485-494: The status rail styling in edge-console.css does not
visually distinguish degraded health from ok health because only the
.health-unknown state is dimmed. Update the existing .ec-statusrail-v health
styles so .health-degraded has its own muted/less-prominent appearance,
consistent with the intent of rendering health states honestly, while keeping
.health-ok visually distinct from degraded and unknown.
In `@web-viewer-sample/src/console/MinioCrossLinks.test.tsx`:
- Around line 16-61: Add a negative-path test in MinioCrossLinks.test.tsx to
verify chip suppression, not just the successful navigation cases. Mirror the
pattern used in the sibling cross-link suites by rendering MinioDataPage with a
folder/item that lacks the gating field for each target link, then assert the
corresponding chip query (for the same data-testid naming used in the existing
tests) returns null. This will protect the chip-rendering logic against inverted
truthy gate regressions in the MinioDataPage / cross-link chip conditions.
In `@web-viewer-sample/src/console/pages.tsx`:
- Around line 741-763: The cross-link chip logic is duplicated across multiple
axis sections, with the same evidence-based disabled state and
window.location.hash handoff pattern repeated in the Btn handlers. Extract a
shared helper such as CrossLinkBtn that wraps Btn and accepts target, payload,
testId, caption, label, and disabled, then replace the repeated inline
onClick/disabled/caption wiring in the cross-link blocks with that helper while
preserving the existing behavior.
In `@web-viewer-sample/src/console/SharedStatusProvider.tsx`:
- Around line 33-47: In SharedStatusProvider.tsx, the poll path currently awaits
runtimeStatus() and then getConversionRecords(100) sequentially even though they
are independent reads. Update the SharedStatusProvider polling logic to start
both requests concurrently (for example in the same block before awaiting
either) and then consume both results together, while keeping the existing
conversionQueue truncation handling and fallback-to-null behavior unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8af08ef3-ee85-4125-806b-9dcf935abf1c
⛔ Files ignored due to path filters (4)
docs/evidence/seven-axis-cross-page-harmony/a1-review-not-started.pngis excluded by!**/*.pngdocs/evidence/seven-axis-cross-page-harmony/rail-all-axes.pngis excluded by!**/*.pngdocs/evidence/seven-axis-cross-page-harmony/review-room-stage-truth-not-observed.pngis excluded by!**/*.pngdocs/evidence/seven-axis-cross-page-harmony/ss-to-review.pngis excluded by!**/*.png
📒 Files selected for processing (29)
docs/evidence/seven-axis-cross-page-harmony/summary.jsondocs/superpowers/plans/2026-07-03-seven-axis-cross-page-harmony.mddocs/superpowers/specs/2026-07-03-seven-axis-cross-page-harmony-design.mdweb-viewer-sample/e2e/cross-axis-handoff.spec.tsweb-viewer-sample/src/console/A1CrossLinks.test.tsxweb-viewer-sample/src/console/A1ViewerEmbed.test.tsxweb-viewer-sample/src/console/ConversionHistory.test.tsxweb-viewer-sample/src/console/CoordinatorCrossLinks.test.tsxweb-viewer-sample/src/console/EdgeConsole.sharedstatus.test.tsxweb-viewer-sample/src/console/EdgeConsole.tsxweb-viewer-sample/src/console/IntakeCrossLinks.test.tsxweb-viewer-sample/src/console/KitGpuFleetCrossLinks.test.tsxweb-viewer-sample/src/console/MinioCrossLinks.test.tsxweb-viewer-sample/src/console/ReviewSessionViewerPane.crosslinks.test.tsxweb-viewer-sample/src/console/ReviewSessionViewerPane.tsxweb-viewer-sample/src/console/SessionCrossLinks.test.tsxweb-viewer-sample/src/console/SharedStatusProvider.test.tsxweb-viewer-sample/src/console/SharedStatusProvider.tsxweb-viewer-sample/src/console/SharedStatusRail.test.tsxweb-viewer-sample/src/console/SharedStatusRail.tsxweb-viewer-sample/src/console/coordinatorClient.conversions-history.test.tsweb-viewer-sample/src/console/coordinatorClient.tsweb-viewer-sample/src/console/edge-console.cssweb-viewer-sample/src/console/handoff.test.tsweb-viewer-sample/src/console/handoff.tsweb-viewer-sample/src/console/incomingHandoff.test.tsxweb-viewer-sample/src/console/incomingHandoff.tsxweb-viewer-sample/src/console/pages.tsxweb-viewer-sample/src/console/useSharedStatus.ts
| # 七軸跨頁和諧整合設計(A1 / CV / SS / KG / M / IN / RT) | ||
|
|
||
| > 日期:2026-07-03 | ||
| > 類型:cross-page integration spec — 七條正典路由「跨頁和諧整合」(非單頁合併;使用者已明確裁定,不重議) | ||
| > Scope:`web-viewer-sample/src/console/` 七頁(`#a1` / `#conv` / `#sessions` / `#instances` / `#minio` / `#intake` / `#runtime`)+既有 Review Room(`#gpu` 正典 / `#review` 別名)交握。**frontend-only、零新後端、零新路由表**。 | ||
| > 狀態:ready for spec-to-done — 使用者已於 2026-07-03 明確指示 spec 完成後立刻執行 spec-to-done | ||
| > 承接:`docs/superpowers/specs/2026-07-02-a1-3d-review-decouple-design.md`(A1 3D 已移交 Review Room,commit `a334e49` / PR #286)——本 spec **原樣承接其結論,不重新裁決 3D 架構**,只補「SS / KG / RT 的 session、GPU、health 狀態如何餵進這個既有交握」。 | ||
| > 效力定位:本 spec 服從 `docs/plans/docs-plans-README.md` §1 效力序與 §3 十一條鐵律;路由正典只引用《互動實作規格》A.1.1,不自建。 | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Document-nature label doesn't match the required category set.
Line 4 labels this as "cross-page integration spec", but the guideline requires marking document nature from a fixed vocabulary (agent boundary / contract / wiki / runbook / spec design / working note). Consider aligning the label to spec design explicitly for consistent tooling/parsing across docs/**/*.md.
As per coding guidelines, docs/**/*.md "MUST mark document nature: agent boundary / contract / wiki / runbook / spec design / working note".
📝 Proposed label fix
-> 類型:cross-page integration spec — 七條正典路由「跨頁和諧整合」(非單頁合併;使用者已明確裁定,不重議)
+> 類型:spec design — 七條正典路由「跨頁和諧整合」(非單頁合併;使用者已明確裁定,不重議)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # 七軸跨頁和諧整合設計(A1 / CV / SS / KG / M / IN / RT) | |
| > 日期:2026-07-03 | |
| > 類型:cross-page integration spec — 七條正典路由「跨頁和諧整合」(非單頁合併;使用者已明確裁定,不重議) | |
| > Scope:`web-viewer-sample/src/console/` 七頁(`#a1` / `#conv` / `#sessions` / `#instances` / `#minio` / `#intake` / `#runtime`)+既有 Review Room(`#gpu` 正典 / `#review` 別名)交握。**frontend-only、零新後端、零新路由表**。 | |
| > 狀態:ready for spec-to-done — 使用者已於 2026-07-03 明確指示 spec 完成後立刻執行 spec-to-done | |
| > 承接:`docs/superpowers/specs/2026-07-02-a1-3d-review-decouple-design.md`(A1 3D 已移交 Review Room,commit `a334e49` / PR #286)——本 spec **原樣承接其結論,不重新裁決 3D 架構**,只補「SS / KG / RT 的 session、GPU、health 狀態如何餵進這個既有交握」。 | |
| > 效力定位:本 spec 服從 `docs/plans/docs-plans-README.md` §1 效力序與 §3 十一條鐵律;路由正典只引用《互動實作規格》A.1.1,不自建。 | |
| # 七軸跨頁和諧整合設計(A1 / CV / SS / KG / M / IN / RT) | |
| > 日期:2026-07-03 | |
| > 類型:spec design — 七條正典路由「跨頁和諧整合」(非單頁合併;使用者已明確裁定,不重議) | |
| > Scope:`web-viewer-sample/src/console/` 七頁(`#a1` / `#conv` / `#sessions` / `#instances` / `#minio` / `#intake` / `#runtime`)+既有 Review Room(`#gpu` 正典 / `#review` 別名)交握。**frontend-only、零新後端、零新路由表**。 | |
| > 狀態:ready for spec-to-done — 使用者已於 2026-07-03 明確指示 spec 完成後立刻執行 spec-to-done | |
| > 承接:`docs/superpowers/specs/2026-07-02-a1-3d-review-decouple-design.md`(A1 3D 已移交 Review Room,commit `a334e49` / PR `#286`)——本 spec **原樣承接其結論,不重新裁決 3D 架構**,只補「SS / KG / RT 的 session、GPU、health 狀態如何餵進這個既有交握」。 | |
| > 效力定位:本 spec 服從 `docs/plans/docs-plans-README.md` §1 效力序與 §3 十一條鐵律;路由正典只引用《互動實作規格》A.1.1,不自建。 |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/specs/2026-07-03-seven-axis-cross-page-harmony-design.md`
around lines 1 - 9, The document-nature label in the spec header uses a
non-standard phrase (“cross-page integration spec”) instead of the required
fixed vocabulary. Update the header in this spec to use the canonical document
nature label “spec design” so it matches the docs/**/*.md classification rules.
Keep the rest of the status/scope text intact and ensure the visible label is
consistent with the document’s role and with other specs for tooling/parsing.
Source: Coding guidelines
| describe("EdgeConsole mounts shared status rail once", () => { | ||
| let container: HTMLDivElement; | ||
| beforeEach(() => { (globalThis as Record<string, unknown>)["IS_REACT_ACT_ENVIRONMENT"] = true; container = document.createElement("div"); document.body.appendChild(container); window.location.hash = "#a1"; }); | ||
| afterEach(() => { document.body.removeChild(container); vi.restoreAllMocks(); }); | ||
|
|
||
| it("renders the rail and polls runtimeStatus once for the whole console", async () => { | ||
| const spy = vi.spyOn(coordinatorClient, "runtimeStatus").mockResolvedValue(RT); | ||
| vi.spyOn(coordinatorClient, "getConversionRecords").mockResolvedValue({ count: 0, items: [] }); | ||
| const root = createRoot(container); | ||
| await act(async () => { root.render(<EdgeConsole />); }); | ||
| await act(async () => { await Promise.resolve(); await Promise.resolve(); }); | ||
|
|
||
| expect(container.querySelector('[data-testid="shared-status-rail"]')).not.toBeNull(); | ||
| // A1 page also fetches runtimeStatus once on mount; the provider adds exactly one more. The rail must | ||
| // not multiply polling per page — assert provider poll count stays bounded (<= 2: A1 mount + provider). | ||
| expect(spy.mock.calls.length).toBeLessThanOrEqual(2); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Test never unmounts the React root, leaking SharedStatusProvider's real timers past test end.
SharedStatusProvider's effect schedules a real setTimeout (poll) and setInterval (watchdog); both are only cleared in its cleanup, which runs on unmount. Here root is never unmounted — afterEach only removes the container from the DOM and restores mocks. The pending timers survive past the test, and once mocks are restored they'll fire real (unmocked) calls to coordinatorClient.runtimeStatus()/getConversionRecords() later, risking flaky unhandled-rejection noise or interference with later tests in the same worker.
Also worth tightening: expect(spy.mock.calls.length).toBeLessThanOrEqual(2) only bounds the upper end — it wouldn't catch a regression where polling silently stops firing.
🧹 Suggested fix
let container: HTMLDivElement;
+ let root: ReturnType<typeof createRoot> | undefined;
beforeEach(() => { (globalThis as Record<string, unknown>)["IS_REACT_ACT_ENVIRONMENT"] = true; container = document.createElement("div"); document.body.appendChild(container); window.location.hash = "`#a1`"; });
- afterEach(() => { document.body.removeChild(container); vi.restoreAllMocks(); });
+ afterEach(() => { act(() => { root?.unmount(); }); document.body.removeChild(container); vi.restoreAllMocks(); });
it("renders the rail and polls runtimeStatus once for the whole console", async () => {
const spy = vi.spyOn(coordinatorClient, "runtimeStatus").mockResolvedValue(RT);
vi.spyOn(coordinatorClient, "getConversionRecords").mockResolvedValue({ count: 0, items: [] });
- const root = createRoot(container);
+ root = createRoot(container);
await act(async () => { root.render(<EdgeConsole />); });
await act(async () => { await Promise.resolve(); await Promise.resolve(); });
expect(container.querySelector('[data-testid="shared-status-rail"]')).not.toBeNull();
- expect(spy.mock.calls.length).toBeLessThanOrEqual(2);
+ expect(spy.mock.calls.length).toBeGreaterThanOrEqual(1);
+ expect(spy.mock.calls.length).toBeLessThanOrEqual(2);
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| describe("EdgeConsole mounts shared status rail once", () => { | |
| let container: HTMLDivElement; | |
| beforeEach(() => { (globalThis as Record<string, unknown>)["IS_REACT_ACT_ENVIRONMENT"] = true; container = document.createElement("div"); document.body.appendChild(container); window.location.hash = "#a1"; }); | |
| afterEach(() => { document.body.removeChild(container); vi.restoreAllMocks(); }); | |
| it("renders the rail and polls runtimeStatus once for the whole console", async () => { | |
| const spy = vi.spyOn(coordinatorClient, "runtimeStatus").mockResolvedValue(RT); | |
| vi.spyOn(coordinatorClient, "getConversionRecords").mockResolvedValue({ count: 0, items: [] }); | |
| const root = createRoot(container); | |
| await act(async () => { root.render(<EdgeConsole />); }); | |
| await act(async () => { await Promise.resolve(); await Promise.resolve(); }); | |
| expect(container.querySelector('[data-testid="shared-status-rail"]')).not.toBeNull(); | |
| // A1 page also fetches runtimeStatus once on mount; the provider adds exactly one more. The rail must | |
| // not multiply polling per page — assert provider poll count stays bounded (<= 2: A1 mount + provider). | |
| expect(spy.mock.calls.length).toBeLessThanOrEqual(2); | |
| }); | |
| }); | |
| describe("EdgeConsole mounts shared status rail once", () => { | |
| let container: HTMLDivElement; | |
| let root: ReturnType<typeof createRoot> | undefined; | |
| beforeEach(() => { (globalThis as Record<string, unknown>)["IS_REACT_ACT_ENVIRONMENT"] = true; container = document.createElement("div"); document.body.appendChild(container); window.location.hash = "`#a1`"; }); | |
| afterEach(() => { act(() => { root?.unmount(); }); document.body.removeChild(container); vi.restoreAllMocks(); }); | |
| it("renders the rail and polls runtimeStatus once for the whole console", async () => { | |
| const spy = vi.spyOn(coordinatorClient, "runtimeStatus").mockResolvedValue(RT); | |
| vi.spyOn(coordinatorClient, "getConversionRecords").mockResolvedValue({ count: 0, items: [] }); | |
| root = createRoot(container); | |
| await act(async () => { root.render(<EdgeConsole />); }); | |
| await act(async () => { await Promise.resolve(); await Promise.resolve(); }); | |
| expect(container.querySelector('[data-testid="shared-status-rail"]')).not.toBeNull(); | |
| expect(spy.mock.calls.length).toBeGreaterThanOrEqual(1); | |
| expect(spy.mock.calls.length).toBeLessThanOrEqual(2); | |
| }); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web-viewer-sample/src/console/EdgeConsole.sharedstatus.test.tsx` around lines
19 - 36, The test in EdgeConsole.sharedstatus.test.tsx leaves the React root
mounted, so SharedStatusProvider cleanup never runs and its real polling timers
can leak past the test. Update the test setup/teardown around createRoot and
EdgeConsole to unmount the root in afterEach (before restoring mocks) so the
provider’s timeout/interval are cleared, and keep the runtimeStatus assertion
bounded in a way that still verifies the expected polling occurs without
allowing it to silently stop.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 72eadf94f2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // 「在 Review Room 開此 session」「Kit / GPU 機隊」是即時可操作語意,只有 status==='active' 成立;對 closed/closing | ||
| // 的過期 session 給滿血按鈕=把過期 session 假裝成真實可操作(比照同分支 0860a54)。「Session 管理」是 lifecycle 全量 | ||
| // 治理視圖,對已結束 session 給連結語意合理,保留 enabled。 | ||
| const live = s.status === "active"; |
There was a problem hiding this comment.
Allow created sessions in runtime cross-links
When a review session is still created, this marks it non-live and disables both Runtime → Review and Runtime → KG with a “session ended” label. That state is not ended: sessions are created without Kit bindings as created (bim-review-coordinator/src/services/sessionStore.ts:48), the backend accepts created in isSessionMutable for viewer lease claims, and ReviewSessionViewerPane lists active/created sessions as attachable. This prevents operators from opening a valid newly-created session from the runtime page.
Useful? React with 👍 / 👎.
| <Btn data-testid={`session-link-instances-${s.session_id}`} caption={t("此 session 落在哪個 GPU node(KG 遙測未取得)", "Which GPU node hosts this session (KG telemetry not available)")} | ||
| onClick={() => { window.location.hash = buildHandoff("instances", { source: "sessions", session: s.session_id }); }}>KG →</Btn> | ||
| {" "} | ||
| <Btn data-testid={`session-link-review-${s.session_id}`} caption={t("在 Review Room 開此 session", "Open this session in Review Room")} | ||
| onClick={() => { window.location.hash = buildHandoff("review", { source: "sessions", session: s.session_id }); }}>Review →</Btn> |
There was a problem hiding this comment.
Disable Review/KG chips for ended sessions
When /api/runtime/status includes retained closing/closed sessions, these newly added per-row Review/KG chips remain enabled even though the row has already been classified as ended just above. Clicking a closed row navigates to Review Room, but that pane only treats active/created sessions as observable/attachable, so the Sessions page offers an action that cannot succeed instead of preserving the same honesty boundary used by the terminate button and the runtime cross-links.
Useful? React with 👍 / 👎.
| const rt = await coordinatorClient.runtimeStatus(); | ||
| let conversionQueue: number | null = null; | ||
| try { | ||
| const recs = await coordinatorClient.getConversionRecords(100); |
There was a problem hiding this comment.
Don't block the status rail on ledger fetches
If /api/conversion/records accepts the request but never settles, this await prevents the poll from publishing the already-fetched /api/runtime/status snapshot, so active session count and health stay stale even though only the optional queue count is unavailable. The watchdog will keep starting new polls, but each can block at the same ledger call; degrade conversionQueue independently so runtime status still refreshes when the ledger route wedges.
Useful? React with 👍 / 👎.
| if (!h.minio_key) return "not_applicable"; | ||
| if (minioObjects === null) return "indeterminate"; | ||
| return minioObjects.some((o) => o.key === h.minio_key); |
There was a problem hiding this comment.
Treat A1 MinIO fetch failures as indeterminate
When getMinioObjects() fails, the catch above collapses the authority list to [], but this verifier only distinguishes null from a populated/empty list. In a MinIO outage or credential-missing environment, a valid M→A1 handoff therefore renders a definitive red not_found banner even though the ID was never checked; use the existing minioErr/loaded state to report indeterminate instead.
Useful? React with 👍 / 👎.
| if (h.minio_key) { | ||
| if (records.some((r) => r.object_key === h.minio_key)) return true; | ||
| return recordsTruncated ? "indeterminate" : false; | ||
| } |
There was a problem hiding this comment.
Treat unavailable CV lookups as indeterminate
For #conv?source=minio&minio_key=..., records is still [] while getConversionRecords() is pending and also remains empty when that fetch fails; because recordsTruncated stays false, this path returns false and paints not_found. In those loading/unavailable cases the receiver has not actually checked the ledger, so the banner should be indeterminate/unavailable rather than a definitive miss.
Useful? React with 👍 / 👎.
| <Btn | ||
| data-testid={`minio-link-a1-${idk}`} | ||
| caption={t("拿此檔到 A1 治理檢核", "Take this file to A1 governance")} | ||
| onClick={() => { window.location.hash = buildHandoff("a1", { source: "minio", minio_key: obj.key }); }} |
There was a problem hiding this comment.
Seed the A1 model from MinIO handoffs
When an operator clicks this “Take this file to A1 governance” chip, A1 receives and verifies minio_key but never copies it into selectedKey; the dropdown remains blank and a1-step-pick stays disabled, so the handoff does not actually take the chosen file into the governance flow. After a verified M→A1 handoff, seed the A1 model selection (without auto-claiming any session) so the operator can continue from the linked file instead of manually finding it again.
Useful? React with 👍 / 👎.
CodeRabbit 與 Codex 兩位 AI reviewer 對 PR #293 交叉核實後確認 6 處真缺陷,逐一修復並補 迴歸測試: - A1/CV 接收端重驗:getMinioObjects 失敗、CV jobs/records 首次 render 尚未 load 完成時, 舊邏輯把「還沒查」誤判成「查無」,對真實 handoff 誤閃 not_found 紅字;補齊 indeterminate 守門(比照既有 M/KG 軸已有的載入中防呆)。 - A1 verified handoff 過去只顯示「已重驗」banner,從未把 minio_key 帶進 selectedKey, operator 仍要手動重找同一份檔案;改為 verified 時自動種入(不影響後續手動改選)。 - Sessions/Runtime 兩頁的 KG/Review Room 跨頁連結未按 session 存活狀態一致把關:Sessions 頁對 closing/closed session 仍給可點按鈕(點了打不開);Runtime 頁反過來把尚未綁 Kit 的 created session 誤標成「已結束」。兩者統一比照後端 isSessionMutable/ ReviewSessionViewerPane 的 active/created 判準。 - SharedStatusProvider 的次要 ledger 查詢無 timeout,卡住時會拖住已成功拿到的主要 runtime 狀態遲遲無法發佈;補一個 pollMs 上限的 race,不阻塞主要資料。 - M 頁 prefix 分支補齊與 minio_key 分支一致的「尚未載入」防呆(今日無按鈕觸發,屬保留能力)。 補 6 個迴歸測試釘住修好的行為;npm run verify(build + 516 tests + struct-log)全綠。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR Review Agent Summary
Blockers
Warnings
Validation Commands
Checks
Human Review Notes
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: de1e763b20
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (h.job_id) { | ||
| if (!jobsLoaded) return "indeterminate"; | ||
| if (jobs.some((j) => j.ifc_ready_job_id === h.job_id || j.conversion_job_id === h.job_id)) return true; | ||
| return jobsTruncated ? "indeterminate" : false; |
There was a problem hiding this comment.
Keep job handoffs indeterminate on fetch failure
When GET /api/external/ifc-ready rejects, load() leaves jobs as the empty/stale array and jobsTruncated as false but still sets jobsLoaded to true, so an incoming #conv?source=intake&job_id=... falls through here and renders not_found. In a coordinator/ifc-ready outage the job was never checked, so valid intake handoffs get a false red “not found” banner; keep an error/loaded-success flag for the jobs fetch and return indeterminate instead.
Useful? React with 👍 / 👎.
| <Btn data-testid={`conv-job-review-${j.ifc_ready_job_id}`} caption={t("在 Review Room 開此 session", "Open this session in Review Room")} | ||
| onClick={() => { window.location.hash = buildHandoff("review", { source: "conv", session: j.review_session_id as string }); }}>Review →</Btn> |
There was a problem hiding this comment.
Gate job Review links by live session state
When an ifc-ready job is retained after its review_session_id has moved to closing/closed, this Review Room chip is still enabled because it only checks that the ID exists. ReviewSessionViewerPane only treats active/created sessions as observable/attachable, so this sends operators to a Review Room that cannot open the session; include the session status in this row or disable the Review link for non-live sessions, matching the guarded Session/Runtime chips.
Useful? React with 👍 / 👎.
| const sessionCandidates = Object.values(shared.sessionsById) | ||
| .filter((s) => s.status === "active" || s.status === "created") | ||
| .map((s) => s.session_id); |
There was a problem hiding this comment.
Avoid suggesting sessions the pane cannot attach
These datalist candidates come from the continuously polling shared status, but the attach gate just below still uses this pane’s one-shot runtimeSessions snapshot. If Review Room is already open when a new active/created session appears, or if the pane’s initial /api/runtime/status call failed while the shared poll later succeeds, the new session is offered as an autocomplete choice but sessionObserved remains false and the manual start button stays disabled until a reload. Either keep the candidate source aligned with the attach source or refresh/merge the runtime snapshot when selecting a shared candidate.
Useful? React with 👍 / 👎.
摘要
七軸(A1/CV/SS/KG/M/IN/RT)跨頁和諧整合:新增共用的 cross-page handoff 契約、共享狀態列(SharedStatusRail)、各軸間證據型 cross-link chip,並讓接收端對真實資料誠實重驗(verified / not_found / indeterminate / not_applicable)。純前端加法(N6),不新增後端服務或路由,不重新裁決 A1/Review Room 的 3D(N3)。
handoff.ts(跨軸 handoff 通用工具)、useSharedStatus/SharedStatusProvider/SharedStatusRail(共享狀態列,單一 5 秒輪詢)、incomingHandoff.tsx(接收端 re-verify 契約,含四態:verified/not_found/indeterminate/not_applicable)web-viewer-sample/e2e/cross-axis-handoff.spec.ts,含 §8 全鏈路走查Frontend Verification
#a1#conv#sessions#instances#minio#intake#runtime(共享狀態列覆蓋全 7 軸);#review?source=...(跨軸 handoff 接收端)session-link-review-*chip;A1 頁a1-link-minio/a1-link-sessionschip;共享狀態列 GPU/Health/佇列/session 數值列stream_conv_20260703090757_89c87388,2639 IFC entities,coverage pass)+ 真實 seeded review session(review_session_0e4ee1079ee1,非 mock)GET /api/runtime/status(:8005 branch coordinator)、POST /api/review-sessions、真實 governance(:49102)、真實 conversion authority(:49101)E2E_COORDINATOR_BASE_URL=http://127.0.0.1:8005 E2E_DISABLE_WEBSERVER=1 npx playwright test cross-axis-handoff.spec.ts(分支隔離 coordinator,不動部署區 :8004)docs/evidence/seven-axis-cross-page-harmony/(rail-all-axes.png、a1-review-not-started.png、ss-to-review.png、review-room-stage-truth-not-observed.png、summary.json);trace 另存artifacts/e2e/seven-axis-cross-page-harmony-trace/(未 tracked,worktree 外)/ui#sessions,點任一 active session 的「→ Review」chip → 應導覽到#review?source=sessions&session=<id>並顯示該 session 已被權威資料重驗確認(非「查無」)vertical_slice.ok=true)已知風險與範圍限制(已與使用者逐項確認,非隱瞞)
.env被 sandbox 的憑證探索拒絕政策擋下(正確行為,非工具偷懶)。此路徑的 unit test 已補齊(CV 頁 minio_key predicate,含中文 key case),僅瀏覽器層級 not observed。nvidia-smi確認部署區 GPU 使用率僅 6%(非資源競爭),根因較可能是單一 viewer 名額被舊連線占用或冷啟動需要更長時間。此 4 項驗證的是 Review Room 既有 GPU 渲染能力,非本次新增範圍(N3)。E2E 已嘗試讀取kit_instance_bindings[].status區分「環境無 Kit」vs「Kit 配置但 failed」,惟後端目前尚無路徑會產生failed狀態(forward-compatible,非本次回歸)。POST /api/review-sessions,非真實 operator 走完整 conversion-ready webhook 自動建立流程。已查證兩條路徑在SessionStore.create()完全收斂、狀態判定公式相同,接收端邏輯對建立來源無感——架構上無法觸發「seeded 過但 organic 壞」的失效模式,但嚴格而言仍屬證據方法論缺口,非 code 層級可關閉項目。GET /api/dev/conversions的count定義即為items.length(無法推導真總數),受 N4 凍結後端約束無法前端補救,僅供揭露。minio_key而非 spec §4.3 原範例的prefix:已於 spec 文件本身加註「既知差異(as-built)」說明理由(minio_key更精確指向確切檔案),prefix收件分支保留供未來使用且有測試覆蓋,非死碼。GitNexus 索引落後揭露
本輪開發期間 GitNexus 索引數次落後 HEAD 多個 commit(含一次 LadybugDB rebuild lock),部分 task 的
detect_changes改採git diff --cached自查 fallback(Task 7/9/10 及數個 fix round),皆已逐一人工核對 staged scope 與意圖修改一致,無 scope 溢出。模型代打揭露
P4(browser evidence)與 P3 FinalReview 執行期間 Fable 5(arbiter tier)額度用盡;P4 經使用者核可暫以 opus max 代打(供應例外,非品質降級,僅本次 run 的 scratch 腳本副本,未動 repo 正式 routing);P3 FinalReview 缺口已依 SOP 由 P5 對抗複驗的全 diff 通讀補償關閉。
驗證
npx vitest run(web-viewer-sample):47 files / 510 tests 全綠npx tsc --noEmit:僅 1 個既有、非本次引入的 baseline 錯誤(windowParentMessage.dom.test.tsx:292)npx eslint:無新增錯誤cross-axis-handoff.spec.ts):4 passed / 2 honest-skip / 0 failed(見上方 evidence)overall_safe=trueorigin/main,解決 1 個真實測試衝突(A1ViewerEmbed.test.tsx,查證後採用與現行程式碼行為一致的版本)+ 1 個 rebase 後型別缺口(ReviewSessionViewerPane.crosslinks.test.tsx補 3 個新增欄位)Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests