Skip to content

七軸(A1/CV/SS/KG/M/IN/RT)跨頁和諧整合:共用 handoff 契約 + 共享狀態列 + 誠實接收端重驗 - #293

Merged
monkey1sai merged 58 commits into
mainfrom
feat/seven-axis-cross-page-harmony
Jul 4, 2026
Merged

monkey1sai merged 58 commits into
mainfrom
feat/seven-axis-cross-page-harmony

Conversation

@monkey1sai

@monkey1sai monkey1sai commented Jul 4, 2026 •

Copy link
Copy Markdown
Owner

摘要

七軸(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)
  • A1/CV/SS/KG/M/IN 六軸各自新增證據型 cross-link chip(帶 source + 相關 id,接收端逐一重驗)
  • RT 軸(CoordinatorPage)新增跨頁 session 面板
  • Review Room 種入 shared-status session 候選 datalist(N3-safe,不自動 claim)
  • Playwright E2E:web-viewer-sample/e2e/cross-axis-handoff.spec.ts,含 §8 全鏈路走查

Frontend Verification

Item Result
Frontend route #a1 #conv #sessions #instances #minio #intake #runtime(共享狀態列覆蓋全 7 軸);#review?source=...(跨軸 handoff 接收端)
Main button(s) tested SS 頁 session-link-review-* chip;A1 頁 a1-link-minio/a1-link-sessions chip;共享狀態列 GPU/Health/佇列/session 數值列
Fixture used 真實隔離 coordinator(:8005)+ 真實 IFC 轉檔產物(洲際好宅/給水案,job stream_conv_20260703090757_89c87388,2639 IFC entities,coverage pass)+ 真實 seeded review session(review_session_0e4ee1079ee1,非 mock)
Backend API called 真實 GET /api/runtime/status(:8005 branch coordinator)、POST /api/review-sessions、真實 governance(:49102)、真實 conversion authority(:49101)
Runtime action 點擊 cross-link chip → 導覽到目標軸 → 接收端對真實 backend 資料重新查驗 id → 顯示終局狀態
Visible success state SS→Review 重驗真實 session 為「observed」(非靜默預設);共享狀態列在全 7 軸顯示真實輪詢資料、GPU 誠實顯示「未取得」(非假綠燈)
E2E command E2E_COORDINATOR_BASE_URL=http://127.0.0.1:8005 E2E_DISABLE_WEBSERVER=1 npx playwright test cross-axis-handoff.spec.ts(分支隔離 coordinator,不動部署區 :8004)
Screenshot / trace 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 外)
Manual test steps 開 /ui#sessions,點任一 active session 的「→ Review」chip → 應導覽到 #review?source=sessions&session=<id> 並顯示該 session 已被權威資料重驗確認(非「查無」)
Known gaps 見下方「已知風險與範圍限制」完整清單;核心 vertical slice 已用真實資料驗證(見 evidence summary.json vertical_slice.ok=true)

已知風險與範圍限制(已與使用者逐項確認,非隱瞞)

  1. M→CV / IN→CV 瀏覽器證據未觀測:隔離 coordinator 需要 MinIO access/secret key 才能列物件,讀取部署區 .env 被 sandbox 的憑證探索拒絕政策擋下(正確行為,非工具偷懶)。此路徑的 unit test 已補齊(CV 頁 minio_key predicate,含中文 key case),僅瀏覽器層級 not observed。
  2. Deep-Kit 四項 GPU 證據(first_frame/stage_matched/datachannel_ready/highlight_ack)未觀測:已對真實 seeded session 做真實 manual attach + claim primary lease + 掛載 viewer host,但 180 秒內未取得真實 WebRTC 影格。已親跑 nvidia-smi 確認部署區 GPU 使用率僅 6%(非資源競爭),根因較可能是單一 viewer 名額被舊連線占用或冷啟動需要更長時間。此 4 項驗證的是 Review Room 既有 GPU 渲染能力,非本次新增範圍(N3)。E2E 已嘗試讀取 kit_instance_bindings[].status 區分「環境無 Kit」vs「Kit 配置但 failed」,惟後端目前尚無路徑會產生 failed 狀態(forward-compatible,非本次回歸)。
  3. SS→Review 唯一觀測到的重驗使用 harness 自建 session:該 session 綁定真實轉檔產物(非 mock),但建立路徑是本次 evidence harness 呼叫 POST /api/review-sessions,非真實 operator 走完整 conversion-ready webhook 自動建立流程。已查證兩條路徑在 SessionStore.create() 完全收斂、狀態判定公式相同,接收端邏輯對建立來源無感——架構上無法觸發「seeded 過但 organic 壞」的失效模式,但嚴格而言仍屬證據方法論缺口,非 code 層級可關閉項目。
  4. A1 rule-run 啟用態 CTA 未觀測:需要 E2E 自己驅動「選 session→跑 rule-run」讓 A1 開啟 Review Room 的按鈕從停用轉為啟用,目前範圍未做(誠實標註於 spec §8 coverage note,非誤導成環境限制)。
  5. 轉檔歷史 panel 無截斷指示器:後端 GET /api/dev/conversions 的 count 定義即為 items.length(無法推導真總數),受 N4 凍結後端約束無法前端補救,僅供揭露。
  6. A1→M chip 送 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:無新增錯誤
  • Playwright E2E(cross-axis-handoff.spec.ts):4 passed / 2 honest-skip / 0 failed(見上方 evidence)
  • P5 對抗複驗(3 輪):全部 findings 真閉合,critic overall_safe=true
  • 已 rebase 至最新 origin/main,解決 1 個真實測試衝突(A1ViewerEmbed.test.tsx,查證後採用與現行程式碼行為一致的版本)+ 1 個 rebase 後型別缺口(ReviewSessionViewerPane.crosslinks.test.tsx 補 3 個新增欄位)

Summary by CodeRabbit

  • New Features

    • Added a shared status/evidence rail across console pages with live sessions, GPU availability, conversion queue, and health.
    • Added cross-page handoff chips/links plus incoming verification banners for deep-link arrivals.
    • Added a conversion history panel with cross-link chips for related targets.
  • Bug Fixes

    • Improved deep-link verification with “honest” states (verified/not found/unknown) and clearer stale handling.
    • Preserved exact identifiers in URL hash handoffs, including non‑Latin keys.
  • Documentation

    • Added integration design docs and Playwright evidence.
  • Tests

    • Expanded unit and E2E coverage for cross-axis handoff and shared status.

monkey1sai and others added 30 commits July 5, 2026 00:13
新增 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>
monkey1sai and others added 15 commits July 5, 2026 00:18
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>
Copilot AI review requested due to automatic review settings July 4, 2026 16:23
@coderabbitai

coderabbitai Bot commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a457a64f-8eab-454c-bb1c-9ca60528d3c3

📥 Commits

Reviewing files that changed from the base of the PR and between 72eadf9 and de1e763.

📒 Files selected for processing (5)
  • web-viewer-sample/src/console/CoordinatorCrossLinks.test.tsx
  • web-viewer-sample/src/console/SessionCrossLinks.test.tsx
  • web-viewer-sample/src/console/SharedStatusProvider.tsx
  • web-viewer-sample/src/console/incomingHandoff.test.tsx
  • web-viewer-sample/src/console/pages.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • web-viewer-sample/src/console/CoordinatorCrossLinks.test.tsx
  • web-viewer-sample/src/console/SharedStatusProvider.tsx
  • web-viewer-sample/src/console/incomingHandoff.test.tsx
  • web-viewer-sample/src/console/pages.tsx

📝 Walkthrough

Walkthrough

This 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.

Changes

Seven-Axis Cross-Page Harmony

Layer / File(s) Summary
Design spec and evidence report
docs/superpowers/specs/2026-07-03-seven-axis-cross-page-harmony-design.md, docs/evidence/seven-axis-cross-page-harmony/summary.json
Adds the integration spec and the generated Playwright evidence report.
Cross-axis handoff contract utility
web-viewer-sample/src/console/handoff.ts, handoff.test.ts
Defines AxisKey/CrossAxisHandoff and the hash build/parse helpers with validation tests.
Shared status context, polling provider, and status rail
useSharedStatus.ts, SharedStatusProvider.tsx(.test), SharedStatusRail.tsx(.test), edge-console.css, EdgeConsole.tsx(.test)
Adds shared runtime status state, polling/watchdog logic, the top-level status rail, styling, and shell wiring.
Incoming handoff re-verification hook
incomingHandoff.tsx, incomingHandoff.test.tsx
Adds the incoming-handoff hook and banner for verified, not_found, indeterminate, and not_applicable states.
A1 governance page cross-links
pages.tsx (A1), A1CrossLinks.test.tsx, A1ViewerEmbed.test.tsx
Adds incoming minio_key verification, an A1 banner, and cross-link chips to MinIO and Review Room.
Conversion Scheduling cross-links and history panel
pages.tsx (CV), coordinatorClient.ts, ConversionHistory.test.tsx, coordinatorClient.conversions-history.test.ts
Adds truncation-aware incoming verification, a conversion-history panel, and cross-link buttons on conversion rows.
Session Management and Kit GPU Fleet cross-links
pages.tsx (SS/KG), SessionCrossLinks.test.tsx, KitGpuFleetCrossLinks.test.tsx
Adds incoming session verification and live-session cross-links, and reworks Kit GPU Fleet to use shared status.
MinIO, Coordinator, and Intake cross-links
pages.tsx (M/C), MinioCrossLinks.test.tsx, CoordinatorCrossLinks.test.tsx, IntakeCrossLinks.test.tsx, web-viewer-sample/e2e/cross-axis-handoff.spec.ts
Adds guarded MinIO loading and handoff verification, a coordinator session-links panel, an intake cross-links column, and the Playwright end-to-end suite.
Review Session Viewer Pane candidate datalist
ReviewSessionViewerPane.tsx, ReviewSessionViewerPane.crosslinks.test.tsx
Adds a shared-status-backed session candidate datalist for the review pane.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change: seven-axis cross-page handoff integration with a shared contract, shared status rail, and honest receiver re-verification.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/seven-axis-cross-page-harmony

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

PR Review Agent Summary

Field Value
Status warning
Risk medium
PR 293
Head feat/seven-axis-cross-page-harmony / 72eadf94f2cd98702c8c3689d1231ded08feeb0f
Base main / bd18d025067675a4c9acfc4a0c915feccdd27f74

Blockers

  • None

Warnings

  • [medium] GitNexus detect changes did not pass: warning.

Validation Commands

  • npm run verify

Checks

  • passed web-viewer-sample verify (web-viewer-sample)

Human Review Notes

  • OpenSpec archive or formal spec evidence detected; active change-id validation was skipped.
  • Optional AI adapter is not required by policy and was skipped.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/status with a self-healing watchdog and honest 未取得/stale rendering), and incomingHandoff.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 existing GET /api/dev/conversions (new getConversionsHistory + loose DevConversionRecord type in coordinatorClient.ts).
  • EdgeConsole.tsx wraps the app in SharedStatusProvider and 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (5)
web-viewer-sample/e2e/cross-axis-handoff.spec.ts (1)

70-70: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: 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 win

Parallelize the two independent poll requests.

runtimeStatus() and getConversionRecords(100) are independent reads but are awaited sequentially, adding their latencies together on every cycle. Given the watchdog's wedge/staleness detection is tuned to 2 × 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 win

Degraded health has no distinct visual style.

Only .health-unknown gets dimmed styling; .health-degraded shares 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 win

Consider extracting a shared cross-link chip helper.

The onClick={() => { window.location.hash = buildHandoff(target, {...}); }} + evidence-based disabled pattern 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 small CrossLinkBtn({ target, payload, testId, caption, label, disabled }) wrapper around Btn would 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 win

Add 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 with role !== "source_ifc" or missing idempotency_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

📥 Commits

Reviewing files that changed from the base of the PR and between bd18d02 and 72eadf9.

⛔ Files ignored due to path filters (4)
  • docs/evidence/seven-axis-cross-page-harmony/a1-review-not-started.png is excluded by !**/*.png
  • docs/evidence/seven-axis-cross-page-harmony/rail-all-axes.png is excluded by !**/*.png
  • docs/evidence/seven-axis-cross-page-harmony/review-room-stage-truth-not-observed.png is excluded by !**/*.png
  • docs/evidence/seven-axis-cross-page-harmony/ss-to-review.png is excluded by !**/*.png
📒 Files selected for processing (29)
  • docs/evidence/seven-axis-cross-page-harmony/summary.json
  • docs/superpowers/plans/2026-07-03-seven-axis-cross-page-harmony.md
  • docs/superpowers/specs/2026-07-03-seven-axis-cross-page-harmony-design.md
  • web-viewer-sample/e2e/cross-axis-handoff.spec.ts
  • web-viewer-sample/src/console/A1CrossLinks.test.tsx
  • web-viewer-sample/src/console/A1ViewerEmbed.test.tsx
  • web-viewer-sample/src/console/ConversionHistory.test.tsx
  • web-viewer-sample/src/console/CoordinatorCrossLinks.test.tsx
  • web-viewer-sample/src/console/EdgeConsole.sharedstatus.test.tsx
  • web-viewer-sample/src/console/EdgeConsole.tsx
  • web-viewer-sample/src/console/IntakeCrossLinks.test.tsx
  • web-viewer-sample/src/console/KitGpuFleetCrossLinks.test.tsx
  • web-viewer-sample/src/console/MinioCrossLinks.test.tsx
  • web-viewer-sample/src/console/ReviewSessionViewerPane.crosslinks.test.tsx
  • web-viewer-sample/src/console/ReviewSessionViewerPane.tsx
  • web-viewer-sample/src/console/SessionCrossLinks.test.tsx
  • web-viewer-sample/src/console/SharedStatusProvider.test.tsx
  • web-viewer-sample/src/console/SharedStatusProvider.tsx
  • web-viewer-sample/src/console/SharedStatusRail.test.tsx
  • web-viewer-sample/src/console/SharedStatusRail.tsx
  • web-viewer-sample/src/console/coordinatorClient.conversions-history.test.ts
  • web-viewer-sample/src/console/coordinatorClient.ts
  • web-viewer-sample/src/console/edge-console.css
  • web-viewer-sample/src/console/handoff.test.ts
  • web-viewer-sample/src/console/handoff.ts
  • web-viewer-sample/src/console/incomingHandoff.test.tsx
  • web-viewer-sample/src/console/incomingHandoff.tsx
  • web-viewer-sample/src/console/pages.tsx
  • web-viewer-sample/src/console/useSharedStatus.ts

Comment on lines +1 to +9
# 七軸跨頁和諧整合設計(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,不自建。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
# 七軸跨頁和諧整合設計(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

Comment on lines +19 to +36
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);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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.

Comment thread web-viewer-sample/src/console/pages.tsx
Comment thread web-viewer-sample/src/console/pages.tsx
Comment thread web-viewer-sample/src/console/pages.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread web-viewer-sample/src/console/pages.tsx Outdated
// 「在 Review Room 開此 session」「Kit / GPU 機隊」是即時可操作語意,只有 status==='active' 成立;對 closed/closing
// 的過期 session 給滿血按鈕=把過期 session 假裝成真實可操作(比照同分支 0860a54)。「Session 管理」是 lifecycle 全量
// 治理視圖,對已結束 session 給連結語意合理,保留 enabled。
const live = s.status === "active";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread web-viewer-sample/src/console/pages.tsx Outdated
Comment on lines +1525 to +1529
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +337 to +339
if (!h.minio_key) return "not_applicable";
if (minioObjects === null) return "indeterminate";
return minioObjects.some((o) => o.key === h.minio_key);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +937 to +940
if (h.minio_key) {
if (records.some((r) => r.object_key === h.minio_key)) return true;
return recordsTruncated ? "indeterminate" : false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 }); }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

PR Review Agent Summary

Field Value
Status warning
Risk medium
PR 293
Head feat/seven-axis-cross-page-harmony / de1e763b20d962b5acd4795ea6d851ca2da229a7
Base main / bd18d025067675a4c9acfc4a0c915feccdd27f74

Blockers

  • None

Warnings

  • [medium] GitNexus detect changes did not pass: warning.

Validation Commands

  • npm run verify

Checks

  • passed web-viewer-sample verify (web-viewer-sample)

Human Review Notes

  • OpenSpec archive or formal spec evidence detected; active change-id validation was skipped.
  • Optional AI adapter is not required by policy and was skipped.

@monkey1sai

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@monkey1sai
monkey1sai merged commit 7e9a80e into main Jul 4, 2026
12 checks passed
@monkey1sai
monkey1sai deleted the feat/seven-axis-cross-page-harmony branch July 4, 2026 17:32

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +1355 to +1356
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +132 to +134
const sessionCandidates = Object.values(shared.sessionsById)
.filter((s) => s.status === "active" || s.status === "created")
.map((s) => s.session_id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

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.

2 participants