Skip to content

feat(console): 統一治理控制台 MVP 垂直切片(A1–A10 overlay 疊 primary viewer + 三 operator 頁 + 點3D↔GUID + 誠實降級) - #182

Merged
monkey1sai merged 28 commits into
mainfrom
codex/openspec/unified-console-mvp
Jun 4, 2026
Merged

monkey1sai merged 28 commits into
mainfrom
codex/openspec/unified-console-mvp

Conversation

@monkey1sai

@monkey1sai monkey1sai commented Jun 4, 2026 •

Copy link
Copy Markdown
Owner

摘要

實作 unified-governance-console 北極星 spec 的 MVP 垂直切片,並把 A1–A10 治理 overlay 真正接上 live primary viewer(非僅單元測試過的 UI 骨架)。實作主體在 web-viewer-sample client;為讓「overlay 從當前 review session 跑 A3 規則檢核」可行,新增一個最小 coordinator session-scoped rule-run 端點(governance-service 的 rule-run 需 server 端 IFC 路徑、瀏覽器不持有也不手填)。

落地的 5 requirements MVP slice + 閉環:

  1. A1–A10 治理 overlay 疊在 primary viewer live 3D(spectator 唯讀 disabled,非隱藏)
  2. 三個獨立 operator 頁(/console#coordinator|intake|runtime,hash 路由,不混 overlay)
  3. 3D 構件 ↔ IFC GUID 雙向(含 live viewport 點選)+ 治理失敗構件 → client highlightPrimsRequest 經既有 DataChannel 標紅(誠實:送出後等 Kit highlightPrimsResult 確認,不假稱已標示)
  4. MVP identity guid_exact;coverage 來源為 quality_metrics_summary.coverage_ratio(原樣呈現,viewer 不自算);fallback <0.9 誠實降級
  5. 前端只經 coordinator :8004(viewer↔Kit WebRTC DataChannel 為合法 carve-out),誠實 provenance + 後端離線誠實顯錯

閉環(overlay 上可操作): 執行 A3 規則檢核(for-session)→ 真實失敗構件清單 → 點構件在 3D 標紅(Kit 確認)→ A8 從 rule-run 開 BCF issue + 下載。

變更範圍

  • bim-review-coordinator(最小後端):新增 POST /api/governance/rule-runs/for-session/:sessionId — 由 session → model_version_id + externalIfcReadyStore(進件下載的 host IFC 路徑)解析 server IFC 路徑後轉發 governance-service POST /api/rule-runs。coordinator 僅 resolve+forward,不執行 rule-run、不改 data shape、不成為新資料權威。honest 404(無進件 IFC)/ 502(governance 離線)。新增 contract doc + 6 tests。
  • web-viewer-sample(client 主體):governance 純邏輯(MappingCache / GovPanelState / HighlightBridge / govEndpoints)、GovernanceOverlay(A2/A3/A4/A8 asbuilt;A5/A6/A9/A10 p3/p4 disabled;A3 run / 失敗清單 / 3D 標示·清除 / A8 issue·BCF)、IntakeSelectPage(選現成模型·不手填·可開 viewer)、OperatorConsole(三頁殼)、main.tsx 路由 + Window.tsx overlay 掛載 + live 接線(A3 feed / 點選反查 / coverage 來源 / cache rebuild)。零新增生產依賴。

驗證

  • web-viewer-sample:npx tsc --noEmit 0 error;npm run verify(vite build + vitest 111 + struct-log 10)全綠。
  • bim-review-coordinator:npm run verify(build + 279 tests,含 6 個新端點測試)全綠。
  • 既有 viewer 測試 console.test.tsx 19/19 不變;既有 mapping-verification highlight pipeline 未動(gov 用獨立 pending map)。

多層交叉對抗驗證(迭代修畢)

  • 內部 5-lens(opus):honesty / boundary+security / spec-coverage / correctness+regression / placeholder+type+DRY → 0 blocker。
  • 外部 CodeRabbit 複審(round 1):14 findings 全修(coverage degrade 閾值 <0.9、routing root-only、dup-GUID rowKey、radio a11y、coverage null 一致性、observable testid、css、readPage test、doc)。
  • 外部 Codex 複審(round 2):9×P2 揭露 overlay 在 live viewer 接線不完整(govFailedElements 無來源、live 點選未接、intake 不前進、無 A8 動作、coverage 讀錯來源)→ 本輪完整接線全部修畢(含上述最小 coordinator 端點)。誠實註記:dual-layer review 互補,外部複審抓到內部 lens 漏掉的真 bug。

Frontend-operable E2E(AGENTS.md §0.1)

  • Frontend URL:/console#coordinator|intake|runtime(operator 三頁);A1–A10 overlay 疊在 primary viewer。
  • Buttons:A3「執行規則檢核」、失敗構件「在 3D 標示」/「清除 3D 標示」、A8「從 rule-run 開 issue」+「下載 BCF 2.1」、intake「開啟審查 viewer」(皆 spectator disabled / 前置未滿足時 disabled,非假按鈕)。
  • 已驗證(dev server + Chrome,截圖見 transcript):三 operator 頁正確 render + hash 路由 + 無 overlay footer + 邊界文字 + 後端不可達誠實 Failed to fetch。
  • 完整互動 E2E(待 deploy):overlay 疊 live 3D 跑 A3→失敗構件→3D 標紅→A8 開 issue(真 IFC identity 轉檔),於 merge 後 scripts/deploy.ps1 -Build 重建部署環境(coordinator+viewer image)完成並保留環境供檢視(goal docs(openspec): 封存 worker 原始 IFC 檔名追蹤 #18)。overlay 渲染 / props / 誠實文案已由 vitest renderToString 覆蓋。

Deploy

coordinator 與 viewer 為 docker 服務;本 PR 動到 coordinator 端點 + viewer 接線,需 scripts/deploy.ps1 -Build 重建 image(golden path;不新增 root-level script)。

Omniverse 鐵律

依鐵律重查 Kit / USD / Omni UI MCP(本環境仍 403);MVP 不新增任何 Kit/USD/ifcopenshell 契約,只重用既有 AppStreamer.sendMessage + highlightPrimsRequest + 既有 identity profile element_mapping。

誠實鐵律

  • 無假數字;fake/smoke mapping 一律拒;未對映誠實「無法在 3D 標示」+ coverage%(來源 quality_metrics_summary,不自算);3D 標示送出後等 Kit 確認再表態(不假稱已標示);A8 / BCF 前置未滿足誠實 disabled;後端不可達誠實顯錯。

Implements: openspec/specs/unified-governance-console/spec.md + change openspec/changes/unified-console-mvp/。

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Unified governance console overlay: failed-element highlighting with mapping-backed confirmation, rule execution, and issue/BCF export; operator console with hash navigation; intake page now blocks unsafe viewer URLs and disables open when unsafe; honest coverage/readonly banners and clear backend-failure messages.
  • Documentation

    • Added MVP implementation plan, specs, and governance rule-run proxy contract.
  • Tests

    • Expanded end-to-end and unit tests covering overlay, operator pages, mapping, gating, and highlight flows.

monkey1sai and others added 19 commits June 4, 2026 13:01
writing-plans 方法產出:21 個 bite-sized TDD task(RED→GREEN→commit),涵蓋
- Phase A 純邏輯單元:MappingCache(雙向 ifc_guid↔usd_prim_path、拒 fake、誠實 coverage)、
  GovPanelState(spectator 唯讀)、HighlightBridge(client 主動拉 highlightPrimsRequest,未對映誠實回拒)、
  govEndpoints(guid_exact + coverage gate 誠實降級)
- Phase B overlay/operator UI:GovernanceOverlay(A2/A3/A4/A8 + A5/A6/A9/A10 願景 disabled)、
  IntakeSelectPage(選現成模型不手填)、OperatorConsole(三頁獨立殼不含 overlay)
- Phase C 漸進式接 viewer(main.tsx 路由 + Window.tsx 最小掛載,逐步可跑)
- Phase D E2E 驗收(兩份真 IFC identity 轉檔→primary viewer A2/A3/A4→點 failed 構件 3D 標紅→A8 issue/BCF + 截圖)
- Phase E self-review(spec coverage / placeholder scan / type consistency)

零後端改動、只打 coordinator :8004、誠實 provenance、不復活 server-push。純計畫文件,未實作 code。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ersion)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ator=source_ifc_entity_count)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t 主動拉,未對映誠實回拒)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… disabled

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r 文案(誠實非隱藏)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…含治理 overlay)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…narrowing + summary 型別)

Phase A 的 A4 測試檔有 4 個 tsc 型別錯誤(source_ifc_entity_count excess prop + res.reason
未對 discriminated union narrowing)。npm run build=vite build 不做型別檢查故漏掉;改用
toEqual({ok:false,reason}) 斷言(更強、免 narrowing)+ summary intersection 型別。
npx tsc --noEmit 全專案 0 error,vitest 85 passed 不變。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iewer 子樹)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…c_guid

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tBridge.clear()

對抗驗證(5-lens)唯一反覆點名的非阻斷 finding:onClearHighlight 傳入 GovernanceOverlay
卻從未被呼叫(dead wiring),且 HighlightBridge.clear() 無任何 caller。
- GovernanceOverlay:失敗構件 Panel 加「清除 3D 標示」鈕(disabled when !canOperate)→ onClearHighlight 上線。
- handleHighlight 加 !canOperate 早退(防禦縱深,對齊 spec「spectator SHALL NOT 觸發」;按鈕已 disabled,這是第二道)。
- 移除無 caller 的 HighlightBridge.clear()(YAGNI)。
tsc --noEmit 0 error,vitest 91 passed(含新增 clear 鈕斷言)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 4, 2026 06:11
@coderabbitai

coderabbitai Bot commented Jun 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@monkey1sai, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 38 minutes and 2 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0e9541e-4bf3-461e-bc47-e3b4fe5ed065

📥 Commits

Reviewing files that changed from the base of the PR and between c6f0439 and 341a376.

📒 Files selected for processing (3)
  • web-viewer-sample/src/Window.tsx
  • web-viewer-sample/src/console/IntakeSelectPage.test.tsx
  • web-viewer-sample/src/console/IntakeSelectPage.tsx
📝 Walkthrough

Walkthrough

Adds a frontend-operable Unified Governance Console MVP: client governance logic (mapping, panel state, coverage gate), HighlightBridge, GovernanceOverlay UI/CSS, operator pages, viewer wiring in Window/main, a coordinator session-scoped proxy, tests, and docs.

Changes

Unified Governance Console MVP

Layer / File(s) Summary
Implementation plan and specs
docs/superpowers/plans/2026-06-04-unified-console-mvp.md, openspec/changes/unified-console-mvp/*, docs/contracts/governance-rule-run-proxy.md
Phased plan (A–E), proposal/spec/tasks, and proxy contract documenting resolve+forward semantics, error mapping, and test references.
Core governance logic: mapping, state, coverage
src/console/governance/mappingCache.ts, src/console/governance/mappingCache.test.ts, src/console/governance/govPanelState.ts, src/console/governance/govPanelState.test.ts, src/console/governance/govEndpoints.ts, src/console/governance/govEndpoints.test.ts
MappingCache: bidirectional GUID↔prim lookups with fake-mapping isolation and ancestor lookup plus version locking. GovPanelState: spectator/primary, stream readiness, and lifecycleActive gating. govEndpoints: coverage gating constants and evaluateCoverageGate with degrade/warn semantics. Tests included.
HighlightBridge: failed element -> DataChannel
src/console/governance/highlightBridge.ts, src/console/governance/highlightBridge.test.ts
Transforms failed elements into highlightPrimsRequest messages (color, label, issue_id, source), checks DataChannel readiness and mapping cache, returns explicit ok/unmapped/datachannel_not_ready outcomes; includes severity normalization. Tests verify mapped/unmapped/fake/not-ready behavior.
Overlay UI and styles
src/console/GovernanceOverlay.tsx, src/console/GovernanceOverlay.test.tsx, src/console/governance/overlay.css
GovernanceOverlay renders MVP engine list, rule-run controls, failed-elements table with per-row highlight and highlight-confirmation display, coverage metrics with degraded/warn messaging, and A8 issue/BCF controls. CSS added; renderToString and jsdom tests cover spectator/waiting/coverage/highlight/issue states.
Operator pages and intake selection
src/console/OperatorConsole.tsx, src/console/OperatorConsole.test.tsx, src/console/IntakeSelectPage.tsx, src/console/IntakeSelectPage.test.tsx
OperatorConsole provides hash-based operator pages (coordinator/intake/runtime) and excludes governance overlay; IntakeSelectPage lists coordinator IFC-ready jobs (filtered) and gates viewer opening via isSafeViewerUrl. Tests validate routing, provenance, and blocked-open behavior.
Routing and overlay glue
src/console/routing.ts, src/console/routing.test.ts, src/console/governance/windowOverlayGlue.ts, src/console/governance/windowOverlayGlue.test.ts
Routing helper detects operator console paths (pathname/hash/search-aware) and short-hash forms; windowOverlayGlue derives OverlayInputs (streamRole, dataChannelReady, panelState) from viewer status with lifecycleActive defaulting behavior.
Viewer integration: Window.tsx and main.tsx
src/Window.tsx, src/main.tsx
Window.tsx mounts GovernanceOverlay, manages model-version-scoped MappingCache, routes USD selection to reverse GUID lookup, implements overlay highlight flow, rule-run polling and issue creation, and processes highlightPrimsResult confirmations; main.tsx uses isOperatorConsolePath to mount OperatorConsole when appropriate.
Coordinator proxy and tests
bim-review-coordinator/src/routes/governanceProxy.ts, bim-review-coordinator/src/app.ts, bim-review-coordinator/tests/governance-rule-run-for-session.test.ts
registerGovernanceProxy accepts resolver deps and adds POST /api/governance/rule-runs/for-session/:sessionId that resolves session→ifc_source_path (host_local_path fallback), validates IDs, forwards payload upstream, and maps errors (400/404/501/502). Integration tests cover resolution, passthrough overrides, and honest failure behavior.
Client helpers
src/console/governanceClient.ts, src/console/components.tsx
Added governanceClient.createRuleRunForSession wrapper and optional data-testid prop on Btn to support tests and server-render checks.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

  • monkey1sai/AI-BIM-governance#153: Prior work extending the coordinator governance proxy and related forwarding logic (overlapping area in governanceProxy changes).
  • monkey1sai/AI-BIM-governance#90: Earlier removal of predecessor highlighting logic; this PR provides a new viewer-side highlight flow and MappingCache-based wiring.

"🐰
I nibble truth into each line,
No made-up GUIDs, only signs.
Spectators watch as prims glow true,
A tiny console — honest view.
Hop, review, and ship it through!"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/openspec/unified-console-mvp

@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: f85807acde

ℹ️ 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".

<GovernanceOverlay
panelState={inputs.panelState}
coverage={coverage}
failedElements={this.state.govFailedElements ?? []}

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 Populate overlay failures before rendering actions

When the primary viewer overlay is rendered after an A3 rule-run, this prop still always resolves to an empty array: the only references to govFailedElements in the repo are the optional state declaration and this read, with no fetch/socket/rule-run handler ever assigning it. As a result the new overlay always shows “no failed elements” and the advertised failed-element → highlightPrimsRequest path is unreachable from the live viewer.

Useful? React with 👍 / 👎.

Comment thread web-viewer-sample/src/Window.tsx Outdated
Comment on lines +1290 to +1292
if (!this._mappingCache || !this._mappingCache.belongsTo(mvId)) {
this._mappingCache = MappingCache.fromDocument(payload, mvId);
}

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 Refresh the cache when the mapping source changes

After one mapping is loaded, this guard only keys the cache by currentModelVersionId, so selecting another asset with a different mapping_url but the same model version—or an asset with no mapping URL—leaves _mappingCache pointing at the previous mapping. In that scenario the overlay coverage and failed-element highlighting can use stale prim paths even though the visible mapping state was reset for the newly selected asset.

Useful? React with 👍 / 👎.

Comment on lines +11 to +15
function readPage(): OperatorPage {
const h = window.location.hash.replace(/^#\/?console\/?/, "").replace(/^#/, "");
if (h === "intake") return "intake";
if (h === "runtime") return "runtime";
return "coordinator";

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 Parse path segments for operator pages

For direct links such as /console/intake or /console/runtime, main.tsx correctly mounts OperatorConsole, but this router only inspects window.location.hash, so both URLs render the coordinator page instead of the requested operator page. This breaks deep links/bookmarks for the pathname form that the new routing predicate and file header advertise; derive the page from pathname as well as the hash.

Useful? React with 👍 / 👎.

Comment on lines +47 to +48
this.deps.sendMessage(buildHighlightPrimsRequest([item], true, requestId));
return { ok: true, primPath, requestId };

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 Wait for Kit highlight results before reporting success

When Kit returns highlightPrimsResult with missing_paths or a fallback path, this code still reports success immediately because sendMessage only queues the request and the generated gov-highlight-* IDs are never tracked by the existing result handler. In that failure scenario the overlay displays “已在 3D 標示” even though Kit did not actually select the requested prim, so the operator gets a false confirmation.

Useful? React with 👍 / 👎.

Comment on lines +51 to +55
<input
type="radio"
name="intake-model"
checked={selected === j.ifc_ready_job_id}
onChange={() => setSelected(j.ifc_ready_job_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 Make intake selection advance the workflow

On the new /console#intake page, selecting a ready job only stores its id in local React state and that selected value is never used to open viewer_url, call /ui/open, or otherwise bind/continue the intake. For any listed job with a review_session_id, the operator can click the advertised “選取” control but cannot proceed from this page, so the A1 model-selection flow is effectively inert.

Useful? React with 👍 / 👎.

Comment on lines +95 to +96
{props.failedElements.slice(0, 50).map((f) => (
<tr key={f.ifc_guid}>

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 Use a unique key for per-rule failures

If the same IFC element fails multiple governance rules, failedElements will contain multiple rows with the same ifc_guid, but this table uses only the GUID as the React key and lastResult index. In that scenario React reuses duplicate keyed rows and highlight status for one rule can be shown on another row, making the per-rule failure list misleading; include the rule code or another stable row id in the key/state key.

Useful? React with 👍 / 👎.

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 implements the unified-governance-console MVP vertical slice entirely within the web-viewer-sample client boundary: it adds an A1–A10 governance overlay rendered on top of the primary viewer’s live 3D stream, plus a separate operator console with 3 standalone pages (coordinator, intake, runtime) routed via /console and #/console/.... It also introduces client-side building blocks (MappingCache, HighlightBridge, panel state + coverage gating) to support 3D selection ↔ IFC GUID mapping and client-initiated 3D highlighting via the existing WebRTC DataChannel message builders.

Changes:

  • Add a governance overlay UI (GovernanceOverlay) mounted in the viewer (Window.tsx) and wired to highlight requests over the existing DataChannel path.
  • Add an operator console shell + hash routing (OperatorConsole, routing.ts) and an A1 intake “select from ifc-ready list” page (IntakeSelectPage).
  • Introduce governance logic modules with unit tests: MappingCache (bidirectional lookup + coverage), HighlightBridge (highlightPrimsRequest), GovPanelState (spectator/ready gating), and coverage evaluation (guid_exact/coverage policy).

Reviewed changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
web-viewer-sample/src/Window.tsx Mounts the governance overlay in the primary viewer and wires MappingCache + DataChannel highlight requests.
web-viewer-sample/src/main.tsx Routes /console and #/console/... to the new OperatorConsole; keeps viewer App as default.
web-viewer-sample/src/console/routing.ts Pure function to detect whether current location should mount OperatorConsole.
web-viewer-sample/src/console/routing.test.ts Unit tests for operator-console route detection.
web-viewer-sample/src/console/OperatorConsole.tsx Operator console shell with 3 pages and hash navigation.
web-viewer-sample/src/console/OperatorConsole.test.tsx Smoke tests ensuring operator pages render and don’t include the viewer overlay.
web-viewer-sample/src/console/IntakeSelectPage.tsx A1 intake page listing selectable models from /api/external/ifc-ready (via coordinator).
web-viewer-sample/src/console/IntakeSelectPage.test.tsx Smoke tests for intake UI (no manual path input; boundary/port expectations).
web-viewer-sample/src/console/GovernanceOverlay.tsx Overlay UI (A2/A3/A4/A8 “asbuilt” + A5/A6/A9/A10 disabled) + failed-element highlight controls.
web-viewer-sample/src/console/GovernanceOverlay.test.tsx Overlay renderToString smoke tests (engines shown, disabled roadmap, spectator/read-only states, degraded coverage messaging).
web-viewer-sample/src/console/governance/windowOverlayGlue.ts Pure “viewer status → overlay inputs/panel state” glue function.
web-viewer-sample/src/console/governance/windowOverlayGlue.test.ts Unit tests for glue logic.
web-viewer-sample/src/console/governance/overlay.css Overlay container styling (absolute overlay on viewer).
web-viewer-sample/src/console/governance/mappingCache.ts Bidirectional GUID↔prim-path cache with fake isolation + coverage ratio.
web-viewer-sample/src/console/governance/mappingCache.test.ts Unit tests for MappingCache behavior (lookup, fake isolation, coverage, version binding).
web-viewer-sample/src/console/governance/highlightBridge.ts Builds/sends highlightPrimsRequest using MappingCache + injected sender and readiness checks.
web-viewer-sample/src/console/governance/highlightBridge.test.ts Unit tests verifying correct message emission and honest failure modes.
web-viewer-sample/src/console/governance/govPanelState.ts Centralized “can operate vs spectator/waiting” state + UI text.
web-viewer-sample/src/console/governance/govPanelState.test.ts Unit tests for GovPanelState resolution.
web-viewer-sample/src/console/governance/govEndpoints.ts MVP identity/coverage constants + coverage gate evaluation.
web-viewer-sample/src/console/governance/govEndpoints.test.ts Unit tests for coverage gate behaviors.
docs/superpowers/plans/2026-06-04-unified-console-mvp.md Implementation plan and task breakdown for the MVP.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +80 to +88
{props.coverage.degraded && (
<div className="gov-banner">
coverage {coveragePct === null ? "未知" : `${coveragePct}%`}(&lt; 100%):部分未對映構件
<strong> 無法在 3D 標示</strong>,依既有 spec 誠實降級,不捏造 prim path。
</div>
)}
{!props.coverage.degraded && coveragePct !== null && (
<Metric value={`${coveragePct}%`} label="mapping coverage" />
)}
Comment on lines +42 to +53
const handleHighlight = (failed: FailedElement) => {
// 防禦縱深:!canOperate(spectator / 未就緒)時不觸發治理動作;按鈕已 disabled,這是第二道保險(對齊 spec「spectator SHALL NOT 觸發」)。
if (!props.panelState.canOperate) return;
const res = props.onHighlight(failed);
setLastResult((prev) => ({
...prev,
[failed.ifc_guid]: res.ok
? `已在 3D 標示:${res.primPath}`
: res.reason === "unmapped"
? "無法在 3D 標示(未對映 usd_prim_path)"
: "等待 viewer 連線(DataChannel 未就緒)",
}));
Comment on lines +95 to +105
{props.failedElements.slice(0, 50).map((f) => (
<tr key={f.ifc_guid}>
<td>{f.rule_code ?? "—"}</td>
<td>{f.severity}</td>
<td>{f.ifc_guid}</td>
<td>
<Btn caption="highlightPrimsRequest(client 主動拉)" disabled={!props.panelState.canOperate} onClick={() => handleHighlight(f)}>
在 3D 標示
</Btn>
{lastResult[f.ifc_guid] && <span className="ec-note" style={{ marginLeft: 6 }}>{lastResult[f.ifc_guid]}</span>}
</td>
Comment on lines +51 to +56
<input
type="radio"
name="intake-model"
checked={selected === j.ifc_ready_job_id}
onChange={() => setSelected(j.ifc_ready_job_id)}
/>
Comment on lines +11 to +14
it("一般 viewer 路徑(含 ?session=)→ 非 operator(維持 <App/>)", () => {
expect(isOperatorConsolePath("/", "")).toBe(false);
expect(isOperatorConsolePath("/", "")).toBe(false);
expect(isOperatorConsolePath("/viewer", "")).toBe(false);
Comment on lines +85 to +89
- 單一測試檔:`npm run test -- src/console/governance/mappingCache.test.ts`(vitest run,`vitest.config.ts` 已設 jsdom + globals)。
- 全部測試:`npm run test`(= `vitest run`)。
- type + build:`npm run build`(= `vite build`,會跑 tsc 型別檢查)。
- 提交前 gate(與 repo 一致):`npm run verify`(= `npm run build && npm run test && npm run test:struct-log`)。
- **baseline(動手前先量)**:第一個 task 前先在 `web-viewer-sample/` 跑一次 `npm run test` 與 `npm run build`,記下現狀通過數,作為 keep/discard 比較基準。
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

PR Review Agent Summary

Field Value
Status blocked
Risk high
PR 182
Head codex/openspec/unified-console-mvp / f85807acde56bcf43d48b415cf0b267198ec4374
Base main / bafb013f9772c632a555d9014139c29ef9fa3517

Blockers

  • [high] Behavior, workflow, code, or repo-boundary changes require an OpenSpec change id or documented exception.

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

  • Optional AI adapter is not required by policy and was skipped.

@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: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web-viewer-sample/src/Window.tsx (1)

1266-1274: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset and refresh MappingCache on every mapping load outcome.

Line 1290 only refreshes cache on model-version change, and Line 1266-1274 / catch path don’t clear prior cache. This can leave stale GUID↔prim mappings active after mapping URL/doc changes, producing incorrect highlights.

Proposed fix
if (!mappingUrl) {
+    this._mappingCache = null;
     this.setState({
         mappingStatus: "沒有 mapping_url,無法載入 element_mapping.json",
@@
-const mvId = this.state.currentModelVersionId;
-if (!this._mappingCache || !this._mappingCache.belongsTo(mvId)) {
-    this._mappingCache = MappingCache.fromDocument(payload, mvId);
-}
+const mvId = this.state.currentModelVersionId;
+this._mappingCache = MappingCache.fromDocument(payload, mvId);
@@
} catch (error) {
+    this._mappingCache = null;
     const message = error instanceof Error ? error.message : String(error);

Also applies to: 1287-1292, 1321-1331

🤖 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/Window.tsx` around lines 1266 - 1274, When a mapping
load ends early (e.g., when mappingUrl is falsy in the if block) or fails in the
catch path, you must also clear and refresh the MappingCache to avoid stale
GUID↔prim mappings; update the early-return branch that calls this.setState({...
mappingItems: [], mappingSummary: null, selectedMappingIndex: 0,
mappingVerificationBlockedReason: null }) and every catch/failure path to also
reset the mapping cache (for example by calling MappingCache.reset() or
recreating the MappingCache instance) and then trigger the same cache refresh
logic the model-version-change path uses so highlights are based on the
new/empty mapping. Ensure the same change is applied to the other
mapping-failure/empty branches that set mappingItems/mappingSummary (the blocks
around the mappingUrl check, the catch path, and the code analogous to the
model-version-change refresh).
🧹 Nitpick comments (2)
web-viewer-sample/src/console/routing.test.ts (1)

11-15: ⚡ Quick win

Add a non-root console negative test to lock the route contract.

Please add a regression assertion that paths like /foo/console are treated as non-operator routes.

Suggested test addition
   it("一般 viewer 路徑(含 ?session=)→ 非 operator(維持 <App/>)", () => {
     expect(isOperatorConsolePath("/", "")).toBe(false);
     expect(isOperatorConsolePath("/", "")).toBe(false);
     expect(isOperatorConsolePath("/viewer", "")).toBe(false);
+    expect(isOperatorConsolePath("/foo/console", "")).toBe(false);
   });

Based on learnings: "Source changes must check related public API, protocol, UI flow, and test impact".

🤖 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/routing.test.ts` around lines 11 - 15, Add a
regression test to ensure non-root console paths are treated as non-operator:
update routing.test.ts to call isOperatorConsolePath with a non-root path like
"/foo/console" (and optionally with query parts like "?session=") and assert it
returns false; this will lock the route contract for the isOperatorConsolePath
function and prevent regressions that treat "/foo/console" as an operator route.
web-viewer-sample/src/console/governance/windowOverlayGlue.test.ts (1)

6-21: ⚡ Quick win

Assert dataChannelReady and spectator-precedence explicitly.

The suite doesn’t verify dataChannelReady, and it misses the spectator: true + streamReady: false precedence case. Add these assertions so the full OverlayInputs contract is guarded.

Based on learnings: Applies to web-viewer-sample/src/**/*.{ts,tsx,js,jsx} : Source changes must check related public API, protocol, UI flow, and test impact.

🤖 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/governance/windowOverlayGlue.test.ts` around
lines 6 - 21, The tests for deriveOverlayInputs lack assertions for
dataChannelReady and miss the spectator-precedence scenario; update the existing
cases (the "primary + stream 已連線有畫面" and "primary + 串流未就緒 → 等待 viewer") to
assert r.dataChannelReady is set appropriately, and add a new test calling
deriveOverlayInputs({ spectator: true, streamReady: false }) that asserts
r.streamRole === "spectator", r.panelState.canOperate === false,
r.panelState.disabledReason === "spectator_read_only", and r.dataChannelReady is
false to ensure spectator precedence and the full OverlayInputs contract
(streamRole, dataChannelReady, panelState.{canOperate,disabledReason}) are
covered.
🤖 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/plans/2026-06-04-unified-console-mvp.md`:
- Line 62: The markdown table row contains an inline code span with unescaped
pipe characters (`#/console/coordinator|intake|runtime`) which breaks the table
(MD056); update the table cell in
docs/superpowers/plans/2026-06-04-unified-console-mvp.md by escaping the
internal pipes (e.g., replace `|` with `\|` inside the code span) or by wrapping
the route in a fenced code block so the `OperatorConsole.tsx` route example
remains a single table column and the table renders correctly.
- Around line 16-19: Update the "北極星 source of truth:" section to follow the
repository docs source-of-truth order: do not present docs as the primary
authority; instead state that source code and contracts are the authoritative
sources and list the current markdown files (e.g.,
openspec/specs/unified-governance-console/spec.md and
docs/superpowers/specs/2026-06-04-unified-governance-console-design.md) as
supporting references under code/contracts authority; specifically replace the
wording that elevates docs to "north-star" and add a brief note that docs are
supplemental to source code/contracts per the repo policy.

In `@web-viewer-sample/src/console/governance/govEndpoints.test.ts`:
- Around line 17-34: Add a test that covers the boundary band where coverage is
between 0.9 (inclusive) and 1.0 (exclusive): call evaluateCoverageGate with a
coverageRatio like 0.95 and isFake: false and assert the gate reports coverageOk
true, degraded false, and warnOnly true (ensuring the non-degraded warning band
is enforced); place this alongside the existing tests in govEndpoints.test.ts so
regressions to the 0.9 threshold are caught.

In `@web-viewer-sample/src/console/governance/govEndpoints.ts`:
- Around line 27-35: In evaluateCoverageGate, change the degraded calculation so
it uses LOW_COVERAGE_THRESHOLD instead of treating any non-1.0 coverage as
degraded: keep coverageOk computed as input.coverageRatio >= MVP_MIN_COVERAGE,
but set degraded to input.coverageRatio < LOW_COVERAGE_THRESHOLD (and keep
warnOnly tied to !coverageOk as before); update the return object for the
non-fake branch accordingly so cases like 0.95 are not marked degraded while
still failing coverage if below MVP_MIN_COVERAGE.

In `@web-viewer-sample/src/console/governance/overlay.css`:
- Line 7: The font shorthand declaration containing ui-monospace, "Cascadia
Code", "Consolas", monospace in the overlay.css file violates stylelint's
font-family-name-quotes rule; update the font property (the 'font' shorthand) to
remove the quotes around Consolas (leave "Cascadia Code" quoted because it
contains a space) so the list reads ui-monospace, "Cascadia Code", Consolas,
monospace.

In `@web-viewer-sample/src/console/GovernanceOverlay.tsx`:
- Around line 57-103: Add stable observable IDs (e.g. data-testid or data-qa) to
all core overlay controls and stateful UI elements in GovernanceOverlay.tsx so
E2E tests can reliably target them: add attributes to the readonly wrapper div
(gov-overlay), the degraded banner (props.coverage.degraded), the clear
highlight Btn (actions prop using onClearHighlight), each highlight Btn rendered
in handleHighlight, the mapping coverage Metric, and the failedElements
rows/cells (keyed by f.ifc_guid). Use consistent keys like
data-testid="gov-overlay", "gov-readonly-banner", "gov-degraded-banner",
"btn-clear-highlight", "btn-highlight-{ifc_guid}", "metric-coverage" and
"row-failed-{ifc_guid}" so Playwright/Chrome tests can locate
route/button/default-fixture/loading/success/failure/retry states without
relying on text content; ensure attributes are added on ProvTag/Panel/Btn usages
and table row <tr> elements referenced by props.failedElements and
props.panelState.canOperate.
- Around line 39-54: The state and rendering currently index rows by ifc_guid
causing collisions when the same GUID appears under different rule_code; update
to use a stable per-row key (e.g., compositeKey =
`${failed.ifc_guid}::${failed.rule_code}` or a unique rowId from FailedElement)
everywhere you reference ifc_guid for identity: change setLastResult to store
and read using that composite key and update any JSX row key/lookup that used
failed.ifc_guid to use the new compositeKey so each row’s highlight state is
isolated (refer to handleHighlight, lastResult, FailedElement and the places
rendering rows/keys).

In `@web-viewer-sample/src/console/IntakeSelectPage.test.tsx`:
- Around line 7-23: The tests in IntakeSelectPage.test.tsx only render static
SSR with renderToString so use the React testing utilities to exercise the async
intake flow: replace renderToString(<IntakeSelectPage />) with a client-side
render (e.g., `@testing-library/react` render) and run effects using
act/async-await; mock coordinatorClient.listIfcReady to emit loading, success
(with entries that both match and do not match expected_stage_url) and failure
responses so you can assert the UI shows loading state, filters results by
expected_stage_url, displays success items, shows an error message on failure,
and supports a retry path that re-triggers coordinatorClient.listIfcReady;
reference IntakeSelectPage and coordinatorClient.listIfcReady when locating the
implementation to mock and the UI elements to assert.

In `@web-viewer-sample/src/console/IntakeSelectPage.tsx`:
- Around line 51-56: The radio input lacks an accessible name; update the radio
in IntakeSelectPage (the input using checked={selected === j.ifc_ready_job_id}
and onChange={() => setSelected(j.ifc_ready_job_id)}) to include a proper label:
give the input a stable id (e.g., `intake-${j.ifc_ready_job_id}`) and wrap it
with or reference a <label> that contains descriptive text (preferably a job
title like j.name or fallback to j.ifc_ready_job_id), or if visible text isn’t
appropriate, add aria-label or aria-labelledby pointing to a nearby descriptive
element so each radio option is announced to screen readers.
- Around line 39-45: Add stable observable IDs and default-fixture hooks for E2E
evidence: in IntakeSelectPage.tsx, add a route ID for the page container, a
data-testid/id on the retry button rendered by Btn (reference: Btn, prop
onClick={load}, busy), and data-testid/id attributes for loading state (busy),
error display (err), empty-state paragraph (jobs.length === 0 branch), and
success/results table (table element). Ensure IDs are stable strings like
data-testid="intake-page", "intake-retry-btn", "intake-loading", "intake-error",
"intake-empty", "intake-table" and include a default fixture hook or attribute
on the container to allow Playwright to mount deterministic data for
loading/success/failure/retry scenarios.

In `@web-viewer-sample/src/console/OperatorConsole.test.tsx`:
- Around line 7-23: The tests only render OperatorBody directly and miss
verifying OperatorConsole's hash-routing behavior; add a test(s) that mounts
OperatorConsole and asserts readPage()/hashchange handling and nav button clicks
via go() update the displayed page: 1) simulate different window.location.hash
values and dispatch a "hashchange" event then assert OperatorBody content
changes accordingly; 2) simulate clicking the console's nav buttons (or call the
public go() method) and assert the URL hash and rendered OperatorBody update;
reference OperatorConsole, OperatorBody, readPage, go, and the "hashchange"
event when locating where to add these assertions.

In `@web-viewer-sample/src/console/OperatorConsole.tsx`:
- Around line 46-53: The nav buttons and active page container lack stable
observable IDs required for E2E assertions; update the NAV rendering and
OperatorBody wrapper so each button rendered from NAV includes a stable
attribute (e.g., data-testid or id) that incorporates the route key (reference:
NAV, the map callback that renders buttons and onClick => go), and ensure the
active page container (where <OperatorBody page={page} /> is mounted) has a
stable observable ID that reflects the current page (reference: page and
OperatorBody). Add these attributes to the button element and to the <main> or a
wrapping element around OperatorBody so tests can reliably target route buttons
and the active view.

In `@web-viewer-sample/src/console/routing.ts`:
- Line 5: The current regex test on pathname matches any segment containing
"console" (e.g., "/foo/console"), so restrict it to only the root console route
by replacing the loose regex test on the pathname with a check that matches
exactly "/console" (optionally allowing a trailing slash) where the
OperatorConsole is mounted; update the conditional that uses pathname (the if
(/(^|\/)console(\/|$)/.test(pathname)) check) to an exact-match check (e.g.,
pathname === "/console" or an anchoring regex that only matches the root) so
OperatorConsole only mounts at the intended entry route.

In `@web-viewer-sample/src/Window.tsx`:
- Around line 1904-1910: The unknown-mapping branch currently returns
{coverageOk:false, degraded:false, ratio:null} which incorrectly hides the
degraded/warn state; instead call evaluateCoverageGate for the unknown case with
isFake true (or equivalent sentinel) so the gate determines coverageOk and
degraded consistently. Locate the coverage construction around
this._mappingCache and replace the fallback object with the result of
evaluateCoverageGate({ coverageRatio: ratio, isFake: true }) (and return its
coverageOk, degraded and ratio) so unknown mapping is treated as
degraded/warn-only per the gate contract.

---

Outside diff comments:
In `@web-viewer-sample/src/Window.tsx`:
- Around line 1266-1274: When a mapping load ends early (e.g., when mappingUrl
is falsy in the if block) or fails in the catch path, you must also clear and
refresh the MappingCache to avoid stale GUID↔prim mappings; update the
early-return branch that calls this.setState({... mappingItems: [],
mappingSummary: null, selectedMappingIndex: 0, mappingVerificationBlockedReason:
null }) and every catch/failure path to also reset the mapping cache (for
example by calling MappingCache.reset() or recreating the MappingCache instance)
and then trigger the same cache refresh logic the model-version-change path uses
so highlights are based on the new/empty mapping. Ensure the same change is
applied to the other mapping-failure/empty branches that set
mappingItems/mappingSummary (the blocks around the mappingUrl check, the catch
path, and the code analogous to the model-version-change refresh).

---

Nitpick comments:
In `@web-viewer-sample/src/console/governance/windowOverlayGlue.test.ts`:
- Around line 6-21: The tests for deriveOverlayInputs lack assertions for
dataChannelReady and miss the spectator-precedence scenario; update the existing
cases (the "primary + stream 已連線有畫面" and "primary + 串流未就緒 → 等待 viewer") to
assert r.dataChannelReady is set appropriately, and add a new test calling
deriveOverlayInputs({ spectator: true, streamReady: false }) that asserts
r.streamRole === "spectator", r.panelState.canOperate === false,
r.panelState.disabledReason === "spectator_read_only", and r.dataChannelReady is
false to ensure spectator precedence and the full OverlayInputs contract
(streamRole, dataChannelReady, panelState.{canOperate,disabledReason}) are
covered.

In `@web-viewer-sample/src/console/routing.test.ts`:
- Around line 11-15: Add a regression test to ensure non-root console paths are
treated as non-operator: update routing.test.ts to call isOperatorConsolePath
with a non-root path like "/foo/console" (and optionally with query parts like
"?session=") and assert it returns false; this will lock the route contract for
the isOperatorConsolePath function and prevent regressions that treat
"/foo/console" as an operator route.
🪄 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: 57e913ab-0549-43a4-a3ae-7f7df51f34c7

📥 Commits

Reviewing files that changed from the base of the PR and between bafb013 and f85807a.

📒 Files selected for processing (23)
  • docs/superpowers/plans/2026-06-04-unified-console-mvp.md
  • web-viewer-sample/src/Window.tsx
  • web-viewer-sample/src/console/GovernanceOverlay.test.tsx
  • web-viewer-sample/src/console/GovernanceOverlay.tsx
  • web-viewer-sample/src/console/IntakeSelectPage.test.tsx
  • web-viewer-sample/src/console/IntakeSelectPage.tsx
  • web-viewer-sample/src/console/OperatorConsole.test.tsx
  • web-viewer-sample/src/console/OperatorConsole.tsx
  • web-viewer-sample/src/console/governance/.gitkeep
  • web-viewer-sample/src/console/governance/govEndpoints.test.ts
  • web-viewer-sample/src/console/governance/govEndpoints.ts
  • web-viewer-sample/src/console/governance/govPanelState.test.ts
  • web-viewer-sample/src/console/governance/govPanelState.ts
  • web-viewer-sample/src/console/governance/highlightBridge.test.ts
  • web-viewer-sample/src/console/governance/highlightBridge.ts
  • web-viewer-sample/src/console/governance/mappingCache.test.ts
  • web-viewer-sample/src/console/governance/mappingCache.ts
  • web-viewer-sample/src/console/governance/overlay.css
  • web-viewer-sample/src/console/governance/windowOverlayGlue.test.ts
  • web-viewer-sample/src/console/governance/windowOverlayGlue.ts
  • web-viewer-sample/src/console/routing.test.ts
  • web-viewer-sample/src/console/routing.ts
  • web-viewer-sample/src/main.tsx

Comment thread docs/superpowers/plans/2026-06-04-unified-console-mvp.md Outdated
Comment thread docs/superpowers/plans/2026-06-04-unified-console-mvp.md Outdated
Comment thread web-viewer-sample/src/console/governance/govEndpoints.test.ts
Comment thread web-viewer-sample/src/console/governance/govEndpoints.ts
Comment thread web-viewer-sample/src/console/governance/overlay.css Outdated
Comment thread web-viewer-sample/src/console/IntakeSelectPage.tsx
Comment on lines +7 to +23
it("coordinator 頁渲染 Coordinator 控制台且不含治理 overlay 容器", () => {
const html = renderToString(<OperatorBody page="coordinator" />);
expect(html).toContain("Coordinator"); // 控制台
expect(html).not.toContain("gov-overlay"); // 不混入 A1–A10 overlay
});

it("intake 頁渲染選現成模型且不含治理 overlay", () => {
const html = renderToString(<OperatorBody page="intake" />);
expect(html).toContain("選取現成模型");
expect(html).not.toContain("gov-overlay");
});

it("runtime 頁渲染 Runtime 狀態且不含治理 overlay", () => {
const html = renderToString(<OperatorBody page="runtime" />);
expect(html).toContain("Runtime");
expect(html).not.toContain("gov-overlay");
});

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add hash-routing interaction coverage for OperatorConsole.

Line 7-23 only verifies OperatorBody rendering; it does not test readPage(), hashchange handling, or nav button clicks (go) in OperatorConsole. Route regressions would be missed.

Based on learnings, source changes must check related public API, protocol, UI flow, and test impact.

🤖 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/OperatorConsole.test.tsx` around lines 7 - 23,
The tests only render OperatorBody directly and miss verifying OperatorConsole's
hash-routing behavior; add a test(s) that mounts OperatorConsole and asserts
readPage()/hashchange handling and nav button clicks via go() update the
displayed page: 1) simulate different window.location.hash values and dispatch a
"hashchange" event then assert OperatorBody content changes accordingly; 2)
simulate clicking the console's nav buttons (or call the public go() method) and
assert the URL hash and rendered OperatorBody update; reference OperatorConsole,
OperatorBody, readPage, go, and the "hashchange" event when locating where to
add these assertions.

Comment thread web-viewer-sample/src/console/OperatorConsole.tsx
Comment thread web-viewer-sample/src/console/routing.ts Outdated
Comment thread web-viewer-sample/src/Window.tsx Outdated
… 預期的實作交付)

pr-review-agent gate 要求 code 變更須對應 active OpenSpec change id(branch=codex/openspec/unified-console-mvp
卻無 openspec/changes/unified-console-mvp/)。archived 2026-06-04-unified-governance-console 為純規格 change,
明文「後續實作 change 依本 capability 為北極星」;本 change 即該預期的實作交付。
- proposal/tasks:MVP 垂直切片 client-only 實作(A/B/C phase + 驗證 + 對抗 + E2E)。
- spec delta:ADD 一項 frontend-operable 交付驗收 requirement(對齊 AGENTS.md §0.1,不改既有 5 行為要求)。
npx openspec validate unified-console-mvp --strict → valid;--all --strict → 42 passed/0 failed。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

PR Review Agent Summary

Field Value
Status warning
Risk medium
PR 182
Head codex/openspec/unified-console-mvp / 5c11e7a113dc4d6f4509df679ec4b9751450938a
Base main / bafb013f9772c632a555d9014139c29ef9fa3517

Blockers

  • None

Warnings

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

Validation Commands

  • openspec validate unified-console-mvp
  • npm run verify

Checks

  • passed openspec validate unified-console-mvp (openspec)
  • passed web-viewer-sample verify (web-viewer-sample)

Human Review Notes

  • OpenSpec changes detected: unified-console-mvp
  • Optional AI adapter is not required by policy and was skipped.

@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: 5c11e7a113

ℹ️ 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 on lines +11 to +15
function readPage(): OperatorPage {
const h = window.location.hash.replace(/^#\/?console\/?/, "").replace(/^#/, "");
if (h === "intake") return "intake";
if (h === "runtime") return "runtime";
return "coordinator";

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 Restore existing console hash pages

When /console is mounted, readPage() now only recognizes intake and runtime and falls back to coordinator, while main.tsx no longer mounts the existing EdgeConsole. This means existing operator links such as /console#issues, /console#review, /console#semantic, and /console#app/... now render the coordinator page even though those page components still exist in EdgeConsole, so the A1 rule-run/review-room/semantic workflows become unreachable from their prior URLs.

Useful? React with 👍 / 👎.

Comment on lines +3 to +7
.gov-overlay {
position: absolute; top: 0; right: 0; width: 340px; height: 100%;
background: rgba(11, 13, 16, 0.92); color: #e7ebf0;
border-left: 1px solid #262c33; overflow-y: auto; padding: 12px;
font: 13px/1.5 ui-monospace, "Cascadia Code", "Consolas", monospace; z-index: 20;

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 Place the overlay inside the stream area

In normal viewer sessions after stage load, Window.tsx sets showUI=true, reserves the right 300px for ArtifactPanel, and shrinks the stream container by that width; this absolute overlay is rendered as a later sibling with right: 0, width: 340px, and z-index: 20. As a result it covers the binding/sidebar panel rather than sitting on the right edge of the live 3D viewport, hiding existing inspector content and leaving only a small strip over the video.

Useful? React with 👍 / 👎.

Comment thread web-viewer-sample/src/Window.tsx Outdated
Comment on lines +1226 to +1229
const firstPath = paths[0];
if (firstPath && this._mappingCache) {
const guid = this._mappingCache.guidForPrimPath(firstPath);
this._appendReviewEvent(guid ? `點選 3D 構件 → ifc_guid=${guid}(帶進治理)` : `點選 3D 構件 ${firstPath} → 無對映 ifc_guid`);

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 Wire viewport selections into GUID lookup

This reverse lookup only runs when the debug USDStage list calls _onSelectUSDPrims; actual live 3D clicks arrive through the stageSelectionChanged handler, which only updates selectedUSDPrims and never executes this new MappingCache lookup. In normal non-debug viewer use, clicking a prim in the streamed viewport therefore does not append or expose the ifc_guid, so the advertised 3D→GUID governance path remains disconnected.

Useful? React with 👍 / 👎.

Comment on lines +56 to +57
return (
<div className={`gov-overlay ${readOnly ? "gov-readonly" : ""}`} role="complementary" aria-label="A1–A10 治理 overlay">

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 Provide console CSS tokens outside ec-root

The overlay renders Panel, Btn, table, and provenance components that are styled with var(--ec-*), but edge-console.css defines those custom properties only on .ec-root; this viewer overlay is mounted as .gov-overlay under Window.tsx, not inside .ec-root. In the live viewer, the nested .ec-panel/.ec-btn declarations therefore lose their token-based background, border, and color values, leaving much of the new overlay unstyled unless the token scope is added to the overlay root.

Useful? React with 👍 / 👎.

- F1 govEndpoints:coverage gate 修正降級閾值——locked 1.0 才 pass,
  fallback(degraded)只在 ratio<0.9 觸發;0.9≤r<1.0 為 warnOnly 不降級。
  補 0.95 warn band 邊界測試(fake/null 仍保守降級)。
- F2 routing:pathname 限根層 /console(^/console(/|$)),/foo/console 不再誤判;
  保留 hash 分支。補巢狀路徑測試。
- F3 GovernanceOverlay:導入 rowKey=rule_code::ifc_guid 穩定 key,避免相同 ifc_guid
  不同 rule_code 之 React key 與 lastResult 碰撞;補多筆同 guid 去碰撞測試。
- F4 IntakeSelectPage:radio 補 aria-label(選取 ${ifc_ready_job_id})可及性名稱。
- F5 Window.tsx render:overlay coverage 一律走 evaluateCoverageGate,
  null mapping → degraded:true(與 gate null 語意一致,誠實顯「coverage 未知」)。
  gitnexus_impact(render, upstream)=LOW(0 callers / 0 processes)。
- F6 穩定選取子:GovernanceOverlay/IntakeSelectPage/OperatorConsole 補 data-testid
  (gov-*/intake-*/op-*);Btn 新增選用 data-testid 轉發(對既有呼叫者零行為變更)。
- F7 overlay.css:font-family Consolas 去引號(stylelint font-family-name-quotes)。
- F8 OperatorConsole:readPage 改 named export + 補純函式單元測試
  (空/intake/runtime/unknown);hashchange/nav 互動由 browser E2E 覆蓋(不引入 @testing-library)。
- F9 計畫文件:將「北極星 source of truth」改為 supporting references(權威為 live spec
  + code contracts,對齊 documentation-source-of-truth policy);table cell 內 inline code
  的 | 跳脫(MD056)。

驗證:npx tsc --noEmit=0 errors;npm run verify 通過(build + 102 tests + struct-log 10)。
note-only #6:intake 非同步流程沿用 renderToString smoke,full async 由 E2E + data-testid 覆蓋,不加 @testing-library。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
monkey1sai and others added 3 commits June 4, 2026 15:31
…sionId(以 session_id 解析 server-side IFC 路徑後透傳 A3 rule-run)

瀏覽器 review viewer 只持有 session_id,不知 server-side IFC path。新增的
coordinator-only 端點從自己的 SessionStore + ExternalIfcReadyStore 解析出
host-side IFC 路徑(job.host_local_path,fallback job.local_path)與
model_version_id,再 reuse 既有 forward helper + GOVERNANCE_API_BASE 透傳給
governance-service POST /api/rule-runs,回 { rule_run_id, status }。

解析鏈:session_id → SessionStore.get → ReviewSession.model_version_id;
externalIfcReadyStore.list().filter(job.review_session_id === sessionId)
(由 conversion-ready auto-session 的 recordReviewSession 寫入反向參照)→
job.host_local_path(markDownloaded 於 /api/external/ifc-ready 同步下載完成時
寫入 storage/ifc-cache/<jobId>/source.ifc 的 host 視角絕對路徑)。

邊界與誠實:coordinator 只解析 + 透傳,不跑 rule-run、不是新資料權威;
可選 override body { ids_path?, rule_set? } 一併透傳,ifc_source_path /
model_version_id 不可被瀏覽器覆寫;不注入 element_mapping_path(coordinator
不持有 host-side mapping 檔,mapping 為 streaming artifact URL)。
400 不合法 session id;404 session/IFC 路徑無法解析;502 governance 不可達
(reuse forward 既有 502 path);絕不偽造 path 或成功。

- governanceProxy.ts:registerGovernanceProxy 接受 deps(resolver +
  isSafeSessionId);GOVERNANCE_API_BASE 改為 per-request 讀取以利測試覆寫。
- app.ts:注入只讀 SessionStore + ExternalIfcReadyStore 的 resolver。
- docs/contracts/governance-rule-run-proxy.md:新契約文件(含解析鏈與回應表)。
- tests:resolve+forward / override 透傳 / 404x2 / 400 / 502 共 6 例。

npm run verify GREEN:build 無 TS error;22 test files / 279 tests passed。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
把 web-viewer-sample 的治理 overlay 從靜態骨架接成真正可從前端操作:

- W1 A3:governanceClient.createRuleRunForSession;overlay 新增 gov-run-rulecheck,
  Window._runGovernanceRuleCheck 起 rule-run→輪詢 60×1s→succeeded 取 failed 結果
  映成 FailedElement 餵 govFailedElements;誠實表態 running/error/succeeded。
- W2 標示誠實:成功只顯示「已送出 3D 標示請求(待 Kit 確認)」;Window 用獨立
  _pendingGovHighlights 追蹤 requestId,highlightPrimsResult 回來後依 selected/missing
  寫 govHighlightConfirm 覆寫文案(不破壞既有 mapping-verify pending 路徑)。
- W3 A8:overlay 加 gov-a8-issue(須 rule-run succeeded)+ gov-a8-bcf 下載連結;
  Window._createGovIssues 打 issuesFromRuleRun;BCF 走 bcfExportUrl(model_version_id),
  無 model version 時誠實提示不捏造 URL。
- W4 點 3D→GUID:抽 _reverseLookupGuid,_onSelectUSDPrims 與 live stageSelectionChanged
  共用(DRY),反查 ifc_guid 寫 govSelectedGuid + 記事件。
- W5 coverage 來源校正:改讀 streamConfig.quality_metrics_summary.coverage_ratio(原樣,
  viewer 不自算);移除 MappingCache.coverageRatio()/summaryMappedCount/sourceEntityCount
  (真實 summary 無分母,wrong-sourced),保留 isFake + 雙向 index + belongsTo。
- W6 進件前進:IntakeSelectPage 加 intake-open,依 job.viewer_url 開啟既有 viewer,
  缺 viewer_url 則 disabled + 誠實說明(不假導航)。
- W7 overlay token:把 edge-console.css .ec-root 的 --ec-* 移植到 .gov-overlay,
  讓掛在 .ec-root 之外的巢狀 .ec-panel/.ec-btn 正確上色。
- W8 短 hash 路由:isOperatorConsolePath 支援 #coordinator/#intake/#runtime,但僅在
  query 無 session= 時生效(viewer ?session= 進件優先);main.tsx 傳入 location.search。
- W9 cache 重建:_loadElementMapping 改以 model version + mapping_url 雙鍵判定重建
  (_mappingCacheUrl),避免同版本換 artifact 時讀到舊對映。

測試:GovernanceOverlay 9→17、routing 3→6、IntakeSelectPage 3→4、mappingCache 調整為 6
(移除 coverage-ratio 測試)。tsc --noEmit 0 errors;npm run verify(build+111 tests+struct-log)green。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lient-only)

完整 live 接線需 1 個最小 coordinator session-scoped rule-run 端點(governance-service rule-run
需 server IFC 路徑、瀏覽器不持有)。更新 proposal/spec delta:從「client-only 零後端」誠實改為
「client 元件 + 1 個 coordinator resolve+forward 端點,前端只經 :8004、不改 data shape」。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

PR Review Agent Summary

Field Value
Status warning
Risk medium
PR 182
Head codex/openspec/unified-console-mvp / 94d8fbed0f4baeda7fb17219ecd1317f3a0713d6
Base main / bafb013f9772c632a555d9014139c29ef9fa3517

Blockers

  • None

Warnings

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

Validation Commands

  • openspec validate unified-console-mvp
  • npm run verify
  • npm run verify

Checks

  • passed openspec validate unified-console-mvp (openspec)
  • passed bim-review-coordinator verify (bim-review-coordinator)
  • passed web-viewer-sample verify (web-viewer-sample)

Human Review Notes

  • OpenSpec changes detected: unified-console-mvp
  • Optional AI adapter is not required by policy and was skipped.

@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: 94d8fbed0f

ℹ️ 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/Window.tsx Outdated
this.setState({ govRuleCheck: { status: "error", error: "尚無 review session" } });
return;
}
this.setState({ govRuleCheck: { status: "running" } });

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 Clear stale failures when starting a new rule run

When an operator reruns A3 after a previous successful check, this state update only marks the run as running; the old govFailedElements and highlight confirmations remain rendered and their “在 3D 標示” buttons stay enabled throughout the polling window or if the new run fails/times out. In that scenario the overlay can highlight failures from the previous rule run/model while reporting that a different run is in progress, so clear the previous failure/highlight/issue state when starting a new rule run.

Useful? React with 👍 / 👎.

<Btn
caption="POST rule-runs/for-session/:sessionId(client 主動拉)"
data-testid="gov-run-rulecheck"
disabled={!props.panelState.canOperate || !props.onRunRuleCheck}

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 rule-run button while polling

When a rule check is already running, this button only changes its label to “檢核中…” but remains enabled, so a double-click or another click during the 60-second polling loop starts additional /rule-runs/for-session jobs for the same session. Those concurrent polls can race to overwrite govRuleRunId/failed rows and waste governance-service CPU, so include the running state in the disabled condition.

Useful? React with 👍 / 👎.

Comment thread web-viewer-sample/src/Window.tsx Outdated

// W4:live 3D 點選 / debug 清單共用的 prim → ifc_guid 反查(DRY)。誠實:無對映記事件且 guid=null。
private _reverseLookupGuid(path: string): void {
const guid = this._mappingCache?.guidForPrimPath(path) ?? null;

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 Resolve selected mesh children to mapped parents

Fresh evidence: the live selection handler now calls this helper, but it still only does an exact lookup. Real converted stages map each IFC GUID to an Xform root while geometry is authored below it as .../Body_###, and Kit’s stageSelectionChanged emits the selected prim path, so normal viewport picks on a mesh child will miss the parent mapping and record govSelectedGuid=null; walk up ancestor paths before giving up.

Useful? React with 👍 / 👎.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web-viewer-sample/src/Window.tsx (1)

1361-1369: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear stale mapping cache when mapping is unavailable or load fails.

_mappingCache/_mappingCacheUrl are preserved in error/no-URL branches, so older model mappings can leak into subsequent highlight/reverse-lookup behavior.

Suggested fix
 if (!mappingUrl) {
+    this._mappingCache = null;
+    this._mappingCacheUrl = null;
     this.setState({
       mappingStatus: "沒有 mapping_url,無法載入 element_mapping.json",
       ...
     });
     return;
 }
 ...
 } catch (error) {
+    this._mappingCache = null;
+    this._mappingCacheUrl = null;
     const message = error instanceof Error ? error.message : String(error);
     this.setState({
       mappingStatus: `mapping 載入失敗:${message}`,
       ...
     });

Also applies to: 1419-1427

🤖 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/Window.tsx` around lines 1361 - 1369, When mappingUrl
is missing or loading fails the private cache fields _mappingCache and
_mappingCacheUrl are not cleared, letting stale mappings leak into later
highlight/reverse-lookup flows; update the branches that handle the no-URL case
(the setState block shown) and the error/load-failure branch (also referenced
around the 1419–1427 area) to explicitly null out or reset this._mappingCache
and this._mappingCacheUrl in addition to updating state so the component cannot
use stale mapping data later.
🤖 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 `@web-viewer-sample/src/console/governanceClient.ts`:
- Around line 63-66: The createRuleRunForSession function embeds raw sessionId
into the URL which can break routing; update the URL construction to use
encodeURIComponent(sessionId) when building the path in createRuleRunForSession
so reserved characters are percent-encoded and the POST target remains correct,
leaving the jsonFetch call and body handling unchanged.

In `@web-viewer-sample/src/console/IntakeSelectPage.tsx`:
- Around line 32-34: The openViewer function directly passes
selectedJob.viewer_url to window.location.assign which can be abused by unsafe
or malformed schemes; update openViewer to validate the URL before navigating:
use the URL constructor (or safe parsing) to ensure the value is a well-formed
absolute URL and allow only safe schemes (e.g., http and https), handle
exceptions from parsing, and only call window.location.assign when validation
succeeds (otherwise log/ignore the value); reference selectedJob.viewer_url and
window.location.assign in your change to locate the code.

In `@web-viewer-sample/src/Window.tsx`:
- Around line 597-604: The _runGovernanceRuleCheck function can be invoked
concurrently and races govRuleRunId/govRuleCheck; add a guard at the start of
_runGovernanceRuleCheck that returns early if this.state.govRuleCheck?.status
=== "running" or this.state.govRuleRunId is set, and ensure you set govRuleCheck
to {status:"running"} immediately after the guard; also ensure every
early-return/error/finish path clears or updates govRuleRunId and govRuleCheck
appropriately (e.g., set govRuleRunId when a run is created and clear it in a
finally block or on error) so overlapping launches are prevented and state
doesn't get stuck.
- Around line 646-657: _createGovIssues currently lets repeated calls run
concurrently, causing duplicate non-idempotent writes; add an in-method
reentry/idempotency guard by checking the component state govIssueCreate.status
and govRuleRunId at the start of _createGovIssues and returning early if status
=== "creating" or if status === "created" and the stored runId matches
this.state.govRuleRunId. When starting a request set govIssueCreate to { status:
"creating", runId: this.state.govRuleRunId }, and on success or failure update
govIssueCreate.status (and clear runId on error) so future calls for a different
runId proceed. Refer to _createGovIssues and the govIssueCreate and govRuleRunId
state fields to implement this guard.
- Around line 1713-1719: The confirmation logic for govHighlightConfirm
currently marks a component as confirmed if result === "success", selectedPaths
includes govPending.primPath, and missingPaths.length === 0, but it omits
checking for fallbacks; update the predicate to also ensure
govPending.fallback_paths (or the actual fallback array field on govPending) is
empty / does not contain entries (i.e. no fallback was used) before setting the
success message so fallback selections are treated as non-confirmed; change the
condition around nextState.govHighlightConfirm assignment to require no fallback
entries in govPending in addition to the existing selectedPaths and missingPaths
checks.

---

Outside diff comments:
In `@web-viewer-sample/src/Window.tsx`:
- Around line 1361-1369: When mappingUrl is missing or loading fails the private
cache fields _mappingCache and _mappingCacheUrl are not cleared, letting stale
mappings leak into later highlight/reverse-lookup flows; update the branches
that handle the no-URL case (the setState block shown) and the
error/load-failure branch (also referenced around the 1419–1427 area) to
explicitly null out or reset this._mappingCache and this._mappingCacheUrl in
addition to updating state so the component cannot use stale mapping data later.
🪄 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: 7a3a76f3-56f7-4878-85cd-29bec2731ca5

📥 Commits

Reviewing files that changed from the base of the PR and between f85807a and 94d8fbe.

📒 Files selected for processing (25)
  • bim-review-coordinator/src/app.ts
  • bim-review-coordinator/src/routes/governanceProxy.ts
  • bim-review-coordinator/tests/governance-rule-run-for-session.test.ts
  • docs/contracts/governance-rule-run-proxy.md
  • docs/superpowers/plans/2026-06-04-unified-console-mvp.md
  • openspec/changes/unified-console-mvp/proposal.md
  • openspec/changes/unified-console-mvp/specs/unified-governance-console/spec.md
  • openspec/changes/unified-console-mvp/tasks.md
  • web-viewer-sample/src/Window.tsx
  • web-viewer-sample/src/console/GovernanceOverlay.test.tsx
  • web-viewer-sample/src/console/GovernanceOverlay.tsx
  • web-viewer-sample/src/console/IntakeSelectPage.test.tsx
  • web-viewer-sample/src/console/IntakeSelectPage.tsx
  • web-viewer-sample/src/console/OperatorConsole.test.tsx
  • web-viewer-sample/src/console/OperatorConsole.tsx
  • web-viewer-sample/src/console/components.tsx
  • web-viewer-sample/src/console/governance/govEndpoints.test.ts
  • web-viewer-sample/src/console/governance/govEndpoints.ts
  • web-viewer-sample/src/console/governance/mappingCache.test.ts
  • web-viewer-sample/src/console/governance/mappingCache.ts
  • web-viewer-sample/src/console/governance/overlay.css
  • web-viewer-sample/src/console/governanceClient.ts
  • web-viewer-sample/src/console/routing.test.ts
  • web-viewer-sample/src/console/routing.ts
  • web-viewer-sample/src/main.tsx
✅ Files skipped from review due to trivial changes (4)
  • openspec/changes/unified-console-mvp/tasks.md
  • openspec/changes/unified-console-mvp/proposal.md
  • docs/contracts/governance-rule-run-proxy.md
  • docs/superpowers/plans/2026-06-04-unified-console-mvp.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • web-viewer-sample/src/console/routing.test.ts
  • web-viewer-sample/src/main.tsx
  • web-viewer-sample/src/console/governance/govEndpoints.test.ts
  • web-viewer-sample/src/console/OperatorConsole.test.tsx
  • web-viewer-sample/src/console/governance/govEndpoints.ts
  • web-viewer-sample/src/console/governance/overlay.css
  • web-viewer-sample/src/console/OperatorConsole.tsx
  • web-viewer-sample/src/console/GovernanceOverlay.tsx

Comment thread web-viewer-sample/src/console/governanceClient.ts
Comment thread web-viewer-sample/src/console/IntakeSelectPage.tsx
Comment thread web-viewer-sample/src/Window.tsx
Comment thread web-viewer-sample/src/Window.tsx
Comment thread web-viewer-sample/src/Window.tsx
統一治理控制台 MVP overlay live-wiring 的第三輪 CodeRabbit + Codex review 修復,frontend-only:

- R1 Window._runGovernanceRuleCheck:running 中重入直接 return(不重疊輪詢);開新 run 前清空殘留
  state(govFailedElements/govHighlightConfirm/govIssueCreate/govRuleRunId)與 _pendingGovHighlights。
- R2 Window._createGovIssues:creating 中重入直接 return(防連點重複開 issue)。
- R3(安全)IntakeSelectPage:viewer_url 導航前過 isSafeViewerUrl(僅 http(s) 絕對 URL 或同源相對
  路徑),拒 javascript:/data:/open-redirect;canOpen 以此 gate,不安全顯示誠實警示「viewer_url 非
  安全 http(s)/同源路徑,拒絕導航」。isSafeViewerUrl 匯出為純函式。
- R4 governanceClient.createRuleRunForSession:sessionId 以 encodeURIComponent 包覆。
- R5 GovernanceOverlay:gov-run-rulecheck 於 ruleCheck.status==="running" 時 disabled(配對 R1),
  標籤維持「檢核中…」。
- R6(誠實)Window highlightPrimsResult gov-confirm:confirmed 另要求 fallbackPaths.length===0
  (Kit 用 fallback 不算真確認,鏡像 mapping-verify predicate)。
- R7 sub-100% coverage warnOnly 透傳 overlay:coverage ∈ [0.9,1.0) 顯示 gov-coverage-warn
  「coverage <100%(未達 MVP 鎖定 1.0;measure-first 警示,非 fallback 降級)」;不改
  evaluateCoverageGate 邏輯。
- R8 MappingCache.guidForPrimPathOrAncestor + Window._reverseLookupGuid:child mesh prim 往父層
  解析到 mapped ancestor 的 guid;guidForPrimPath 維持 exact。

測試:+7(R3 驗證器/不安全 URL 拒導航 mount、R5 running-disabled、R7 warn×3、R8 ancestor)。
驗證:npx tsc --noEmit=0;npm run verify 綠(build + 118 tests + struct-log 10/10)。
R9 確認:mappingCache 已不讀 element_mapping summary 算 coverage(W5);routing isOperatorConsolePath
已支援 #coordinator/#intake/#runtime(W8,session= 時讓位)—— 皆已解決,無需改動。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

PR Review Agent Summary

Field Value
Status warning
Risk medium
PR 182
Head codex/openspec/unified-console-mvp / 4ff19ef40cc7d904b842a0076b63d75ef1255c6f
Base main / bafb013f9772c632a555d9014139c29ef9fa3517

Blockers

  • None

Warnings

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

Validation Commands

  • openspec validate unified-console-mvp
  • npm run verify
  • npm run verify

Checks

  • passed openspec validate unified-console-mvp (openspec)
  • passed bim-review-coordinator verify (bim-review-coordinator)
  • passed web-viewer-sample verify (web-viewer-sample)

Human Review Notes

  • OpenSpec changes detected: unified-console-mvp
  • Optional AI adapter is not required by policy and was skipped.

@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: 4ff19ef40c

ℹ️ 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".

title="治理失敗構件 · 在 live 3D 標示"
sub="點 failed 構件 → HighlightBridge 經 DataChannel 在 3D 標紅(client 主動拉,非 server-push)"
prov="asbuilt"
actions={<Btn caption="clearHighlightRequest(client 主動拉)" data-testid="gov-clear" disabled={!props.panelState.canOperate} onClick={() => props.onClearHighlight()}>清除 3D 標示</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 Clear highlight status when clearing 3D marks

When an operator has highlighted a failed element and then clicks this clear button, the code only sends clearHighlightRequest; it does not clear either the overlay's local lastResult or the parent govHighlightConfirm state. In that scenario the Kit highlight is removed from the viewport while the row still says it was sent/selected in 3D, so the UI gives a stale confirmation until another rule run or remount resets the state.

Useful? React with 👍 / 👎.

// 改用 ancestor 解析(往父層走,直到命中 mapped prim),命不中才回 null(誠實,不捏造)。
const guid = this._mappingCache?.guidForPrimPathOrAncestor(path) ?? null;
this._appendReviewEvent(guid ? `點選 3D 構件 → ifc_guid=${guid}(帶進治理)` : `點選 3D 構件 ${path} → 無對映 ifc_guid`);
this.setState({ govSelectedGuid: guid });

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 Expose the looked-up GUID after viewport picks

Fresh evidence beyond the earlier lookup-thread comments: rg -n "govSelectedGuid" web-viewer-sample/src shows this new state is only declared and assigned here, with no render or downstream consumer. For normal stageSelectionChanged clicks, the GUID lookup therefore disappears into hidden state instead of being shown or used by any governance action, so the advertised 3D→GUID path remains non-operable for users.

Useful? React with 👍 / 👎.

Comment on lines +578 to +580
if (!this._mappingCache) {
return { ok: false, reason: "unmapped" };
}

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 Load mappings before offering failed-element highlights

In the normal non-debug viewer, _mappingCache stays null because the only _loadElementMapping() call is wired to the debug DemoControlPanel (rg -n "_loadElementMapping\("), while the new governance overlay never loads it. For sessions that have a real mapping_url, A3 can populate failed rows but clicking “在 3D 標示” still immediately returns unmapped, so the primary overlay's failed-element highlight path is unavailable unless the operator first opens debug UI and manually loads the mapping.

Useful? React with 👍 / 👎.

<table className="ec-table">
<thead><tr><th>rule_code</th><th>severity</th><th>ifc_guid</th><th /></tr></thead>
<tbody>
{props.failedElements.slice(0, 50).map((f) => (

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 Surface failures beyond the first 50 rows

When a rule run returns more than 50 failed elements, this hard slice silently hides the rest of the failures and provides no count, pagination, or way to highlight/create context for the omitted rows. Real governance runs can easily exceed this threshold, so operators may believe they have reviewed all failed components while most of the result set is inaccessible from the new overlay.

Useful? React with 👍 / 👎.

>
{issueCreateText && <Field k="issue 建立" v={issueCreateText} prov="asbuilt" />}
{props.bcfUrl ? (
<a className="ec-btn" data-testid="gov-a8-bcf" href={props.bcfUrl} target="_blank" rel="noreferrer">

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 Tie BCF downloads to the current rule run

This link is shown as part of “A8 Issue / BCF · 從本次 rule-run”, but it only filters by model_version_id; inspecting governance-service/bcf/api.py shows /api/bcf/export accepts only model_version_id and status, not the current rule-run id. If the model already has issues from older rule runs or manual creation, the operator can download a BCF that contains unrelated historical issues while the panel presents it as the current run's output.

Useful? React with 👍 / 👎.

Comment thread web-viewer-sample/src/Window.tsx Outdated
streamConfig.quality_metrics_summary.coverage_ratio(型別文件規定 viewer MUST NOT compute,
原樣呈現);缺值時 ratio=null → gate 判 degraded(顯「coverage 未知」降級橫幅),不捏造 coverage%。 */}
{this.state.showStream && (() => {
const inputs = deriveOverlayInputs({ spectator: isSpectatorStreamMode(), streamReady: this._hasRemoteVideoFrame() });

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 governance actions for blocked lifecycles

The overlay's operability is derived only from spectator mode and whether a video frame exists, while the rest of Window.tsx already treats closed/closing/failed/queued lifecycles as blocked via isBlockedLifecycle. In a session whose lifecycle has moved to one of those blocked states but still has a stream frame, the A3 and A8 REST buttons remain enabled and can create rule-runs/issues because those paths are not protected by _sendStreamMessage's lifecycle guard.

Useful? React with 👍 / 👎.

…ng/自動載入 mapping)

修復 Codex round-4 P2 findings(統一治理控制台 MVP,frontend-only):

- T1 清除 3D 標示重設狀態:Window.onClearHighlight 一併清 govHighlightConfirm
  與 _pendingGovHighlights;overlay 加 handleClearHighlight 重設本地 lastResult,
  避免殘留「已送出/已在 3D 標示」誤導(按鈕 testid gov-clear 不變)。
- T2 顯示反查到的 ifc_guid:Window 透傳 selectedGuid={govSelectedGuid ?? null},
  overlay 新增 selectedGuid prop,非 null 時誠實顯示「點選 3D 構件 → ifc_guid=…」
  (testid gov-selected-guid;無對映時 Window 設 null 不顯示假 guid)。
- T3 一般檢視自動載入 element_mapping:_completeStageLoad 尾端呼叫新增的
  _maybeAutoLoadMapping(),僅在有 mapping_url 且該 url 未載入(_mappingCacheUrl)時
  reuse _loadElementMapping(),補齊非 debug 場景 _mappingCache 恆 null → overlay
  永遠 unmapped 的真實功能缺口。無 mapping_url 時誠實不做事;不動既有 stage-load
  流程與 debug onLoadMapping 路徑。
- T4 失敗構件 >50 誠實標註:保留 50 列上限,>50 時加 gov-failed-truncated 標註
  「顯示前 50 筆/共 N 筆失敗構件(其餘未列出)」,不靜默截斷。
- T5 BCF 範圍誠實:gov-a8-bcf 旁加 gov-a8-bcf-scope 說明 BCF 匯出為本 model
  version 所有正式 issue(非僅本次 rule-run);端點無 run filter,不捏造。
- T6 非 active lifecycle 停用治理動作:govPanelState/windowOverlayGlue 接受
  lifecycleActive,新增 disabledReason "session_not_active" + 文案;Window 由
  reviewLifecycleStatus 計算 lifecycleActive(僅 active/created 視為 active,
  queued/blocked/failed/closing/closed/dropped 唯讀)。

測試:govPanelState +5、windowOverlayGlue +3、GovernanceOverlay +7(含 T1 jsdom
互動測試)。npx tsc --noEmit=0;npm run verify 綠(14 files / 133 tests + struct-log 10)。
gitnexus_impact:_completeStageLoad upstream=HIGH(中央 stage-ready 匯流點,4 processes),
但本次為純附加 fire-and-forget 呼叫,不改既有控制流;_loadElementMapping=LOW。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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: 4c5b392960

ℹ️ 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/Window.tsx Outdated
Comment on lines +1746 to +1748
nextState.govHighlightConfirm = {
...this.state.govHighlightConfirm,
[govPending.ifc_guid]: confirmed ? "已在 3D 標示(Kit 已選取)" : "Kit 未選到該構件(missing/fallback)",

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 Key highlight confirmations by failure row

When the same IFC GUID appears in multiple failed-rule rows, the row key/local pending state now distinguishes rows by rule_code, but the async Kit confirmation is still stored under only ifc_guid. In that scenario, confirming or failing one highlight request causes every row for that element to show the same “已在 3D 標示”/missing status, so operators cannot tell which rule row was actually acted on; carry the row key/rule code through _pendingGovHighlights and govHighlightConfirm as well.

Useful? React with 👍 / 👎.

Comment on lines +232 to +233
disabled={!props.panelState.canOperate || !ruleCheckSucceeded || !props.onCreateIssues}
onClick={() => props.onCreateIssues?.()}

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 issue creation after it succeeds

After a successful A8 creation, this button becomes enabled again because the disabled condition ignores issueCreate.status, and _createGovIssues only suppresses the in-flight creating state. Clicking it again calls the idempotent issues/from-rule-run endpoint, which skips the already-created source refs and returns created: 0, causing the overlay to overwrite the prior “opened N issues” confirmation with “opened 0 issues”; keep it disabled (or preserve the created count) once the current run has already been materialized.

Useful? React with 👍 / 👎.

Comment on lines +1788 to 1789
if (prims[0]) this._reverseLookupGuid(prims[0]);
if (prims.length === 0) {

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 Clear the selected GUID on empty viewport selection

When Kit reports an empty stageSelectionChanged selection after the operator deselects or clicks empty space, this new lookup path does nothing and the empty branch only clears selectedUSDPrims. The overlay therefore continues to display the previous govSelectedGuid, making the 3D→GUID context stale until another mapped prim is selected; clear govSelectedGuid when prims.length === 0.

Useful? React with 👍 / 👎.

const item: HighlightItem = {
prim_path: primPath,
ifc_guid: failed.ifc_guid,
color: severityToColor(failed.severity),

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 Map governance severities before coloring

The rule engine emits severities such as high, medium, and IDS required, but severityToColor only treats error as red and warning as amber, so the default failed governance rows are highlighted with the blue fallback instead of the advertised failed-element/red visual cue. In normal A3 runs using default-governance.yaml, high/medium failures therefore look like neutral selections; translate governance severities to the stream color scale before building the highlight item.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

PR Review Agent Summary

Field Value
Status warning
Risk medium
PR 182
Head codex/openspec/unified-console-mvp / 4c5b39296016cacedf6d452c76d60684385dd77a
Base main / bafb013f9772c632a555d9014139c29ef9fa3517

Blockers

  • None

Warnings

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

Validation Commands

  • openspec validate unified-console-mvp
  • npm run verify
  • npm run verify

Checks

  • passed openspec validate unified-console-mvp (openspec)
  • passed bim-review-coordinator verify (bim-review-coordinator)
  • passed web-viewer-sample verify (web-viewer-sample)

Human Review Notes

  • OpenSpec changes detected: unified-console-mvp
  • Optional AI adapter is not required by policy and was skipped.

F1 治理標示確認改以 rowKey(rule_code::ifc_guid)索引:同一 ifc_guid 多筆
   不同 rule_code 的失敗列不再共用 / 互相覆蓋確認狀態。
   - Window.tsx:_pendingGovHighlights 一併記 rowKey;highlightPrimsResult
     gov 分支以 govPending.rowKey 寫 govHighlightConfirm。
   - GovernanceOverlay.tsx:highlightConfirm 改讀 rowKey(f)(與 lastResult 一致)。
F2 A8 開 issue 鈕在 issueCreate.status==="created"(與 "creating")時 disabled,
   避免連點重複開 issue 集;succeeded 後仍顯示「已從 rule-run 開 N 筆 issue」。
F3 stageSelectionChanged 取消選取(prims 為空)時一併清 govSelectedGuid=null,
   避免 overlay gov-selected-guid 行殘留舊 guid。
F4 highlightBridge.ts 新增純函式 normalizeSeverity(大小寫不敏感):
   critical/high/error→error、medium/warning→warning、其餘原樣透傳;
   highlightFailed 以其結果呼叫 severityToColor(不改 severityToColor 本身)。

驗證:npx tsc --noEmit = 0;npm run verify 綠(vitest 141 passed / 14 files
+ struct-log 10 PASS)。新增 GovernanceOverlay 3 測(F1×2、F2×1)、
highlightBridge 4 測(F4 high→紅 + normalizeSeverity×3);調整 1 既有 W2 測
(highlightConfirm key 改 rowKey)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

PR Review Agent Summary

Field Value
Status warning
Risk medium
PR 182
Head codex/openspec/unified-console-mvp / c6f04390d0109db40c72d645b0b4d2b267266e06
Base main / bafb013f9772c632a555d9014139c29ef9fa3517

Blockers

  • None

Warnings

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

Validation Commands

  • openspec validate unified-console-mvp
  • npm run verify
  • npm run verify

Checks

  • passed openspec validate unified-console-mvp (openspec)
  • passed bim-review-coordinator verify (bim-review-coordinator)
  • passed web-viewer-sample verify (web-viewer-sample)

Human Review Notes

  • OpenSpec changes detected: unified-console-mvp
  • Optional AI adapter is not required by policy and was skipped.

@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: 2

🤖 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 `@web-viewer-sample/src/console/IntakeSelectPage.tsx`:
- Around line 11-16: isSafeViewerUrl currently only checks protocol and allows
open redirects; update isSafeViewerUrl to validate the URL's origin against an
allowlist (at minimum window.location.origin and the coordinator origin) instead
of just protocol. Locate isSafeViewerUrl and change the try block to compute
parsed.origin and return true only if parsed.origin === window.location.origin
|| parsed.origin === getCoordinatorOrigin() (or the constant COORDINATOR_ORIGIN
used in your app); also ensure relative URLs are allowed by treating them as
same-origin. Update the navigation call site that uses isSafeViewerUrl (the code
around lines 49-50) to rely on the tightened isSafeViewerUrl check.

In `@web-viewer-sample/src/Window.tsx`:
- Around line 2052-2061: The call to evaluateCoverageGate incorrectly treats a
null this._mappingCache as non-fake; update the isFake argument so unknown
mapping is considered fake (i.e. treat absence as degraded). Specifically,
change the evaluateCoverageGate invocation (the symbol evaluateCoverageGate) to
pass isFake: this._mappingCache?.isFake ?? true (or equivalent ternary), so when
this._mappingCache is null it evaluates as true; keep the rest of the logic that
assigns gate, ratio and coverage unchanged (see variables ratio, gate,
coverage).
🪄 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: 202840ca-eee1-4545-8f8a-790e3d23ea60

📥 Commits

Reviewing files that changed from the base of the PR and between 94d8fbe and c6f0439.

📒 Files selected for processing (14)
  • web-viewer-sample/src/Window.tsx
  • web-viewer-sample/src/console/GovernanceOverlay.test.tsx
  • web-viewer-sample/src/console/GovernanceOverlay.tsx
  • web-viewer-sample/src/console/IntakeSelectPage.test.tsx
  • web-viewer-sample/src/console/IntakeSelectPage.tsx
  • web-viewer-sample/src/console/governance/govPanelState.test.ts
  • web-viewer-sample/src/console/governance/govPanelState.ts
  • web-viewer-sample/src/console/governance/highlightBridge.test.ts
  • web-viewer-sample/src/console/governance/highlightBridge.ts
  • web-viewer-sample/src/console/governance/mappingCache.test.ts
  • web-viewer-sample/src/console/governance/mappingCache.ts
  • web-viewer-sample/src/console/governance/windowOverlayGlue.test.ts
  • web-viewer-sample/src/console/governance/windowOverlayGlue.ts
  • web-viewer-sample/src/console/governanceClient.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • web-viewer-sample/src/console/governance/windowOverlayGlue.ts
  • web-viewer-sample/src/console/governanceClient.ts
  • web-viewer-sample/src/console/GovernanceOverlay.tsx

Comment thread web-viewer-sample/src/console/IntakeSelectPage.tsx
Comment thread web-viewer-sample/src/Window.tsx
…pping 視為 degraded)

- isSafeViewerUrl 從「只查 protocol」收緊為「同源或 coordinator origin」,擋掉導向任意 https origin 的
  open-redirect / phishing(viewer_url 雖由 coordinator 提供仍防禦縱深);openViewer 改導向正規化 URL。
- Window coverage gate:_mappingCache 為 null(未載入/未知)時 isFake 視為 true → degraded,
  不在 client 無法標示時仍顯示有把握的 coverage%(誠實保守)。
- 更新 isSafeViewerUrl 測試:跨來源 https 改判 false(open-redirect 拒)。
tsc 0 / verify 綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

Copilot reviewed 31 out of 32 changed files in this pull request and generated 4 comments.

Comment on lines +11 to +16
export function isSafeViewerUrl(u?: string | null): boolean {
if (!u) return false;
try {
const parsed = new URL(u, window.location.origin);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
// R6(安全強化):只查 protocol 仍放行 https://attacker.example(open-redirect / phishing)。
Comment on lines +90 to +97
<tbody>
{jobs.slice(0, 50).map((j) => (
<tr key={j.ifc_ready_job_id}>
<td>
<input
type="radio"
name="intake-model"
data-testid="intake-radio"
Comment on lines +421 to +429
// 告知 React 這是 act() 測試環境(消除「not configured to support act」warning)。
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

afterEach(() => {
if (container) {
document.body.removeChild(container);
container = null;
}
});
Comment on lines +14 to +17
it("一般 viewer 路徑(含 ?session=)→ 非 operator(維持 <App/>)", () => {
expect(isOperatorConsolePath("/", "")).toBe(false);
expect(isOperatorConsolePath("/", "")).toBe(false);
expect(isOperatorConsolePath("/viewer", "")).toBe(false);
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

PR Review Agent Summary

Field Value
Status warning
Risk medium
PR 182
Head codex/openspec/unified-console-mvp / 341a376e6525f8b0435cb78ed347b6a0a28fd66e
Base main / bafb013f9772c632a555d9014139c29ef9fa3517

Blockers

  • None

Warnings

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

Validation Commands

  • openspec validate unified-console-mvp
  • npm run verify
  • npm run verify

Checks

  • passed openspec validate unified-console-mvp (openspec)
  • passed bim-review-coordinator verify (bim-review-coordinator)
  • passed web-viewer-sample verify (web-viewer-sample)

Human Review Notes

  • OpenSpec changes detected: unified-console-mvp
  • Optional AI adapter is not required by policy and was skipped.

@monkey1sai
monkey1sai merged commit 8529b6b into main Jun 4, 2026
2 checks passed
@monkey1sai
monkey1sai deleted the codex/openspec/unified-console-mvp branch June 4, 2026 09:35
monkey1sai added a commit that referenced this pull request Jun 9, 2026
…nonical specs (#197)

於 worktree `.worktrees/openspec-converge-2026-06-09` 一次收斂 8 個 active OpenSpec
change(對應已 merged 的 PR #182–#186 / #194 / #195),archive 至
`archive/2026-06-09-<id>/` 並把 spec delta 聯集併入 live canonical specs。
archive = 不可變歷史快照(delta 原樣保留,git 偵測為 R100 純改名);
canonical = 聯集 + 跨 change 矛盾調和(沿用 2026-06-03 批次歸檔之收斂規約)。

收斂的 8 個 change:
- unified-console-mvp (#182)
- console-mapping-proxy (#183)
- unified-console-fe-redesign (#184)
- unified-console-semantic-viewer (#184/#186)
- unified-console-issues-tab (#185)
- console-live-semantic-coexist (#186)
- product-governance-console-integration (#194;原 untracked,本次一併納入追蹤)
- docs-design-spec-source-of-truth (#195)

Canonical spec 同步:
- `unified-governance-console`:5 → 21 requirements。新增 16 條 ugc delta
  requirement(mvp 1 / mapping-proxy 1 / fe-redesign 3 / semantic-viewer 3 /
  issues-tab 1 / live-coexist 2 / product-governance 5,逐條 byte-verbatim)。
- 跨 change 矛盾調和(operator console 掛載點):原 Req「operator 頁分離於三條
  /console 獨立路由、A1–A10 overlay-only」(unified-console-mvp MVP 切片)經
  fe-redesign(:8004/ui 六 hash 路由 + RK6 + CONSOLE_DIST_DIR)與
  product-governance(PR #194 :8004/ui 改掛 EdgeConsole)演進,調和為
  「:8004/ui EdgeConsole 產品操作台 shell,A1–A10 既為 console 頁亦為 viewer
  overlay 操作面」;fe-redesign「React UnifiedConsole 六路由」req 吸收進此調和 req
  (六路由 / RK6 / CONSOLE_DIST_DIR fallback / ?session 讓位 逐條保留),舊措辭標
  superseded(保留歷史脈絡 blockquote)。
- 其餘調和:issues-tab 全幅「問題」分頁加 Req1 carve-out(同一 viewer 非互斥殼);
  semantic-viewer「中央切 <video>」與 live-coexist「語意側欄並存」coherent(中央
  video + 左側欄不覆蓋);mvp req 的 :49100 ban 補「治理/資料 API 目的」scope(與
  既有 :8004-only carve-out 一致,自洽於同 req 後句的 WebRTC DataChannel 著色)。
- `docker-web-plane-host-native-kit`:6 → 7(新增 product-governance 的
  「Rebuild and E2E Evidence for Product Console」)。
- `documentation-source-of-truth` / `demo-fast-mvp-orchestration` /
  `agent-doc-context-budget`:docs-design 的 delta 已於 #195/#196 併入 canonical,
  本次 archive-only(canonical 為 superset,不重複套用,避免污染)。

未完成 task 驗證分類(誠實鐵律;本批無新 runtime/E2E evidence):
- verify-blocked-tooling(OpenSpec CLI 本機不可用:`npx openspec` 回 "could not
  determine executable to run"):console-mapping-proxy 4.1、
  console-live-semantic-coexist 4.1、docs-design 4.6/4.7。改用結構驗證並通過
  (21/7 requirement、每 req 有 scenario、無殘留 delta header、diff --check clean)。
- requires-runtime-not-observed(需 host-native Kit/GPU + scripts/deploy.ps1 +
  live browser 3D,headless 無法觀測,未捏造):unified-console-mvp 5.2、
  console-mapping-proxy 4.3、unified-console-fe-redesign 5.3(streaming-server
  source_client_id 後端強制為明示 Non-goal/待補)。
- deferred-followup(明示後續 PR):unified-console-semantic-viewer 1.1/1.5/3.4
  (完整 GovViewerLayout 重構 / orbit-pan-zoom DataChannel / CH-H1b 完整版面)。
- verify-pass:console-live-semantic-coexist 4.2(PR/CI/merge 已成,archive/sync
  即本 commit)、unified-console-semantic-viewer 3.2(CH-H2 測試檔已落地)。
- 無 task 屬「與 repo 不一致需省略」——8 個 change implementation 皆已在 main 且
  一致(triage 8 agent 逐一 code spot-check 確認)。

對抗驗證(3 agent 平行):
- completeness:0 dropped;17 delta + 4 pre-existing = 21,math closes。
- contradiction:coherent;4 reconciliation 全過,1 minor :49100 wording 已修。
- structure/immutability/git:clean;24×R100 rename(delta 不可變)+
  product-governance diff -r exit 0;active changes 僅剩 archive/。

GitNexus detect_changes:N/A——本變更僅動 openspec/ markdown spec(無 code
symbol/flow),且 detect_changes 對 linked worktree staged 為盲(見既有 closeout
note)。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request Jun 11, 2026
…patch_error 可見) (#206)

* plan: 中文 model_version_id 轉檔派工修復實作計畫(artifact_id sanitize + dispatch_error 可見)(#205)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* plan: fix Task4 webhook header 措辭(X-Webhook-Secret 非 HMAC)+ 斷言欄位 status + import 清單補 IfcReadyListItem(四軸 review 修正落地)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* task#0: feat(coordinator): 新增 sanitizeArtifactIdPart 純函式(中文 id → safe artifact_id)

純函式 + 五個回歸鎖測試(vitest),尚未接線 dispatch 路徑(Task 1)。
規則逐字鎖 conversion_authority.py SAFE_ID_RE = ^[A-Za-z0-9_.-]+$。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* task#1: fix(coordinator): dispatch artifact_id 走 sanitize,中文 model_version_id 不再 400 (#205)

toInternalIfcReadyEvent 內 ifc_artifact.artifact_id 改用 sanitizeArtifactIdPart
組裝,使含中文的 external_model_version_id 通過 conversion 端 SAFE_ID_RE。
補單元回歸鎖(純英文 id 輸出不變)與端到端 dispatch 整合測試(stub conversion
以同款 SAFE_ID_RE 驗收,證明中文 id dispatch 不再被 400 擋下)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* task#2: feat(console): IfcReadyListItem 補 dispatch_error 欄位(對齊 coordinator summarize)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* task#3: feat(console): #/conv Ifc-ready jobs 顯示 dispatch_error 明細 (#205)

job 有 dispatch_error 時於該列「dispatch」欄附註截斷明細(完整字串走 title),
沿用既有 ec-warn-note 樣式;無錯誤不渲染(顯示 —)。新增 mount 測試覆蓋
有錯/無錯兩種 job 的渲染行為,無 mock 假資料。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* task#3: fix(console): IntakeSelectPage 既有 fixture 補 dispatch_error 修型別回歸

task#2 把 dispatch_error 加成 IfcReadyListItem 必填欄位,但同目錄 sibling
fixture(#182 起既有)未同步補欄位,tsc --noEmit 報 TS2741。vite/vitest
不跑 tsc 故 runtime 全綠,屬 merge 前型別清潔度問題。修法:fixture 補
dispatch_error: null。tsc 該錯已清,IntakeSelectPage 6/6 + console 37/37 綠。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* task#4: test(e2e): 中文 model_version_id 派工 + dispatch_error 可見 browser E2E + evidence (#205)

#/conv → Refresh queue → 真 coordinator GET /api/external/ifc-ready →
真實 ifcready_* ID 的 user-facing vertical slice。

- 中文 external_model_version_id(271_pieple_管線)POST ifc-ready → sanitize 後
  artifact_id 為 safe,conversion 端真 SAFE_ID_RE 不再 400 → dispatched(非 dispatch_failed)。
- 另造必失敗 job(forcefail 哨兵 → stub 回 400)→ dispatch_failed,
  #/conv 顯 conv-dispatch-error-<jobId> 節點,title 含完整錯誤字串(明細可見)。

採 (B) STUB CONVERSION API(誠實鐵律):spec 自起本 branch coordinator(tsx)
+ 同 SAFE_ID_RE 規則的 Node http stub conversion server;真規則已在單元/整合層
(external-ifc-ready.test.ts)鎖死。evidence README 標 STUB CONVERSION API。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(coordinator): dispatch 內部 _safe_id 欄位全 sanitize,中文 model/correlation 不再 400 (#205)

mv1/mv2 對抗複驗根治:conversion_authority.py:261-264 對 model_version_id /
correlation_id / tenant_id / project_id 全跑 _safe_id(SAFE_ID_RE)。原修復只
sanitize ifc_artifact.artifact_id,中文 model_version_id 與 worker 派生含冒號的
correlation_id 仍會被真 API 擋成 400;整合測試 stub 只驗 artifact_id 故假綠。

- toInternalIfcReadyEvent:model_version_id / correlation_id 走 sanitizeArtifactIdPart,
  tenant_id / project_id 走 sanitizeSafeIdField(非字串透傳由 authority 套 server-side 預設);
  external_model_version_id 保留原始供 callback/binding 對帳,外部契約不變。
- 整合 stub 升級為對齊真 API 驗證面:對 artifact_id + 上述四欄全跑 SAFE_ID_RE,任一非 safe → 400。
- 補測試:中文 model_version_id 內部事件兩欄(sanitize 後 / 原始)+ 含冒號 correlation_id sanitize
  單元測試;worker 派生 correlation_id(worker:project::version::task)整合測試走嚴格 stub dispatched。
- 審計結論:tenant_id/project_id 在 coordinator 端無 SAFE_ID_RE 輸入驗證(requiredIdentity 只擋空值),
  中文 project/tenant 命名會踩同一條 400,故一併 sanitize。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: 對抗複驗 findings 修補(event_id sanitize + correlation 回拋對帳 + 證據/措辭)(#205)

- mv2b(critical): toInternalIfcReadyEvent 的 event_id fallback 不再用原始 correlationId,
  改 evt_${sanitizeArtifactIdPart(correlationId)};外部帶 event_id 也過 sanitize。
  conversion_authority.py:238 對 event_id 跑 _safe_id,worker 派生冒號 correlation
  未 sanitize 會先炸 400。加 worker-compat 無 event_id 的整合回歸測試。
- cr1(high regression): correlationIndex 同時登記 sanitize 後鍵(sanitized !== 原始時
  雙鍵指向同一 job),讓 conversion result 以 sanitize 後 correlation_id 回拋仍命中
  原 job(非 404)。加端到端對帳測試(worker 冒號 correlation → result callback 命中)。
- st1(honesty): startSafeIdValidatingStub 補驗 event_id / idempotency_key(缺省 fallback
  到 event_id)/ export_job_id / source_rvt_artifact_id(optional None/空字串放行),
  對齊真 conversion_authority create_conversion_job 全 _safe_id 驗證面。
- f1b(揭露): conversion-artifact-id-sanitize.spec.ts 補 conditional-skip 限制明文
  (skip≠fail、靜默全 skip=假信心、本機/指揮官 gate、無 CI e2e job、升級條件)。
- ch1(措辭): console.test.tsx describe 標題改為如實(欄位形狀對齊真後端 schema,
  渲染層驗證;真後端值由 E2E 驗),不再宣稱「真實後端欄位無 mock 假資料」。
- ce1(evidence): commit run2 最終碼 summary.json + conv-list.png。
- rl1(回歸鎖): streaming-conversion-client.test.ts 補全 safe 輸入逐欄 identity 斷言
  (model_version_id/correlation_id/tenant_id/project_id/event_id === 原始值)。

驗證:bim-review-coordinator npm run build 通過、vitest 全量 314 passed;
web-viewer-sample console.test.tsx 37 passed。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: openspec change + spec 入庫(conversion-artifact-id-sanitize 追溯鏈)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: PR #206 review 修補(correlation 索引分桶防 aliasing + openspec spec delta + 文件標記/stub 202)

- [P2/Major] externalIfcReadyStore:sanitize 後 correlation 鍵移入獨立
  sanitizedCorrelationIndex,只供 conversion 結果回拋(getByCorrelation fallback)
  查詢;intake 去重(findExisting)只看原始鍵,杜絕「真實 correlation 恰等於
  他 job sanitize 值」被誤判 idempotent replay 的 aliasing;新增 intake aliasing
  回歸測試(host-native-conversion-ingest.test.ts)
- [Blocker] 補 openspec/changes/conversion-artifact-id-sanitize/specs/.../spec.md
  ADDED requirements/scenarios(sanitize 規則、回拋對帳 + aliasing 防護、
  #/conv dispatch_error 可見),openspec validate 轉綠
- [Major] evidence README 與 implementation plan 補「文件性質」標記
- [nit] conversion dispatch stub 受理回應 200 → 202(對齊真 API);設計文件
  操作者一詞混入西里爾字母修正為 operator

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: correlation collision 對帳以 conversion_job_id 消歧(PR #206 review P2 r2)

job A(unsafe correlation,sanitize 後為 S)與 job B(真實 correlation 恰為 S)
並存時,A 的 conversion 結果以 S 回拋,單靠 correlation 字串無法裁決。
getByCorrelation 改收 optional conversionJobId:原始鍵與 sanitize 鍵的候選
job 中優先回傳 conversion_job_id 吻合者,無法消歧時維持原始鍵優先(向後
相容);ingestConversionReport 帶入 report.conversion_job_id。新增 store
層 collision 消歧回歸測試。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

3 participants