Skip to content

Add project architecture overview UI - #3

Merged
monkey1sai merged 2 commits into
cursor/fix/date---feature/fix/issue/env-setup-agents-md-753dfrom
cursor/fix/date---feature/fix/issue/project-architecture-ui-651d
May 4, 2026
Merged

monkey1sai merged 2 commits into
cursor/fix/date---feature/fix/issue/env-setup-agents-md-753dfrom
cursor/fix/date---feature/fix/issue/project-architecture-ui-651d

Conversation

@monkey1sai

@monkey1sai monkey1sai commented May 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Adds a landing-page architecture overview to web-viewer-sample showing repo responsibilities, core data flow, and boundary rules.
  • Keeps the visualization in the browser client without changing coordinator, storage, BIM control, or streaming runtime behavior.

Walkthrough

project_architecture_ui_walkthrough.mp4

Testing

  • npm run build in web-viewer-sample passes.
  • npm run lint in web-viewer-sample still reports pre-existing lint errors in App.tsx, AppStream.tsx, Forms.tsx, and StreamOnlyWindow.tsx.
  • Manual browser verification confirms the architecture heading, six service cards, Primary Data Flow, and Boundary Rules render at localhost:5173.

Notes

  • No PR template is present in this repository.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

Co-authored-by: monkey1sai <monkey1sai@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented May 4, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 87597896-e895-4f17-a309-8fce07656fc6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/fix/date---feature/fix/issue/project-architecture-ui-651d

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Co-authored-by: monkey1sai <monkey1sai@users.noreply.github.com>
@monkey1sai
monkey1sai marked this pull request as ready for review May 4, 2026 02:47
Copilot AI review requested due to automatic review settings May 4, 2026 02:47

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

Adds a new “Project Architecture UI” landing-page section to the web-viewer-sample client to visually document repo responsibilities, primary data flow, and boundary rules.

Changes:

  • Added a new ArchitectureOverview React component rendering repo cards + flow/rules lists.
  • Embedded the architecture overview into the initial AppOnlyForm landing layout.
  • Added new CSS for the landing grid layout and architecture overview styling.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
web-viewer-sample/src/components/ArchitectureOverview.tsx New UI component for architecture overview content (nodes, flows, boundary rules).
web-viewer-sample/src/Forms.tsx Updates landing form layout to include the new architecture overview panel.
web-viewer-sample/src/App.css Adds styling for the new landing layout and architecture overview components.

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

href="https://docs.omniverse.nvidia.com/embedded-web-viewer/latest/index.html" target="_blank"
rel="noopener noreferrer">Embedded Web Viewer Guide</a>.
It provides a user interface and functionality that supports Kit applications created from
the <b> USD Viewer</b> Template in the <a href="https://github.com/NVIDIA-Omniverse/kit-app-template"
Comment on lines +113 to +121
<label>This client is part of the <a
href="https://docs.omniverse.nvidia.com/embedded-web-viewer/latest/index.html" target="_blank"
rel="noopener noreferrer">Embedded Web Viewer Guide</a>.
It provides a user interface and functionality that supports Kit applications created from
the <b> USD Viewer</b> Template in the <a href="https://github.com/NVIDIA-Omniverse/kit-app-template"
target="_blank" rel="noopener noreferrer">kit-app-template</a>.
<br/>
If you are using this client to stream any other application you need to select the 2nd option below in order for the streamed application to become visible.
</label>
@monkey1sai
monkey1sai merged commit ed6cc46 into cursor/fix/date---feature/fix/issue/env-setup-agents-md-753d May 4, 2026
5 checks passed
monkey1sai added a commit that referenced this pull request May 4, 2026
* docs: add Cursor Cloud specific instructions to AGENTS.md

Add development environment setup notes for Cloud Agent VMs including:
- Service startup commands (Linux equivalents)
- Testing instructions (per-service pytest, npm test)
- .env configuration notes
- Known limitations (bim-streaming-server requires GPU)

Co-authored-by: monkey1sai <monkey1sai@users.noreply.github.com>

* Add project architecture overview UI (#3)

* Add project architecture overview UI

Co-authored-by: monkey1sai <monkey1sai@users.noreply.github.com>

* Clean up architecture landing form lint

Co-authored-by: monkey1sai <monkey1sai@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: monkey1sai <monkey1sai@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: monkey1sai <monkey1sai@users.noreply.github.com>
monkey1sai added a commit that referenced this pull request May 11, 2026
把 docs/PROJECT_DEVELOPMENT_WORKFLOW.md 與 main 上 docs/plans/AI-BIM-governance-saas-roadmap-2026-05.md
完整對齊,避免兩份文件分歧 source of truth。確立角色分工:workflow v3 = 開發流程入口,
roadmap = 技術決策/OpenSpec 候選權威,互相 cross-reference。

事實校正(A):
- §4.1 Phase 3:runtime 部分 blocked → 在另一分支驗證中(非 environment-blocked)
- §4.2 Dedicated Multi-Kit Routing:blocked → 🟡 在另一分支驗證中
- §4.2 Single Kit GPU Render:補同步 PR #20 (commit 0e94a5b) same-Kit 並行驗證
- §5 風險表:補對應 SaaS 路線圖候選編號

命名對齊(B):
- §6.2 / §6.3 / §7 Phase 5 / §8 / §12.1:ifc-to-usdc-real-converter → worker-real-conversion-quality
- ifc-usd-quality-gate 整合進 #1 KPI(#1 land 後再評估是否拆分獨立 spec)
- gpu-kit-pool-scheduler → streaming-multi-instance-orchestration(業務語意層)
- async-worker-pool-and-redis / object-storage-abstraction:標記為 Phase 4 細項,不開新 spec
- ai-rule-carbon-service-foundation → ai-rule-carbon-result-contract
- rtx-physx-mdl-rendering:Kit base 已內建,啟動 app 加 dependency 即可
- sensor-simulation-overlay:Isaac Sim 獨立部署
- api-gateway-and-rate-limiting:Phase 6 凍結

補入內容(C):
- §7 Phase 4 開頭加 NVIDIA Multi-Kit 並行官方定義 cross-reference(roadmap §11.4)
- §7 Phase 4 / Phase 5 各任務後加採用標籤(✅ / ⚠ / ❌)
- §7 Phase 3 待補清單末加業務語意層 vs runtime infrastructure 層註解
- §10 source of truth 表格加 SaaS 路線圖列
- §12 新增 §12.4 P2.5 候選(#1A presence_layer / #2A OVAS Helm)
- §12 新增 §12.5 P3-frozen 候選(#7/#8/#9)
- 頂部 metadata 加文件分工說明

Phase 6 凍結標記(D):
- §7 Phase 6 標題加 ⏸ 凍結中
- §7 Phase 6 段首加凍結決策說明與解凍程序
- §12.5 列出 #7/#8/#9 候選

Lineage 認知(E):§5 風險表加 row 9(lineage graph query API 尚未實作 → 對應 P1 候選 #3)
候選 #4(F):§12.2 補入 coordinator-session-lifecycle-events-audit

收斂後:
- workflow v3 不重述 roadmap 的決策矩陣、spec id、§11.4 Multi-Kit 定義、硬體 §9
- roadmap 不重述 workflow v3 的 sequence diagram、PR checklist、服務測試命令
- 兩份文件互補不替代,互相 cross-reference

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request May 11, 2026
* docs: 新增專案開發流程設計文件

- 基於兩張架構圖(PoC → SaaS、目標架構)分析當前進度
- 定義六大開發階段(Phase 0-6)的目標、交付物與驗收標準
- 包含技術架構演進路徑、資料流設計、API 規格
- 新增測試金字塔與品質保證策略(Unit/Integration/E2E/Load tests)
- 提供部署與維運計畫(Local/Staging/Production)
- 定義團隊協作流程(Git branching、PR review checklist)

此文件將作為後續 Phase 1-6 實作的規劃依據。

Co-authored-by: monkey1sai <monkey1sai@users.noreply.github.com>

* docs: 調整專案開發流程文件格式

- 統一 markdown 表格格式
- 調整列表縮排與標點

Co-authored-by: monkey1sai <monkey1sai@users.noreply.github.com>

* docs: 依新版架構圖 v2 重寫專案開發流程

- 對齊新架構:_s3_storage + _conversion-service 已合併為 _worker
- 對應 OpenSpec 7 個 capability spec(worker-artifact-pipeline、
  review-session-request-lifecycle、multi-artifact-kit-routing 等)
- 連結現有 docs/contracts/ 7 份 API 合約
- 反映實際進度:Phase 0/1/2 完成、Phase 3 進行中、Phase 4-6 待規劃
- 補充每階段對應的 PR / commit 證據
- 6 大 KPI 對應架構圖 ④ 區塊
- 加入 Source of Truth 文件對應表與 OpenSpec PR workflow 速查

關鍵變更:
- Phase 0 基線穩定化 (完成)
- Phase 1 _worker 收攏 (完成,PR #11/#14)
- Phase 2 review-session-request 閉環 (完成,PR #13)
- Phase 3 Session lifecycle 多 artifact / 多 instance (進行中)
- Phase 4 高併發平台化 (待規劃)
- Phase 5 Omniverse 平台能力最大化 (待規劃)
- Phase 6 Production & SaaS 營運 (待規劃)

Co-authored-by: monkey1sai <monkey1sai@users.noreply.github.com>

* docs: v3 依新版架構圖 v1+v2 重寫專案開發流程

主要更新:
- 加入 7 層目標架構(v2 圖):使用者/權限層、Client/Portal層、
  核心業務服務層、Omniverse Runtime/Simulation層、平台能力層、
  DevOps/營運治理層
- 新增 IFC → USD 品質保證管線(7 步驟,標記 step 5 為最重要技術風險點)
- 補入 D. ai-rule-carbon-service 與 E. notification/webhook service
- 補入 Revit Plugin、Admin Console、External API/Webhook Consumer
- 補入 SSO / JWT / RBAC / API Key 認證方式與 5 種使用者角色
- 補入租戶權限階層:公司 - 租戶 - 區 - 棟 - 戶 - 號
- 補入驗證證據分層(runtime-verification-evidence capability):
  non-GPU contract / single Kit GPU / dedicated multi-Kit / stress
- 補入 2026-05-08 端對端驗證結果(控制面已驗證、GPU render 與
  multi-Kit routing 屬 blocked 狀態並已記錄前置條件)
- 補入 PR #17 original_filename 追蹤完成
- KPI 從 6 大擴充為 8 大(新增 IFC-USD 品質 Gate、多租戶+RBAC)
- Phase 5 升級為 Omniverse 平台能力最大化 + AI Service
  涵蓋真實 IFC-USDC converter、RTX/PhysX/MDL、IAQ/HVAC/感測模擬
- Phase 6 補入完整 SaaS 維度
- 9 份 capability spec 對應到各 phase 的進度表

行數:636 - 884 (+530 / -282)

Co-authored-by: monkey1sai <monkey1sai@users.noreply.github.com>

* docs(workflow): 對齊 SaaS 路線圖 2026-05(命名/狀態/P2.5 候選/凍結標記)

把 docs/PROJECT_DEVELOPMENT_WORKFLOW.md 與 main 上 docs/plans/AI-BIM-governance-saas-roadmap-2026-05.md
完整對齊,避免兩份文件分歧 source of truth。確立角色分工:workflow v3 = 開發流程入口,
roadmap = 技術決策/OpenSpec 候選權威,互相 cross-reference。

事實校正(A):
- §4.1 Phase 3:runtime 部分 blocked → 在另一分支驗證中(非 environment-blocked)
- §4.2 Dedicated Multi-Kit Routing:blocked → 🟡 在另一分支驗證中
- §4.2 Single Kit GPU Render:補同步 PR #20 (commit 0e94a5b) same-Kit 並行驗證
- §5 風險表:補對應 SaaS 路線圖候選編號

命名對齊(B):
- §6.2 / §6.3 / §7 Phase 5 / §8 / §12.1:ifc-to-usdc-real-converter → worker-real-conversion-quality
- ifc-usd-quality-gate 整合進 #1 KPI(#1 land 後再評估是否拆分獨立 spec)
- gpu-kit-pool-scheduler → streaming-multi-instance-orchestration(業務語意層)
- async-worker-pool-and-redis / object-storage-abstraction:標記為 Phase 4 細項,不開新 spec
- ai-rule-carbon-service-foundation → ai-rule-carbon-result-contract
- rtx-physx-mdl-rendering:Kit base 已內建,啟動 app 加 dependency 即可
- sensor-simulation-overlay:Isaac Sim 獨立部署
- api-gateway-and-rate-limiting:Phase 6 凍結

補入內容(C):
- §7 Phase 4 開頭加 NVIDIA Multi-Kit 並行官方定義 cross-reference(roadmap §11.4)
- §7 Phase 4 / Phase 5 各任務後加採用標籤(✅ / ⚠ / ❌)
- §7 Phase 3 待補清單末加業務語意層 vs runtime infrastructure 層註解
- §10 source of truth 表格加 SaaS 路線圖列
- §12 新增 §12.4 P2.5 候選(#1A presence_layer / #2A OVAS Helm)
- §12 新增 §12.5 P3-frozen 候選(#7/#8/#9)
- 頂部 metadata 加文件分工說明

Phase 6 凍結標記(D):
- §7 Phase 6 標題加 ⏸ 凍結中
- §7 Phase 6 段首加凍結決策說明與解凍程序
- §12.5 列出 #7/#8/#9 候選

Lineage 認知(E):§5 風險表加 row 9(lineage graph query API 尚未實作 → 對應 P1 候選 #3)
候選 #4(F):§12.2 補入 coordinator-session-lifecycle-events-audit

收斂後:
- workflow v3 不重述 roadmap 的決策矩陣、spec id、§11.4 Multi-Kit 定義、硬體 §9
- roadmap 不重述 workflow v3 的 sequence diagram、PR checklist、服務測試命令
- 兩份文件互補不替代,互相 cross-reference

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

* docs(workflow): 補 review follow-up(§10.1 / §4.2 / metadata)

依 PR #8 code review 補正:

W1. §10.1 Capability Spec 對應 Phase(行 814+)
- 標題「9 份」→「10 份」
- 加 row `runtime-verification-task-status` Phase 3 ✅
- 表頭上方加「對應 SaaS 路線圖 §1.4 OpenSpec 已歸檔 change → 現行 spec 溯源表」cross-reference
- main 上 openspec/specs/ 實際有 10 個 capability(PR #20 之後新增)

W2. §4.2 驗證證據分層(行 262)
拆「Single Kit GPU Render」一個 row 為 3 個 row,並區分 dedicated 為第 4 個:
- Single Kit GPU Render (real IFC→USDC)            🚫 blocked → 對應 P0 候選 #1
- Single Kit GPU Render (worker-hosted fixture)    ✅ 通過(PR #20 commit `0e94a5b`)
- Same-Kit Concurrent Stream (primary + spectator) ✅ 通過(PR #20 commit `0e94a5b`)
- Dedicated Multi-Kit Routing (≥2 Kit processes)   🟡 在另一分支驗證中
避免一個 cell 混合 blocked 與 passed 兩種狀態。

Suggestions 補正:
- 頂部 metadata(行 7、14)markdown link display text 由 path 字串改為語意化
  「SaaS 路線圖 2026-05」label,避免 raw markdown 中 display 與 href 不一致
- 頂部 metadata(行 11)「9 份 spec」→「10 份 spec」(補 runtime-verification-task-status)
- §10.1 衝突解決順序段(行 831)補一句說明本文件與 SaaS 路線圖屬 OpenSpec 補充
  planning artifact,不在優先順序內覆蓋 openspec/specs/ 權威

對應 PR #8 review (#8 (comment))
的 Warnings + Suggestions。

git diff --check: ✓ no whitespace issues

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

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: monkey1sai <monkey1sai@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request May 22, 2026
…egex unit test

Review feedback on PR #100 from senior code reviewer:

- **Important #1**:`_run_powershell_conversion` 內 `[-800:]` tail truncation 可能
  把 spec scenario 1 required substring `"---- stderr tail (last 100 lines) ----"`
  砍掉。改成 message 開頭顯式 prepend `kit_stdout_log: ` + `kit_stderr_log: ` 兩行
  +tail(額度提到 3000 chars,容納 header 與 tail 內容),保證 spec 要的 substring 仍在。
- **Important #2**:加 `test_run_powershell_conversion_regex_extracts_log_paths_from_ps1_throw`
  unit test,monkeypatch `subprocess.run` 返回真實 ps1 throw heredoc shape(含 Windows
  path 的 `C:\` drive-letter colon 與 backslash),assert regex 抓兩個 path 對 +
  ConversionAuthorityError.message 含 spec substring。鎖定 ps1 throw shape ↔ Python
  regex 契約,L1 直接 cover。
- **Minor #3**:`import re` 移到 module top(consistent with other stdlib imports),
  移除 inline `import re as _re`。

Verification:
- streaming-server pytest:**34 passed**(33 + 1 新 regex test)
- 既有 33 case 不破

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request May 22, 2026
…error 帶 log path (#100)

* streaming-server capture Kit conversion logs:async redirect + result.error 帶 log path(streaming-server-capture-kit-conversion-logs)

完整 OpenSpec change(scaffold + apply 同 commit)。

## Why

2026-05-22 user 用 341MB 真實 IFC 跑 fast-mvp loop,coordinator pipeline 全 work
(下載 / dispatch / sandbox / auto-poll / ingest),但 Kit subprocess `exit 0` 沒寫
model.usdc,error message 只有 ps1 line 316 throw 字串(`"output was not created"`)。

根因:`bim-streaming-server/scripts/convert-ifc-to-usdc.ps1` 用 `Process.Start` 沒設
`RedirectStandardOutput` / `RedirectStandardError`,Kit subprocess 所有 stdout/stderr
直接寫到 PowerShell host 主控台(被吞)沒進 conversion result。任何「Kit silently exit 0
但沒寫 output」debug 線索皆無。

## What changed(9 files / +575 / -15)

### ps1(+77 / -15)
- `convert-ifc-to-usdc.ps1::Invoke-KitConversion`:
  - 加 `$startInfo.RedirectStandardOutput = $true` / `RedirectStandardError = $true`
  - artifact dir 內開 `kit-stdout.log` / `kit-stderr.log`(AutoFlush UTF-8 StreamWriter)
  - async `Register-ObjectEvent` + `BeginOutputReadLine` / `BeginErrorReadLine` 避免
    sync ReadToEnd + WaitForExit 大量 output 經典 deadlock
  - finally:second WaitForExit(drain pending events)→ Unregister-Event → close writer
  - 失敗 throw 改 multi-line message,含:reason、kit_stdout_log 路徑、kit_stderr_log 路徑、
    stderr last 100 行 tail、stdout last 50 行 tail
  - 成功時 log file 仍保留(baseline 對比用)

### Python adapter(+15)
- `conversion_authority.ConversionAuthorityError`:加 optional `metadata: dict | None`
  attribute(backward compat:既有 raise 不影響)
- `ifc2usdc_powershell_adapter._run_powershell_conversion`:subprocess fail 時 regex 從
  combined stderr/stdout 抓 `kit_stdout_log:` / `kit_stderr_log:` 兩行 host absolute path,
  附到 ConversionAuthorityError.metadata
- `conversion_authority.StreamingConversionStore._fail_job`:加 optional `metadata` 參數,
  merge 進 `result.error` dict;catch ConversionAuthorityError 那條 path 傳入 exc.metadata

### Pytest(+58)
- `tests/test_host_native_conversion_service.py` 加 2 case:
  - failed conversion with metadata → result.error 含 kit_stdout_log / kit_stderr_log
  - regression:沒 metadata 不該硬塞 keys(`FakeFailedConverter` 既有 path 不破)

### OpenSpec(5 files / +398)
- proposal / design / tasks / acceptance / `streaming-ifc-usdc-conversion-authority`
  MODIFIED requirement(`Conversion failures expose actionable diagnostic`)+ 4 新 Scenario

## GitNexus blast radius = LOW

| symbol | risk | d=1 |
|---|---|---|
| `_run_powershell_conversion` | LOW | `convert`(同 class) |
| `Ifc2UsdcPowershellConverterAdapter` | LOW | `host_native_conversion_service.py` import |

## Verification

| Level | Result |
|---|---|
| L1 streaming-server `pytest tests -q` | **33 passed**(31 既有 + 2 新) |
| L1 coordinator `npm run verify` | **173 passed**(不動,regression OK) |
| L1 root pytest | **9 passed** |
| L2 `openspec validate streaming-server-capture-kit-conversion-logs --strict` | **valid** |
| L2 `openspec validate --specs --strict` | **26 passed / 0 failed** |
| L3 GitNexus pre-impact | LOW |
| L4 真實 runtime | **跳過** — 留 merge 後重啟 streaming-server 讀新 code + user 重 trigger 341MB IFC,看 GET /result error 含 kit_stdout_log / kit_stderr_log 並 tail 看 Kit 真實錯誤訊息 |

## Backward compatibility

- 既有 ConversionAuthorityError 不帶 metadata raise 點(>10 處)全部不變(metadata 預設 None)
- result.error 加 keys 是純 additive(`code` / `message` 仍在)
- callback outbox payload 不變(metadata-only,log paths 屬 host-local diagnostic,
  不轉發雲端,per `conversion-webhook-lifecycle` 原則)
- 既有 11+168+9 tests 全綠

## Predecessor / Scope

✓ Predecessor:`coordinator-auto-poll-streaming-conversion`(archive PR #99,2026-05-22)
本 change 只加觀察性,**不嘗試 fix Kit 自己為什麼 silent exit 0 沒寫 USDC**(那是 Kit /
HOOPS 本身的問題);下一個 change 可以拿本 change 收集的 Kit log 做 root cause analysis。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(streaming-server): code review:message 顯式 prepend log paths + 加 regex unit test

Review feedback on PR #100 from senior code reviewer:

- **Important #1**:`_run_powershell_conversion` 內 `[-800:]` tail truncation 可能
  把 spec scenario 1 required substring `"---- stderr tail (last 100 lines) ----"`
  砍掉。改成 message 開頭顯式 prepend `kit_stdout_log: ` + `kit_stderr_log: ` 兩行
  +tail(額度提到 3000 chars,容納 header 與 tail 內容),保證 spec 要的 substring 仍在。
- **Important #2**:加 `test_run_powershell_conversion_regex_extracts_log_paths_from_ps1_throw`
  unit test,monkeypatch `subprocess.run` 返回真實 ps1 throw heredoc shape(含 Windows
  path 的 `C:\` drive-letter colon 與 backslash),assert regex 抓兩個 path 對 +
  ConversionAuthorityError.message 含 spec substring。鎖定 ps1 throw shape ↔ Python
  regex 契約,L1 直接 cover。
- **Minor #3**:`import re` 移到 module top(consistent with other stdlib imports),
  移除 inline `import re as _re`。

Verification:
- streaming-server pytest:**34 passed**(33 + 1 新 regex test)
- 既有 33 case 不破

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request May 28, 2026
CodeRabbit P0 修正(回應 PR #136 review comments):

1. _materialize_sidecars 短路 bug:adopt 拿到 sidecars 但
   mapping_has_ifc_type / mapping_has_ifc_name 皆 falsy 時,SHALL 仍跑
   sidecar pass + enumeration。對齊 spec scenario「HOOPS success without
   IFC CustomData triggers sidecar pass」(adopted-but-semantic-empty 正是
   本 PR 主要 cover 的情境)。

2. _run_ifcopenshell_semantic_sidecar 在 by_type 失敗時改 return None,
   不寫 empty sidecar。對齊 docstring「never raises」+ spec scenario
   「Missing IFC source or IfcOpenShell unavailable stays honest」。

3. 過濾 IfcProduct 沒 Representation(IfcSite / IfcBuilding /
   IfcBuildingStorey / IfcSpace),避免 mesh-prim ordinal join 全錯位。
   shape_index 從 0 連續編號,不留空隙。

4. Spec.md normative body 全部翻成繁體中文,parser headers(Requirement /
   Scenario / WHEN / THEN / AND)與 Python identifier / IFC keywords / API
   path 保留原文。新增 Scenario「Sidecar pass filters IfcProduct without
   renderable Representation」覆蓋 #3 fix。

5. tasks.md helper 名稱對齊實作(_load_ifc_semantic_sidecar /
   _sidecar_entry_for_mesh_index),verify 命令改走 .venv\Scripts\python.exe
   對齊 CLAUDE.md §3 venv 強制要求。

6. design.md 加 P2 follow-up note:sidecar 落在 /artifacts public mount
   與既有 element_mapping.json 公開暴露程度一致,本 change 不增量改變
   attack surface;hardening 屬獨立 follow-up。

新 test cover:
- test_sidecar_pass_skips_ifcproduct_without_representation(#6 fix)
- test_sidecar_pass_returns_none_when_by_type_raises(#2 fix)
- test_materialize_runs_sidecar_pass_when_adopt_returns_semantic_falsy(#1 fix)

Verify:
- pxr-無關 helper tests (1.1/1.2/1.3 + #6 + #2):5/5 passed
- pxr-依賴 integration test (#1):留 L4 host-native verify
- npx openspec validate streaming-server-ifcopenshell-semantic-sidecar-pass --strict ✓
- git diff --cached --check: clean (LF/CRLF 是 Windows convention warning)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request Jun 1, 2026
…#4 #10 #11 #13) (#140)

* docs(openspec): 提案 harden-host-native-conversion-service

CH-1 收斂 host-native conversion service 5 個 hardening 點(#3 artifacts 防穿越 / #4 誠實 health / #10 placeholder 全檔掃描 / #11 HOOPS 失敗診斷結構化 / #13 storage sandbox root 顯式)。proposal/design/tasks + spec delta(host-native-conversion-authority-service ADD 4 條、streaming-ifc-usdc-conversion-authority ADD 1 條);openspec validate --strict 通過。

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

* fix(streaming-server): harden host-native conversion service (#3 #4 #10 #11 #13)

CH-1 apply — conversion service 5 點 hardening:
- #3 /artifacts 改 scoped GET /artifacts/{job_id}/{filename} + relative_to 防穿越,移除 StaticFiles flat mount 與 broad except
- #4 /health 誠實回報:呼叫 converter.preflight(),未就緒回 degraded+reason(HTTP 維持 200);HeadlessConverterNotConfigured 補 preflight
- #10 placeholder 偵測改全檔掃描(移除 4096 上限);_PLACEHOLDER_MARKERS 下放 conversion_authority 單一 source
- #11 HOOPS 失敗 log path 改 ##CONV_META## sentinel JSON 抽取,取代脆弱 prose regex
- #13 storage sandbox root 顯式:未設 STORAGE_ROOT 即 raise(不退化 cwd);start-host-native-conversion-service.ps1 補設 STORAGE_ROOT

驗證: pytest 80 passed(baseline 61 + 19 新增 CH-1 案例,零回歸); openspec validate --strict 通過; gitnexus impact 6 symbol 全 LOW。

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

* fix(streaming-server): CH-1 review fixes (空字串 storage_root / preflight 死碼 / sentinel regex)

對抗式 review(opus 5 lens)findings:
- [major] storage_root=空字串繞過 cwd-fallback 契約: __init__ 改 strip + truthy 檢查,空白(含純空白 env)即 raise;補 test_adapter_ctor_raises_when_storage_root_blank 鎖回歸
- [minor] preflight storage_root 檢查是死碼(Path 物件恆 truthy): 移除,__init__ 為 single gate
- [minor] #11 sentinel regex 非貪婪遇 log path 含字面右括號會截斷: 改貪婪 + 行錨 + MULTILINE
- 連帶: l4_verify_sidecar_pass.py 直接建構 adapter 補 storage_root;proposal Impact 聲明此 caller

驗證: pytest 81 passed(80 + 1 新回歸測試,零回歸)。review agent 殘留 scratch 檔已清除。

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

* fix(streaming-server): CH-1 外部 reviewer fixes (跨 job 穿越 / health reason / mkdir)

CodeRabbit + Copilot + Codex review findings:
- [P2 真 security] /artifacts 跨 job 穿越: Windows filename 含 encoded backslash(%5C)被 Path 當分隔可讀 sibling job;改兩層 relative_to(job_dir 在 artifacts_root 內 + candidate 在 job_dir 內)enforce per-job;補 test_artifacts_route_rejects_cross_job_backslash_with_404
- [minor] /health reason 改 message or code(actionable);加 broad except → degraded(不因 converter 異常回 500)
- [minor] start-host-native-conversion-service.ps1 補 STORAGE_ROOT mkdir

不改(已記錄權衡): placeholder 全檔 read_bytes(explore demo-scope YAGNI,follow-up);全形標點 lint(repo 中文註解慣例)。

驗證: pytest 82 passed(81 + 1 跨 job 回歸測試)。

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request Jun 15, 2026
* plan: MinIO 輪詢自動 intake(O4 觸發機制 B 案)實作計畫

依 superpowers writing-plans 規格產出 9 個 task 的最小可機械執行計畫:
coordinator minioWatcher(env opt-in 預設關,ListObjectsV2 輪詢 → 新 */model.ifc
→ 確定性 idempotency key → loopback POST /api/external/ifc-ready)、
GET /api/external/minio-watch/status、#/conv ConversionSchedulingPage 狀態 Panel、
含 backend 整合測試與 Playwright browser E2E(STUB MINIO + STUB CONVERSION,不碰按鈕)。

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

* plan: fix Task6 tmpApp 死碼刪除 + WatcherLogger interface 補 withTraceId optional(四軸 review minor 落地)

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

* task#0: feat(coordinator): minioWatch config 欄位(env opt-in,預設關)

新增 CoordinatorConfig 9 個欄位(minioWatch* + minioWatchSelfBaseUrl)
與 loadConfig 對應的 env 讀取邏輯;interval 下限夾 10。
新增 tests/config-minio-watch.test.ts(3 it 全通過)。

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

* task#0: fix config-minio-watch 測試隔離 + minioWatchSelfBaseUrl 覆蓋

afterEach 的 MINIO_KEYS 補上 MINIO_WATCH_SELF_BASE_URL,避免該 env 在同一
process 後續測試殘留造成 test pollution;第一個「預設關閉」案補 expect(
c.minioWatchSelfBaseUrl).toBe(""),讓該欄位有行為斷言(原本零覆蓋)。

純測試改動:minioWatchSelfBaseUrl 欄位與讀取邏輯 task#0 已實作(default ""),
本次只補上對既有行為的斷言與清理,未動 production code。

備註(未修,環境政策擋下):spec §3 要求同步 .env.example 補 9 個 MINIO_WATCH_*
欄位佔位;本環境對 .env / .env.example 的 Write/Edit/Bash 寫入全被 deny rule +
auto-mode classifier 擋下,無法在此 commit 落地,留待指揮官以放行後補上。

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

* task#0: fix config-minio-watch 測試環境清理 + 補 SELF_BASE_URL env 覆蓋

Important #1:config.ts 頂層 dotenv.config() 會在 vitest 首次 import 時把本地 .env
注入 process.env,只有 afterEach 不足以保護第一個 case。新增 beforeEach 對 MINIO_KEYS
逐一 delete,確保每個 case 從乾淨環境自行設值,避免某機器 .env 補了 MINIO_WATCH_*
導致「預設關閉」case 誤判。

Important #2:minioWatchSelfBaseUrl 是整合測試注入 loopback base url 的關鍵 seam,
原本只斷言預設空字串。新增 case 斷言 MINIO_WATCH_SELF_BASE_URL=http://127.0.0.1:9999
時 config 回傳該值;已驗證若 env key 拼錯此 case 會以 expected '' to be ... 失敗,
擋住「watcher 自打空字串不報錯」的靜默失效。

純測試檔變更,production config.ts 未動。coordinator 全套 321 tests 綠。

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

* task#0: fix interval clamp 移至 overrides 合併後 + .env.example 補 MINIO_WATCH_* 欄位 + kitpool fixture 型別補齊(quality review 2 項 + tsc,指揮官修)

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

* task#2: feat(coordinator): 新增 @aws-sdk/client-s3 + s3-request-presigner(MinIO list/presign,唯讀兩 API 面)

理由:自寫 AWS SigV4 簽章易錯難審,SDK 為 S3 互通事實標準;watcher 僅用
ListObjectsV2Command(唯讀 list)+ GetObjectCommand presigner(presigned GET)。

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

* task#1: fix 補 package.json engines node>=20.0.0(AWS SDK 鏈強制 Node 20+,與部署 Dockerfile 對齊)

quality finding IMPORTANT #2:@aws-sdk/client-s3 透過 @aws-sdk/core /
token-providers 等子套件帶入大量 engines node>=20.0.0 約束,但 package.json
原本無 engines 欄位、npm runtime 不強制 engine range。若 coordinator 容器跑
Node 18 則這些子套件行為未定義。

修法:在 package.json 補 "engines": { "node": ">=20.0.0" },把約束顯式化。
與現況一致——infra/docker/coordinator-web-plane.Dockerfile 已用
node:20-bookworm-slim、本機 runtime 為 v22.22.0,皆滿足。

驗證:npm run verify(build + 321 tests / 25 files 全綠),npm 未噴
EBADENGINE/unsupported-engine 警告。純 package metadata 變更,未動任何
code symbol(不觸發 GitNexus impact)。

IMPORTANT #1(S3Client 必須顯式帶 credentials 短路隱式憑證鏈/IMDS 探測):
S3Client 構造屬 task#3(watcher)尚未實作,本分支 src 內無 new S3Client,
無可改 production code;列為 task#3 實作約束 + PR body 文件項,不在本次補。

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

* task#1: fix 補 AWS SDK 顯式 credentials 防護網 + 揭露非預期傳遞依賴(quality IMPORTANT #1/#2)

IMPORTANT #1:新增 dependency-layer guard tests/aws-sdk-credentials-guard.test.ts,
以哨兵值等式 + <1s 解析時間驗證 new S3Client 傳顯式 credentials 時不落入
default provider chain(IMDS / shared-config 靜默撈無關金鑰)。與尚未建的
minioWatcher.ts 無耦合(task#3 才建),鎖的是 client-s3 依賴層行為契約。
mutation 驗證:移除顯式 credentials 後 guard 確實紅(撈到 ~/.aws AKIA… 金鑰)。

IMPORTANT #2:plan Task 2 補充透明度揭露——@aws-sdk/core 傳遞拉入
@aws-sdk/nested-clients 與 @aws/lambda-invoke-store(均不直接使用、npm 無法排除),
並澄清 spec「唯讀兩 API 面」指主動呼叫面、非第三方依賴設計。Task 3/5 inline
標註 S3Client MUST 顯式 credentials + PR checklist 須確認 guard 綠。

驗證:cd bim-review-coordinator && npm run verify → tsc 綠 + 26 files/323 tests passed
(baseline 25/321,+1 file +2 tests 即本 guard)。

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

* task#1: fix AWS env 隔離清單補 web-identity 三變數(quality review,指揮官修)

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

* task#2: feat(coordinator): minioWatcher 純函式核心(key 解析 + 確定性 idempotency/correlation 導出)

新增 src/services/minioWatcher.ts 純函式層(不含 I/O):
- deriveIntakeFromKey:prefix/keySuffix 去除 + 恰兩層驗證 → projectId / externalModelVersionId
- idempotencyKeyFor:bucket|key|etag sha256 前 16 hex,前綴 mw_
- correlationIdFor:minio-watch-<hash8>
- stripEtagQuotes:去除 S3 ETag 外層引號
新增對應 vitest 單元測試 6 個,全部通過。

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

* task#2: fix minioWatcher 補 idempotency/correlation key 不得含 `|` precondition JSDoc + prefix 非 '/' 結尾防衛(quality IMPORTANT #1/#2)

IMPORTANT #1:idempotencyKeyFor / correlationIdFor 的 hash input 以 `|` 分隔 bucket|key|etag;S3 object key 允許任意 UTF-8,含 `|` 的 key 可能與另一組撞 hash 致第二個 intake 被當 idempotent_replay 靜默丟棄。補 JSDoc 標註 key 不得含 `|` 之 precondition(行為不變,production 路徑規約不含 `|`)。

IMPORTANT #2:deriveIntakeFromKey 在 prefix 非空且不以 '/' 結尾時(如 prefix='89' 對 '899/...')會 startsWith 命中後切出 projectId='9' 而非 '899',造成 job 掛錯 project 且無 error log。新增頂部防衛回 ok=false 帶 reason,並補一個邊界測試(red→green)。

驗證:tests/minio-watcher-derive.test.ts 7 passed;full coordinator suite 330 passed / 27 files;tsc build exit 0。

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

* task#2: fix deriveIntakeFromKey 雙斜線 key 靜默正規化 + 補兩條 ok=false 分支測試

IMPORTANT #1:withoutSuffix.split("/").filter(Boolean) 會吃掉空 segment,
使 S3/MinIO 獨立 key `899//xxx/model.ifc` 被誤判為合法兩層、與正常
`899/xxx/model.ifc` 撞同一 projectId/modelId 重複觸發(靜默資料品質問題)。
改為 split("/") 不 filter,恰兩層且所有 segment 皆非空才合法。

IMPORTANT #2:補測 deriveIntakeFromKey 兩條原先無覆蓋的 ok=false 分支
(有效 prefix 但 key 不在 prefix 下 / key 不以 keySuffix 結尾),
此二者為 watcher loop 過濾惡意/無關 object 的第一道防線。另補雙斜線
頭/尾各一 case 鎖死 IMPORTANT #1 行為。

驗證:npm run verify(build + 27 檔 334 測試全綠;derive 套件 7→11)。

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

* task#3: feat(coordinator): minioWatcher loop(list/baseline/觸發 loopback intake/status,fake S3 stub 驗)

setTimeout 鏈(比照 pollConversionResult)+ in-memory seen Map(key→etag):
首輪 baseline 不觸發、後續輪新 key/新 etag 觸發 loopback POST /api/external/ifc-ready,
getStatus() 回完整狀態(baseline/seen/triggered/skipped_malformed/last_error/last_triggered)。
S3Client forcePathStyle + 顯式 credentials(IMPORTANT #1:避 default chain IMDS timeout),
presign GET URL 作 source_ifc.ref。新增 5 條 fake S3/intake stub 測試全綠;
build clean、全 339 測試通過。aws-sdk-credentials-guard 仍綠。

註:loop test 的 watcher 區域型別改用具名 MinioWatcherStatus(原 spec Record<string, unknown>
在 strict tsc + include tests/ 下不可賦值),不影響任何斷言。

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

* task#3: fix presign 失敗逐物件隔離 + app.ts 掛載 watcher + minio-watch/status route(quality IMPORTANT #1/#3)

修 quality 三項發現:

- IMPORTANT #1:minioWatcher.triggerIntake 的 getSignedUrl(presign) 原在 try 外,
  失敗會浮到 tick() 外層 catch → 覆蓋整輪 last_error 且中斷同輪其餘物件觸發。
  改用獨立 try/catch 包裹,失敗只 recordTriggered(key,null,...) 後 return,與下方
  fetch try/catch 對等(per-object 隔離,不污染整輪 last_error)。
- IMPORTANT #2:經 TDD 量測,現行 JSON.parse(L231) 已在 triggered_total += 1(L232)
  之前,無效 JSON body 時 parse 拋例外 → catch 記錯誤、計數不 +1,計數與 error 狀態
  本即一致。新增 minio-watcher-loop「2xx 但非 JSON」回歸測試鎖此不變式(無 code 改動)。
- IMPORTANT #3:startMinioWatcher 未掛入 app.ts、status endpoint 不存在(404)。
  createCoordinatorApp 加 env opt-in 啟動段(server listening 後取實際 port 作 selfBase;
  測試以 minioWatchSelfBaseUrl 立即啟)、新增唯讀 GET /api/external/minio-watch/status
  (關閉回 enabled=false、不洩漏 credentials)、dispose 呼叫 watcher.dispose()。
  WatcherLogger.anomaly 的 fields 改用真實 AnomalyData 型別,使 app.ts 傳入的真
  StructLogger 在 strict 函式型別檢查下可賦值(否則 TS2322)。

驗證:cd bim-review-coordinator && npm run verify → build(tsc) 綠 + 342 tests 全綠
(含新 minio-watch-status-route 2 + 新 loop 回歸 1 + 既有 aws-sdk-credentials-guard)。

GitNexus impact(createCoordinatorApp, upstream)=LOW(僅 index.ts 呼叫,改動全 additive
且 gated by default-off config)。detect_changes(staged) 在 linked worktree 看不到 staged
(已知坑),fallback git diff --name-only --cached 自查 scope 乾淨(4 檔,皆預期)。

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

* task#3: fix minioWatcher intake POST 加 AbortSignal.timeout 防 loop 凍結 + 補重啟 idempotent_replay 整合測試

Important #1:triggerIntake 的 loopback fetch 原無逾時保護,若 app 因負載/死鎖長時間不回
/api/external/ifc-ready,fetch 永不 resolve、tick() 無限 await、finally 的下一輪 setTimeout
不觸發 → 整個 watcher loop 凍結。比照 streamingConversionClient 的
AbortSignal.timeout(requestTimeoutMs) 模式,新增 optional intakeTimeoutMs(default 10s)並在
fetch 帶 signal: AbortSignal.timeout(...)。逾時 AbortError 由既有 catch 與其他網路失敗同等
對待(recordTriggered + return,不上浮、不計 triggered_total)。

Important #2:補「重啟(新 watcher 實例、同 store)重掃同 key 同 etag → idempotent_replay
仍計觸發且 ifc_ready_job_id 相同」端到端整合測試(spec §6 測試需求第 3 點),覆蓋
idempotencyKeyFor 確定性 + store 去重組合路徑。

驗證:npm run verify(tsc + vitest)29 files / 344 tests 全綠;minio-watcher-loop 8 tests 全綠
(含新增 2 條,逾時測試先以 hanging intake stub 親見凍結 timeout 失敗、加 signal 後通過)。

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

* fix: minioWatcher 對抗複驗四項 findings(tenant 動態化 / dispose 安全 / SSRF 防護 / 去時鐘 flaky)

逐項修掉 reviewer 未閉合的對抗複驗 findings:

- [t1] tenant_id 不再硬編碼:MinioWatcherOptions 加 tenantId;config 加
  minioWatchTenantId(env MINIO_WATCH_TENANT_ID,預設 tenant_demo_001 維持現行為);
  app.ts 掛載傳入。修掉部署切 tenant 時 watcher intake 帶錯 tenant 的靜默資料污染。
- [t2] dispose() 改 async:先 stopped=true 停排程 → await 當前 in-flight tickPromise
  settle(2s 上限 race)→ 才 client.destroy(),避免在 SDK 請求進行中銷毀 client 觸發
  unhandled rejection。app.ts dispose 配合 await(shutdown.ts 早已 await,相容)。
- [t3] SSRF 防護:startMinioWatcher 入口以 assertLoopbackSelfBaseUrl 驗 selfBaseUrl,
  host 須為 127.0.0.1/localhost 且 protocol http:,否則 fail-fast throw,防 env 注入時
  X-Webhook-Secret 經 SSRF 洩漏給任意 host。
- [t4] 去時鐘 flaky:status 加單調遞增 poll_count(每輪 list 成功 +1),測試改以 poll_count
  遞增取代 last_poll_at 時間戳比較(消同毫秒 false-negative);hanging-intake 測試
  intakeTimeoutMs 150ms→600ms 放寬,去 CI 高負載抖動。

測試:新增 t1/t2/t3/t4 失敗測試(先紅後綠),minio-watcher-loop 12 案全綠;
unit_kitpool 全欄位 config fixture 補 minioWatchTenantId。npm run build 0 error,
coordinator 全測試 348 passed。

殘留:bim-review-coordinator/.env.example 需補一行 MINIO_WATCH_TENANT_ID=(接在
MINIO_WATCH_KEY_SUFFIX= 之後),該檔受 agent deny 規則保護無法由本 agent 寫入。

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

* task#4: fix minio-watch status route 測試補 last_error 斷言(消除 test lie)

第二個 it 標題承諾「status 形狀完整(含 last_error 欄位)」,但原斷言只驗
last_poll_at / triggered_total / skipped_malformed_total,缺 last_error,
屬標題與斷言不一致(test lie)。MinioWatcherStatus 介面明確宣告
last_error: string | null,status route 直接回 getStatus() 含此欄位。
補 expect(res.body).toHaveProperty("last_error") 對齊標題承諾。
已 scratch 驗證該斷言有牙(欄位缺失時 toHaveProperty 會 fail),非空斷言。

純測試改動,未動 production code;npm run verify 全綠(29 files / 348 tests)。

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

* task#4: fix minio-watch status route teardown await dispose + 雙啟動 guard 不變式註解

IMPORTANT #1:minio-watch-status-route.test.ts afterEach 改 await active.dispose()。
dispose 自 watcher 存在時為 async(await in-flight tick settle + S3 client.destroy())。
原 fire-and-forget 會讓 watcher 收斂與 io.close/server.close 競態,違反 shutdown.test.ts
不變式(dispose 先完成再關閉)。改 await 後與該不變式一致,杜絕 timer/socket 洩漏到
下一測試。

IMPORTANT #2:app.ts startMinioWatcherIfEnabled 加不變式註解,顯式說明兩條啟動路徑
(listening 事件 + selfBaseUrl 已設時立即啟動)共用同一 minioWatcher guard 達成
idempotent:即使兩條同時成立,立即路徑先設好 minioWatcher,listen callback 為非同步,
listening 事件到達時 guard 直接 return,不會啟第二個 watcher。純註解、零 runtime 變動。

驗證:affected 5 tests 綠;coordinator 全套 348/348 綠(重跑 3 次穩定);tsc build 通過。

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

* task#4: fix minio-watch 兩處測試謊言/flaky(status last_error 補非空斷言 + dispose 補 await)

對抗複驗 IMPORTANT #1(minio-watch-status-route.test.ts):
「watcher 啟用但 endpoint 不可達」一案標題承諾驗到 last_error,但原斷言僅
toHaveProperty("last_error")——Vitest 只驗 key 存在不驗非空值。GET 請求在
createCoordinatorApp 返回後幾乎立刻發出,而 watcher 首輪是 setTimeout(runTick, 0)
可能尚未跑完,此時 last_error 仍為 null,斷言仍通過(test lie,與 271af5e 宣稱
「消除 test lie」前後不一致)。修法:加 getStatusUntil 輪詢 status endpoint 直到
last_error 非 null,再 expect(...).toMatch(/ECONNREFUSED/),真正證明 tick 跑過且
ECONNREFUSED 被 watcher 捕捉。已 scratch 證實原斷言 last_error 此刻為 null,並把
matcher 故意改錯確認新斷言有牙(fail: expected 'connect ECONNREFUSED 127.0.0.1:1'
to match /.../)。

對抗複驗 IMPORTANT #2(minio-watcher-loop.test.ts 行 397):
重啟 idempotent_replay 整合測試中 watcher.dispose() 為 async(await in-flight tick
settle)卻未 await 就改 state.objs 並建新 watcher。舊 watcher 50ms 輪詢若有 in-flight
tick 剛進 listAllKeys,該 tick 的 S3 list 可能在 state 變更後才打到 stub,多發一筆
intake 讓 received 累積到第 3 筆,打亂「恰好 2 筆」邏輯形成 flakiness。修法:補
await watcher.dispose() 後再改 state 及建新 watcher,與本檔 afterEach(行 12)、
dispose 測試(行 297–299)的既有 await 模式一致。

純測試改動,未動 production code。npm run verify 全綠(29 files / 348 tests,與
271af5e 計數一致無回歸)。scope 自查:git diff --name-only --cached 僅兩個測試檔,
無 src/ 變更(GitNexus detect_changes 因 linked worktree 看不到 staged,已 fallback
git 自查)。

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

* task#4: fix dispose 介面統一 Promise<void> + 全呼叫端補 await(quality review,指揮官修)

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

* task#5: minioWatcher → 真 coordinator intake 整合測試(job 進 store + 重掃 idempotent_replay)

新增 tests/minio-watch-intake-integration.test.ts:
- it1:app listen(0) 取實際 port,以 startMinioWatcher 直接構造 watcher 指向真
  coordinator loopback intake(app 自帶 watcher 關閉避免雙啟);baseline 後新增物件
  → 真 intake 鏈建立 ifc-ready job,store 可見(project_id=988 / version=zzz)。
  streaming 不可達 + ifcDownloadStrict:false → dispatch_failed 但 job 仍建立。
- it2:同 idempotency key POST 兩次(等價重啟重掃)→ 第二次 idempotent_replay,
  回同一 ifc_ready_job_id 不新建。

為何 watcher 直接構造而非走 app 自啟:production config 對 minioWatchIntervalSeconds
夾下限 10s(防忙迴圈),整合測試窗內等不到第二輪 tick;直接用同一支 startMinioWatcher
(app.ts 掛載的也是它)以 0.2s tick 快驗,零觸碰 production config。app 自啟與 status
route 由 minio-watch-status-route.test.ts 覆蓋。
triggered_total 改 waitFor 而非同步斷言:coordinator store.create 先於 fetch 回應,
同步讀偶發見 0(消除 full-suite 並行下的 race flake)。

驗證:npm run verify 綠(build + 350 tests);full-suite 連跑 6 次全綠(消除 flake)。

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

* task#5: fix 整合測試 teardown 合約對齊 + 端到端斷言補強

修掉 quality review 三項 IMPORTANT 發現(皆限 tests/minio-watch-intake-integration.test.ts):

- #1 afterEach 的 active.dispose() 未 await(fire-and-forget),違反 dispose():Promise<void>
  合約;改 await active.dispose(),對齊 auto-poll-conversion.test.ts,避免特定負載下於
  server.close() 前非同步 destroy S3 client 引發 unhandled rejection / 競態。
- #2 s3Stub teardown 缺 closeAllConnections():watcher keep-alive socket 殘留會讓
  http.Server.close() callback 永不觸發、afterEach 卡到 hook timeout;對齊
  minio-watcher-loop.test.ts,close 前先強制斷連。
- #3 it1 端到端覆蓋不足(僅驗 project_id/external_model_version_id):補驗
  download_status 終態 'downloaded'(非 failed)、presigned URL 含 X-Amz-Signature、
  etag e9 端到端透傳、watcher last_triggered.error 為 null。download_status 改 waitFor
  終態(修掉 markDownloading 先於 download settle 的 race,同步讀偶見 'downloading'),
  此 wait 並涵蓋原「job 進 store」前置 wait(移除冗餘)。

驗證:npx vitest run(整合測試 3x 連綠)、full suite 30 files / 350 tests 全綠、
npm run build typecheck 通過。僅改測試檔,無 production symbol 變更。

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

* task#5: fix 修正 minio-watch intake 整合測試過時注釋與 it2 dead s3Stub

IMPORTANT #1:it1 行 97 注釋宣稱 presigned GET「會失敗」與終態斷言矛盾。
s3Stub 對任何請求(含 GET)回 200 XML,ifcDownloadStrict=false 下
download_status 實際抵達 downloaded(行 151 expect 為證)。改寫注釋據實描述。

IMPORTANT #2:it2 建立 s3Base 卻從未傳入 app 或任何 watcher(source_ifc.ref
硬編 127.0.0.1:1),為 dead resource allocation(多佔 port + 多串一次
afterEach close + 誤導讀者)。移除 startS3Stub 呼叫與其專用 state,補注釋說明
本 case 不跑 watcher loop 故不需 S3 stub。

驗證:npx vitest run tests/minio-watch-intake-integration.test.ts,baseline 與
改後皆 2 passed。

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

* task#5: fix firstJobId truthy guard(防 undefined===undefined test lie,指揮官修)

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

* task#6: feat(web-viewer): #/conv MinIO 自動偵測 Panel + coordinatorClient.minioWatchStatus(關閉誠實顯示未啟用)

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

* task#6: fix MinIO watcher Panel 靜默失敗 + dispatch_error test-lie + poll_count 型別補漏

三項 quality 修復(finding 對應):

IMPORTANT #1(pages.tsx ConversionSchedulingPage.load):
原本單一 try/catch 串接兩個 await,listIfcReady 或 minioWatchStatus 任一失敗都
共用同一段 catch,且訊息寫死「/api/external/ifc-ready」。
改用 Promise.allSettled 讓兩端點獨立 settle,各給獨立錯誤 state(err / mwErr),
watcher 失敗時 Panel 顯示 data-testid="minio-watch-error" 而非誤導性 placeholder,
也不再把錯誤端點誤標成 ifc-ready。

IMPORTANT #2(console.test.tsx dispatch_error 測試):
原測試只 mock listIfcReady,task#6 後 load() 會續 await 未 mock 的 minioWatchStatus(),
jsdom 真 fetch 非同步 reject 時序不定 → test lie。補
vi.spyOn(coordinatorClient,"minioWatchStatus").mockResolvedValue({enabled:false,...})
固定時序。

IMPORTANT #3(coordinatorClient.ts MinioWatchStatus):
補 poll_count?: number,對齊後端 MinioWatcherStatus.poll_count(loop liveness,
免時鐘解析度依賴),避免 API 回傳值被 TypeScript 靜默丟棄。

新增 ConversionSchedulingPage.test.tsx 兩個 client-render 測試覆蓋 #1 新行為
(兩端點錯誤獨立、互不連坐),已親眼確認 stash 舊 pages.tsx 時 red。
驗證:vitest run 214 passed、npm run build 綠、eslint 四檔 0 error。

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

* task#6: fix #/conv watcher enabled=true 分支讓後端 note 穿透 + 補 enabled=true 計數/race window 渲染測試

IMPORTANT #1:coordinator app.ts:841 的 race window(minioWatchEnabled=true
但 watcher handle 尚未建立)回 { enabled: true, note },無 bucket/prefix/計數。
原本 enabled=true 分支無 mw.note 顯示路徑,操作者只看到一排 dash,無從判斷正常
race 還是真故障。在 enabled=true 分支最前加 {mw.note && <p className="ec-note">…}
讓後端 note 穿透。

IMPORTANT #2:補 spec §6.2「enabled=true → 計數 render」測試,鎖住 bucket/prefix/
last_poll_at + baseline/seen/觸發/跳過 計數字串與 minio-watch-triggered table 的
渲染路徑(先前無測試,模板字串或 table 條件渲染若有 typo CI 抓不到);另補 race
window note 穿透測試(先寫失敗→實作→綠)。

web-viewer-sample vitest 全綠 216/216;npm run build 通過。

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

* task#6: fix poll_count 渲染為輪詢次數 + 斷言(dead field 消除,指揮官修)

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

* task#7: test(e2e): minio-watch 自動 intake vertical slice(STUB MINIO + STUB CONVERSION,不碰按鈕)+ evidence README

E2E spec spawn 真 coordinator(tsx src/index.ts,MINIO_WATCH_ENABLED=true、interval 1s)
+ 本機 fake S3 stub(ListObjectsV2 XML)+ stub conversion(202 queued)。stub 注入
988/auto/model.ifc → watcher 自動 intake → #/conv Panel「啟用中」+ triggered≥1 +
Ifc-ready jobs 988 列,全程不碰任何按鈕。本機實跑 1 passed (13.8s)、0 skipped。
真 MinIO(192.168.20.234:9000)+ 真 IFC→USDC = not observed(P7 部署區,需 credentials
+ host-native GPU runtime)。evidence README 揭露 STUB 限制與 P7 not-observed。

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

* task#7: fix minio-watch E2E 補 UI 層 triggered≥1 + 988 列 conversion=queued 直接斷言

gap1:MinIO 自動偵測 Panel 的「baseline / seen / 觸發 / 跳過」Field 第 3 槽(觸發)
  以 regex 斷言為非零整數,讓 triggered≥1 由瀏覽器 DOM 驗證,而非僅後端 status API 對帳。
gap2:Ifc-ready jobs 表 988 列 scope 到該 Panel(避開 MinIO triggered 表 988/auto/model.ifc
  子字串誤命中)並用 /^988$/ 精確匹配 project 欄,斷言 conversion 欄=queued,
  確認 job 達 dispatched/queued 級。

實測:build:ui 後 npx playwright test → 1 passed (13.7s)、0 skipped;
  Panel 渲染 1 / 2 / 1 / 0、988 列 conversion=queued;截圖
  artifacts/e2e/minio-watch-auto-intake-conv.png 產生。evidence README 同步補載 UI 層斷言說明。

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

* task#7: fix minio-watch E2E 輪詢下限降檔 + Windows 收尾與埠搶占可觀測

修掉 minio-watch-auto-intake E2E 三項 quality 發現:

C1:MINIO_WATCH_INTERVAL_SECONDS=1 被 config.ts 夾至 10s,
E2E 實際以 10s 輪詢,繁忙/cold-start 機器逼近 180s 上限有逾時風險。
config.ts 新增 MINIO_WATCH_INTERVAL_FLOOR_SECONDS(預設 10,唯一降檔入口;
production 不設=floor 10 不變,既有 config-minio-watch floor=10 案不受影響),
兩處夾值改用此 floor。E2E env 設 floor=1 讓 1s 輪詢真正生效。
config 新增 2 個單元 case 鎖死「floor=1→interval=1」「floor 仍夾 override」。

I1:afterAll 在 Windows shell:true 下 kill("SIGTERM") 只殺 cmd.exe,
node 子進程變孤兒續占 freePort。新增 stopCoordinator:Windows 走
taskkill /F /T 連同子樹一起殺,Unix 維持 SIGTERM。

I2:freePort 探測與 coordinator bind 之間的 TOCTOU 窗被搶占時,
原本沉默等滿 60s 才以無脈絡訊息逾時。waitForHealth 加 earlyExit:
coordinator 提早退出即帶 stderr 尾段 fail-fast,指出疑似埠搶占。

驗證:coordinator vitest 全綠(352 passed / 30 files;config 新增 6 passed);
E2E playwright 通過(4.4s,較 baseline 13.8s 顯著縮短=1s 輪詢已生效);
tsc -p 與 eslint --max-warnings 0 皆 0 error。

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

* task#7: fix MinIO watch floor 忙迴圈守衛 + Unix stopCoordinator await-on-exit

important-1(config.ts:328):floor 守衛 Math.max(0,…) 允許 FLOOR=0,FLOOR=0+
INTERVAL=0 時 minioWatchIntervalSeconds 降到 0 → minioWatcher setTimeout(runTick, 0)
每輪 tick 完成立即重排,形成 Node event-loop 忙迴圈不停打 MinIO ListObjectsV2。
改為 Math.max(1,…) 設硬下限 1s;補 config-minio-watch.test.ts FLOOR=0 case 斷言 ≥1。

important-2(minio-watch-auto-intake.spec.ts:101):Unix stopCoordinator 的
kill("SIGTERM") 送 signal 即回、非等進程退出,afterAll 緊接的 fs.rmSync(tmpRoot)
可能與 coordinator async shutdown(仍寫 SESSION_STORE_DIR/EVENT_LOG_DIR)競爭,
有 file-lock 語意的 CI 上造成清理失敗遺留 tmp。Unix 路徑改 await proc.once("exit")
(3s 上限保護)後再 return。

驗證:bim-review-coordinator vitest run 30 files / 353 tests 全綠(含新 FLOOR=0
case 與 minio-watcher-loop 12 tests);tsc -p tsconfig.json build clean。spec.ts
為本機/手動 Playwright gate(無 CI job),standalone tsc 型別檢查通過、未跑 runtime。

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

* fix: minio-watch 對抗複驗 findings 收尾(.env.example 降檔欄位 + E2E s3State 重置)

逐項修掉 reviewer 未閉合 findings:

- [f1] bim-review-coordinator/.env.example 補 MINIO_WATCH_INTERVAL_FLOOR_SECONDS=
  (空值,置於 MINIO_WATCH_SELF_BASE_URL= 之前)。config.ts 已宣告此欄為唯一降檔
  入口(loadConfig 的 Math.max floor),deploy.ps1 missing-key merge 依賴 .env.example
  得知新欄位;不給預設數字以免誤導 production 降檔。

- [f2] e2e/minio-watch-auto-intake.spec.ts beforeAll 守門後顯式重置
  s3State.objs 回單一 baseline 物件(defense-in-depth)。註:reviewer 描述的
  「--repeat-each 第二輪吃掉上輪物件 → triggered_total 卡 0」靜默失敗在 PW 1.60.0
  不重現——實測 --repeat-each 每輪 fresh-import 模組(moduleLoadId 每輪不同),
  模組頂層單例不跨輪洩漏,且本 describe 僅一個 test;故 stated 機制屬誤報。
  仍採其建議顯式重置,以防未來 PW 改為重用模組或新增第二個 test 時殘留 988 被
  當 baseline。修後 E2E 仍綠:單跑 1 passed、--repeat-each 2 為 2 passed、0 skipped。

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

* task#8: chore(coordinator): .env.example 加 MINIO_WATCH_* 欄位名(含預設值,不落實 credentials)

- MINIO_WATCH_ENABLED=false(預設關)
- MINIO_WATCH_INTERVAL_SECONDS=60(預設輪詢間隔)
- MINIO_WATCH_KEY_SUFFIX=/model.ifc(規約檔名)
- credentials 欄位留空;MINIO_WATCH_INTERVAL_FLOOR_SECONDS / MINIO_WATCH_SELF_BASE_URL 保留(對抗複驗新增欄位)
- 全 coordinator 驗證:30 test files / 353 tests pass
- 全 web-viewer-sample 單元測試:22 test files / 216 tests pass
- web-viewer-sample lint 47 errors 均為既有 pre-existing,非本 task 引入

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

* task#8: fix bim-review-coordinator/.env.example 補 MINIO_WATCH_TENANT_ID 欄位

config.ts L414 讀取 MINIO_WATCH_TENANT_ID(預設 tenant_demo_001),但 .env.example
的 MINIO_WATCH_* 區塊缺此 key。deploy.ps1 Phase 2 missing-key merge 以 .env.example
為 key source(preflight-env.ps1 Get-EnvAudit),example 缺 key → 部署時不補入也不報
missing,操作員看不到此欄位;切 tenant 未設值會讓 watcher intake 帶錯 tenant(靜默
資料污染進 store 與雲端 callback)。

- .env.example:在 MINIO_WATCH 區塊末、SELF_BASE_URL 之前加入 MINIO_WATCH_TENANT_ID=
  (空值,不給預設值,讓 deploy missing-key merge 能偵測並提示操作員顯式設定)。
- 新增 tests/env-example-minio-watch-parity.test.ts:鎖 config.ts 讀取的每個
  MINIO_WATCH_* env 都必須在 .env.example 有 KEY= 宣告,並斷言 TENANT_ID 為空值。

驗證:新 parity 測試先以「MINIO_WATCH_TENANT_ID 缺漏」紅 → 補欄位後綠;
config-minio-watch.test.ts 維持綠;coordinator 全套件 355 tests 全綠。

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

* task#8: fix .env.example MINIO_WATCH_ENABLED 對稱守衛(防 watcher 預設開進生產)

env-example-minio-watch-parity.test.ts 原本只對 MINIO_WATCH_TENANT_ID 有「必須空值」
守衛,對 MINIO_WATCH_ENABLED 無同等檢查。若日後有人把 .env.example 的
MINIO_WATCH_ENABLED 改成啟用值,deploy.ps1 Phase 2 missing-key merge 會把它寫進
生產 .env(若生產原本無此 key),credentials 未設時 watcher 啟動後第一輪 list 立即
打 MinIO 失敗(記 last_error 不 crash)。補一條對稱 assertion,禁止 .env.example
出現任何會被 config.ts parseBooleanEnv 視為啟用的值(true/1/yes/on,trim 後大小寫
不敏感),允許空值或明確關閉值(false/0/no/off)。

純測試新增(test-only);.env.example 未變動(現值 =false 安全)。
TDD:先暫時把 .env.example 翻 =true 看新測試以「expected [...] to not include 'true'」
失敗,再還原確認 3 測試全綠;coordinator 全套件 356 測試通過無回歸。

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

* task#8: fix parity test 補反向掃描(config.ts 動態提取 key 三方一致,指揮官修)

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

* fix: 補 ListObjectsV2 兩頁分頁測試(continuation 迴圈覆蓋)+ tenant parity 措辭如實化(P5 e2/critic,指揮官修)

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

* docs: openspec change(含 specs delta)+ spec 入庫(minio-watch-auto-intake 追溯鏈)

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

* docs(evidence): minio-watch summary.json + conv 截圖入庫(tracked 隨 PR 可審)

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

* fix: minio-watch review P1/P2 修復(intake 失敗自癒重試 + prefix normalize + 文件誠實化)

Codex review(PR #210, c1b28b1)三項發現處置:

- P1 自癒:triggerIntake 改回傳三態,tick 只在成功觸發或永久 skip 才標
  seen;暫時性失敗(presign/網路/逾時/HTTP error/2xx 非 JSON)不標 →
  下輪重試。舊行為先標 seen 再觸發,一次暫時性失敗會永久吞掉該物件,
  與「每輪全量對帳漏抓自癒」宣稱矛盾。新增 5xx 重試成功測試鎖定。
- P2 prefix:loadConfig 在 overrides 合併後 normalize 非空
  MINIO_WATCH_PREFIX 為以 '/' 結尾(空字串保持空),消除無尾斜線
  prefix 造成整批 skipped_malformed 靜默無作為。補 env/overrides 測試。
- P1 重啟 baseline(已知限制揭露,不在本 change 實作持久化):seen 與
  intake 去重索引同為 in-memory,重啟後逕行全量觸發會重複建 job,
  持久化 watermark 屬後續 change。spec delta / design doc §7 / evidence
  README 同步誠實揭露停機期間上傳不自動補觸發+補救路徑。
- Copilot 發現:evidence README 過時句修正(design spec 已隨 branch
  入庫)+ 截圖 tracked 入庫狀態如實更新。

驗證:bim-review-coordinator npm run verify 全綠(31 檔 361 測試);
openspec validate minio-watch-auto-intake --strict valid。

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

* fix: minio-watch review 第二輪修復(failed-replay 誠實化 + 兩個 misconfig fail-fast)

Codex 對 80f4306 重審的三項發現處置:

- P1 failed-replay:intake replay「不重下載不重派工」為上游 spec 既定
  不變量;首 POST 建 job 後同步下載 502、重試只會拿同一筆失敗 job 的
  200 replay,永不自癒。watcher 改判 download_status=failed 的 2xx:
  誠實記入 last_triggered(帶 job_id、不計 triggered_total)並停止
  無效重試(design §5 既審「不重送、#/conv 可見」取捨)。
- P2 allowlist:EXTERNAL_INTAKE_IP_ALLOWLIST 非空且 127.0.0.1/::1 皆
  不在名單時 watcher self-POST 必然永久 403 → 啟動 fail-fast。export
  authProvider.isIpAllowed 重用同一份判定(exact/CIDR/normalize)。
- P2 keySuffix:不以 '/' 開頭的後綴會讓全部命中物件 derive 出 trailing
  空 segment 而靜默 skip → startMinioWatcher fail-fast。不自動補 '/':
  複合後綴(_v2/model.ifc)語意合法,盲目 normalize 會改變匹配集合。

spec delta / design doc §5 §7 / evidence README 同步揭露三者語意。

驗證:npm run verify 全綠(31 檔 364 測試);openspec --strict valid。

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: monkey1sai <xshiujj@gmail.com>
monkey1sai added a commit that referenced this pull request Jun 16, 2026
* docs(spec): conv 插隊/重試控制動作 (IX-CV-03) 設計 spec

spec-to-done 起手 commit;經 plan-next-spec-to-done 推薦 + 多 agent 對抗複驗(2 BLOCKER 修正後)。

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

* plan: #conv 轉檔佇列插隊/重試控制動作(IX-CV-03)實作 plan

依 writing-plans 規格拆 9 個 task:dispatcher delete-on-success 改造(cr1 BLOCKER 1
先決回歸鎖)→ ConversionDispatchQueue prioritize/requeue → 兩條控制路由 + audit →
queue_position 上 wire(cr1 BLOCKER 2)→ 前端 jsonPost/control 方法 → IntentDialog
首個 controlled-action 共用件 → #conv 列控制鈕 → Playwright E2E(誠實可達框架)→
全量回歸 + GitNexus detect_changes。每步附完整 code block 與精確指令。

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

* plan: fix Task 0 測試改為只鎖 dispatch_failed 半段,移除跨 task retry 路由依賴

reviewer [major]:Task 0 原測試含 POST .../retry,在 Task 2 路由就緒前以 404 持續紅燈,
違反「每 task commit 後測試集全綠」可獨立 commit 原則。改為 Task 0 只斷言
「派工失敗→dispatch_failed 且 pending 保留不自動再派工」(永遠 500 stub),不引用 retry 路由;
retry 重派完整 round-trip 由 Task 2 conversion-control-routes.test.ts 兜底。
同步修正 Task 0 回歸鎖預期與 Task 2 驗證步驟敘述。

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

* task#0: fix(coordinator): dispatcher delete-on-success 保留 dispatch_failed pending 供 retry

把 pendingDispatchEvents.delete(jobId) 從 worker 取件即刪改為 markDispatched
成功後才刪,使 dispatch_failed job 保留 pending 脈絡可被 retry 重派。!pending
守門路徑與 dispose drain 行為不變。新增 integration 測試鎖「派工失敗後保留
pending、不自動再派工」半段;retry 重派 round-trip 由 Task 2 route 測試兜底。

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

* task#0: fix(coordinator): dispose 清空 pending + test 直接斷言保留(falsifiable)

修兩個 quality 發現:

Important #1(test 黑盒缺口):dispatch_failed pending 保留原本只能靠 callCount/status
間接推論,無法區分「pending 保留」與「pending 被刪但沒人再 enqueue」。CoordinatorApp
新增 test-only accessor `pendingDispatchEvents`,測試改為直接 `app.pendingDispatchEvents
.has(jobId)` 斷言,具 falsifiability(回到舊 buggy 刪 pending 行為會 fail)。

Important #2(dispose 殘留洩漏路徑):drain 只回收 queued job,dispatch_failed 的
pending 不在 drain 範圍,Task 2 retry route 未實作前無人清。dispose 收尾補
`pendingDispatchEvents.clear()`(process lifecycle 結束全清最安全),並加測試鎖
dispose 後 pending map size=0。

驗證:npm run build 綠;npm test 33 files / 379 tests 全綠;兩個 fix 各自做過
falsify-check(暫時還原 buggy 行為確認新斷言會 fail)。

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

* task#0: fix(coordinator): pendingDispatchEvents 收斂為 ReadonlyMap + 揭露 in-flight dispose race

IMPORTANT #1:CoordinatorApp public interface 的 pendingDispatchEvents 由
Map<string, unknown> 收斂為 ReadonlyMap<string, unknown>。回傳的仍是同一個內部
Map 實例(test-only accessor 的 .has/.size 不受影響),但 import CoordinatorApp 的
消費端(Task 2 retry route / 未來 plugin)在型別層只能讀,不能 .delete()/.clear()
繞過 dispatcher closure 的 delete-on-success 清理一致性與 dispose 全清保護。補
@internal 註解標示所有 mutation 只能從本檔內部 closure 與 dispose 走。

IMPORTANT #2:在 dispose() 的 pendingDispatchEvents.clear() 旁補 KNOWN RISK 註解,
揭露 in-flight-during-dispose 競態:drain() 不等已 shift 的 in-flight job、dispose
也未 await closure settle;SIGTERM graceful shutdown 在 closure 執行中觸發時,clear()
先刪 in-flight pending 但 closure 仍會跑完 markDispatched/markDispatchFailed(對已
clear map 的 .delete 無害 no-op,但 store 仍有寫入副作用)。現有測試不覆蓋此競態,
徹底消除需 Task 2 加 in-flight tracking + dispose await settle(同 minioWatcher 模式)。

GitNexus impact(upstream CoordinatorApp)=LOW,唯一 d=1 消費端 src/index.ts 解構
{server,io,config,structLog,dispose} 不取 pendingDispatchEvents,production 路徑不受影響。
驗證:tsc --noEmit PASS、npm run build PASS、vitest 全量 33 files / 379 tests PASS。

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

* task#0: fix(coordinator): pending accessor 收成 hasPendingDispatch getter + dispose 冪等

quality review 兩項 Important 收斂(指揮官手動修):
- pendingDispatchEvents 從公開 CoordinatorApp interface 移除,改暴露 hasPendingDispatch(jobId):boolean getter;
  retry route 在同一 closure 內直接讀 map,公開介面只承諾 boolean,unknown payload 不外洩。
- dispose() 加冪等守門:測試 explicit dispose() + afterEach 二次 dispose 不重跑 drain/clear/markDropped。
驗證:tsc build 乾淨 + coordinator 33 檔/379 tests 全綠。

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

* task#1: feat(coordinator): ConversionDispatchQueue 補 prioritize/requeue method

新增兩個 additive public method:
- prioritize(jobId): 把 queued job 移到隊首(插隊),回 true/false
- requeue(jobId): 重新 enqueue 並回新的 1-based queue position

新增 4 個 unit 測試全綠;既有 13 個測試零退化。

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

* task#1: fix requeue 去重防線 + hasPendingDispatch 標 @internal

Important 1:requeue 對已在 queue/in-flight 的 jobId 加 idempotency guard,
直接回現有 position 不重複 append。避免 retry 被重複觸發(雙擊/競態的兩個
HTTP 請求)時 worker 對 downstream streaming server 重複 dispatch。去重防線
在 method 內自足,不依賴尚未實作的 Task 2 route state guard;JSDoc 揭露此語意。

Important 2:CoordinatorApp.hasPendingDispatch getter 的 JSDoc 補 @internal
test-only tag,讓 API extractor/consumer 在型別層看見 test-only 合約,杜絕
production route 誤把它當依賴。

驗證:tsc --noEmit 綠;vitest 全 384 tests 綠(含新增 idempotent no-op 測試,
先 RED「expected ['A','B','C','B'] to equal ['A','B','C']」後 GREEN)。

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

* task#1: fix requeue in-flight 不洩漏哨兵 0(回 null)+ 補 prioritize/requeue in-flight 測試

Important 1:requeue() 對 in-flight job 原本透過 getQueuePosition 回 0,違反
spec §4.2「不用 0 哨兵」鐵律——retry route 若把 0 傳給 markQueuedForConversion,
下游讀者會誤判該 job 為 in-flight 而非排隊中。改為 in-flight 時回 null;queued
idempotent no-op 仍回 1-based position;return 型別 number → number|null。
此 method 目前無 production caller(retry route 屬 task#2 未實作),build 型檢通過。

Important 2/3:補兩個用 gate dispatcher 讓 job 真 in-flight 的 falsifiable 測試——
prioritize(in-flight)→false 且隊列不變;requeue(in-flight)→null 且不重複 append。
固定 spec §6.1 行為意圖,防日後改 early-return 邏輯無聲退化。

驗證:conversion-dispatch-queue 16 tests 綠;coordinator npm run build 型檢通過;
full vitest 386 tests 綠零退化。

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

* task#1: fix(coordinator): requeue 單一守門消除 fragile dual-guard + 刪測試多餘 setTimeout

quality review 兩項 Important 收斂(指揮官手動修):
- requeue 改單一 getQueuePosition 守門(pos===0 → null),不疊第二道 inFlightJobId 檢查,
  徹底消除『防線單獨被移除時 0 哨兵洩漏』的 fragile dual-guard。
- 刪 conversion-dispatch-queue.test.ts 裡裸 setTimeout(100):waitFor 已同步到 dispatch_failed,
  catch(markDispatchFailed+保留 pending)與 store 寫入同 microtask,store=dispatch_failed ⇒ pending 必在。
驗證:tsc build 乾淨 + dispatch queue 16 tests 全綠。

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

* task#2: feat(coordinator): 新增 conversion prioritize/retry 控制路由 + audit

新增 isSafeIfcReadyJobId safe-id 別名與兩條協調器自有 dispatch 佇列控制路由:
- POST /api/conversion/jobs/:id/prioritize(插隊 + 重算 queued position + audit log)
- POST /api/conversion/jobs/:id/retry(dispatch_failed/dropped_on_restart requeue + audit log)

只動協調器 in-memory FIFO,不碰 bim-streaming-server。成功一律寫結構化 audit log。
補上 conversion-control-routes 測試(prioritize 4 案 + retry 2 案),含 retry 重派完整 round-trip
(500-stub → dispatch_failed → retry → queued → 再被 worker 取件成功),兜底 Task 0 未涵蓋的半段。

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

* task#2: fix coordinator controlled action audit reason + 列表 queue_position wire + 補測試覆蓋

- summarizeIfcReadyJob additive 加 queue_position: job.queue_position ?? null
  (cr1 BLOCKER 2):列表端點上 wire,否則 #conv 經列表取件時 position 永遠
  undefined、插隊鈕 disabled 條件靜默失效。external-ifc-ready.test.ts 加形狀回歸鎖。
- AuditData 加 optional reason 欄;prioritize/retry 兩條 audit 呼叫把 reason 傳入
  data(模式 3 ③ 審計完整性);先前 reason 只回 HTTP body、未進 audit trail。
- conversion-control-routes.test.ts 補:dropped_on_restart(dispose 後脈絡確失)→
  retry→422、reason 寫入 audit log 兩個 case;reason-audit 已親驗 RED(無 reason
  傳入時 expected undefined)。

驗證:coordinator npm run verify 全綠(build + 394 tests / 34 files)。
detect_changes(staged) 因 linked worktree 已知坑回 No changes,改 git diff
--name-only --cached 自查 scope=4 檔(app.ts/structLog.ts + 2 測試),乾淨。

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

* task#2: fix 控制路由加 IP 守門 + retry requeue null 顯式 409

IMPORTANT 1:/api/conversion/jobs/:id/prioritize 與 /retry 兩條 mutation
路由原本無任何驗證,同 LAN / CORS origin 可匿名寫入協調器 control-plane。
新增 rejectIfIpNotAllowed 守門,沿用 /api/external/ifc-ready 既有模式
(isIpAllowed + config.externalIntakeIpAllowlist),非 allowlist IP 回 403,
且在 id/state 檢查之前先擋。

IMPORTANT 2:retry route 的 pos ?? 1 fallback 會在 requeue 回 null
(jobId 正 in-flight 的競態)時靜默把 store position 寫成 1,但 HTTP body
queue_position 仍是 null,store/response 不一致且前端「插隊鈕 disabled」判定
可能誤判。改為 pos === null 時回明確 409,移除靜默 fallback。

IMPORTANT 3:未動 afterEach 清理順序——dispose() 內無任何 Socket.IO emit
(只 cancel poller / drain queue / 更新 store),reviewer 假設的 emit-after-close
flaky 在此 codebase 結構性不存在;且現有順序與被引為基準的
conversion-dispatch-queue.test.ts 完全一致,依 YAGNI 不改。

驗證:npm run build 通過;conversion-control-routes 11 tests(8 原 + 3 新
IP 守門)綠;coordinator 全套 34 files / 397 tests 綠。新增 IP 守門測試先
RED(expected 404 to be 403)再 GREEN。

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

* task#2: fix 協調器控制路由 actor 截斷與空 allowlist 守門對稱

IMPORTANT 1:resolveActor 對 X-Operator / X-Actor 標頭加 .slice(0, 200)
budget 上限,與 parseReason 的 .slice(0, 500) 對稱,避免超大 header 撐爆
audit JSONL(每筆 audit record 的 actor 欄位膨脹)。

IMPORTANT 2:rejectIfIpNotAllowed 加 `length > 0` guard,與
IntranetDevAuthProvider.authenticate 的 `length > 0 && !isIpAllowed(...)`
語意對稱。空 allowlist 代表「未啟用 IP 守門」→ bypass 全部放行,而非
`![].some()` = true 造成全 403,消除兩條路由對空 allowlist 的相反語意。

TDD:先補 3 個失敗測試(actor 截斷、prioritize/retry 空 allowlist bypass)
→ 親眼看到 403/8000 fail → 最小實作 → 14 tests 綠;npm run verify
(build + 400 tests)全綠。

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

* task#2: fix(coordinator): requeue 回誠實 position(0=in-flight) 對齊 intake 表示法

quality review 抓到真問題:requeue 在『enqueue 後 worker 閒置同步取為 in-flight』時回 0,
但實作者另寫 null→409 分支 + 測試斷言 null → 內部不一致。

修法(與既有 intake 流程一致,非 reviewer 建議的 null→409——後者會弄壞整合測試):
- requeue 回 getQueuePosition 語義 position(0=in-flight 派工中、≥1=queued),冪等不重複 append,
  型別收斂為 number。position 0 + queued_for_conversion 是 intake(app.ts:846-854)既有合法
  『派工中』表示法,非矛盾;前端插隊鈕 queue_position<=1 disabled 對 in-flight 正確。
- retry route 移除錯誤的 pos===null→409 分支(worker 閒置時 retry 立即重派是成功非 409;
  且 enqueue 已執行,回 409 會造成 store/response 不一致),直接 markQueuedForConversion(id,pos)。
- 更新 unit test:requeue in-flight 斷言回 0(不是 null)、不重複 append。
驗證:tsc + coordinator 34 檔/400 tests 全綠(含 retry 整合測試 status===200 續綠)。

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

* task#3: feat(coordinator): summarizeIfcReadyJob 補 queue_position 上 wire + 前端型別

- toMatchObject 加 queue_position: null(additive,已派工後為 null)
- coordinatorClient.ts IfcReadyListItem 補 queue_position?: number | null
- app.ts 已有 queue_position wire(2108 行,resume 確認)
- 24 個 external-ifc-ready 測試全綠

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

* task#3: fix(coordinator): summarizeIfcReadyJob 的 queue_position 依規格移到 conversion_authority 之後

- finding 3:queue_position 原置於 status 之後(2108 行),規格要求 additive 加於
  conversion_authority 之後;改為 conversion_authority → queue_position → dispatch_error
  鍵序,對齊規格表述(JSON 鍵序不影響行為,24/24 external-ifc-ready 測試維持綠)。
- 此 commit 使 task#3 真正觸及 app.ts(finding 1 要求 task#3 git add 須含 app.ts)。

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

* task#3: fix queue_position 型別收緊 + queued 正值列表回歸鎖

Important #1:IfcReadyListItem.queue_position 改為 non-optional
(number | null)。summarizeIfcReadyJob 永遠輸出此欄(job.queue_position
?? null),故前端型別不應允許 undefined;強制 task#4+ 消費方只處理
number | null,與 spec §4.3 的 null 守門語意對齊。連動補齊 4 個
fixture literal(ConversionSchedulingPage / console / IntakeSelectPage
test)的 queue_position: null required key。

Important #2:列表端點補 queued_for_conversion 正值 queue_position
回歸鎖。新增 blocking streaming stub(對首個 POST 永不回應)讓 serial
worker 卡住 in-flight,後續 job 停在 queued_for_conversion;斷言列表
端點對 queued job 回 queue_position=1、對 in-flight job 回 0,鎖住
wire 完整三段語意(null / 0 / ≥1)。先以「dispatched 才上 wire」破壞
驗證 RED(expected null to be 1)再還原。

驗證:bim-review-coordinator npm test 401 passed(+1);npm run build
(tsc)EXIT=0;web-viewer-sample tsc --noEmit 無 queue_position 型別錯誤
(殘留 indexHtml.test.ts node 型別錯誤為 standalone tsc 既有環境噪音、
非本次改動)。

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

* task#3: fix 標註 in-flight queue_position=0 斷言依賴 runWorker 同步 shift 語意

external-ifc-ready.test.ts 第 359 行對 first job 斷言 queue_position=0。
此值依賴 conversionDispatchQueue.runWorker 在第一個 await dispatcher 之前
同步完成 shift + inFlightJobId=jobId(conversionDispatchQueue.ts:107-108)。
行為正確且非 race condition(blocking stub 確保 first job 永久 in-flight),
但若 worker 內部時序假設改變(shift 被移入 setImmediate/queueMicrotask),
此斷言會先失效。加註說明避免後人誤判為 race condition。

僅新增測試註解,無 production code 改動;25 個測試全綠。

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

* task#3: fix 修掉 quality 兩發現(hang-safe 測試 + IfcReadyListItem 補 updated_at)

[IMPORTANT #1] external-ifc-ready.test.ts:把 queued queue_position 測試的兩個
POST 202 斷言移入 try 區塊。blocking stub 持有永不回應的 in-flight keep-alive
socket,需靠 finally 的 blocking.release() 銷毀;任一斷言失敗若落在 try 之外,
release 不執行,afterEach 的 server.close() 會等不到 socket 銷毀而 hang 到 runner
timeout。移入 try 確保任何斷言失敗都走到 finally。

[IMPORTANT #2] coordinatorClient.ts:IfcReadyListItem 補 updated_at: string。
summarizeIfcReadyJob(app.ts:2133) 永遠輸出 updated_at,spec §2.4 訂為前端可見
證據;比照 queue_position 收緊先例補齊,避免 task#4 prioritize/retry 後 load()
重抓讀 item.updated_at 時 TS 報型別錯誤。同步補齊 4 個既有 fixture literal
(console/ConversionScheduling/IntakeSelect test)的新 required key。

驗證:bim-review-coordinator external-ifc-ready 25/25 綠;frontend 受影響三套
(console/ConversionScheduling/IntakeSelect)81/81 綠;tsc --noEmit 無新增錯誤
(既有 3 個 indexHtml.test.ts node 型別噪音為 pre-existing,與本改動無關)。

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

* task#3: docs(spec): §2.3/§4.3 queue_position 對齊實作 non-optional

quality review 唯一 Important(非 code bug):spec §4.3 寫 queue_position? optional,但實作建模為
non-optional(summarizeIfcReadyJob always-emit、消費端只處理 number|null 不含 undefined,避免
disabled 判斷遇 undefined falsy 誤判)。實作更正確 → spec 文字對齊(移除 ?),防後續 agent 回歸。

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

* task#4: feat(viewer): coordinatorClient 補 jsonPost + conversionPrioritize/Retry

新增 jsonPost helper 與 ConversionControlResponse 型別;
在 coordinatorClient 物件加 conversionPrioritize / conversionRetry 兩個 POST 方法。
新增 coordinatorClient.test.ts 覆蓋 prioritize 打對路徑/method/body、非 2xx throw 兩案;全套 vitest 251 案綠。

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

* task#5: feat(viewer): 新增 IntentDialog 首個 controlled-action 共用件

模式 3(intent→confirm)非樂觀 modal;uncontrolled textarea ref 讀值
確保測試環境 DOM 直接設值後 onConfirm 收到正確 reason 字串。
2 tests green,全套 253 tests pass。

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

* task#5: fix IntentDialog modal 缺漏 CSS(補 .ec-modal* / .ec-field-k / .ec-input)

IntentDialog.tsx 引用的 5 個 class(ec-modal-backdrop / ec-modal /
ec-modal-actions / ec-field-k / ec-input)在所有 .css 皆無定義,
browser E2E 下無 backdrop、不置中、不浮層,視覺驗收會失敗。

在 edge-console.css 補上對應規則(沿用既有 --ec-* token:--ec-bg-2 /
--ec-line-2 / --ec-fg-3 等),backdrop 用 position:fixed + z-index:1000
蓋過 .ec-root 固定 grid。新增 IntentDialog.css.test.ts 以靜態 selector
存在性把關(jsdom 無法計算 stylesheet layout)。

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

* task#5: fix IntentDialog rejection 安全網 + 測試 root unmount

[Important #1] click handler 改為 async + try/catch await onConfirm,
防 caller 漏 catch 時 rejection 逸散為 unhandledrejection(規格 §5/§4.6:
失敗反饋仍由 caller 在 onConfirm 內 setErr+解除 busy;component 只防逸散)。
prop 加 JSDoc 明確 caller 須自行 catch 的責任契約。

[Important #2] 測試 root 提升至 describe 作用域,afterEach 補
await act(unmount),比照 console.test.tsx 模式,消除跨 test DOM 污染脆性。

新增 RED→GREEN 測試:onConfirm reject 不逸散為 unhandledrejection。
驗證:vitest src/console 218 passed;npm run build 通過(type check clean)。

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

* task#5: fix IntentDialog dialog accessible name 與 busy 鎖回歸測試

- IntentDialog 的 role=dialog 補 aria-labelledby 指向 title h3(WCAG 2.1 SC 4.1.2,screen reader 進對話框可知是什麼)。
- 新增 busy=true 測試:確認/取消/textarea 皆 disabled、按鈕文字切「執行中…」、busy 下點擊不觸發 onConfirm(防重打回歸保護)。
- 新增 aria-labelledby 測試先 RED(attribute 缺→null)再實作轉 GREEN;busy 行為既有故只補回歸測試。

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

* task#6: feat(viewer): #conv 列插隊/重試控制鈕接 IntentDialog 真 POST

dispatch_failed/dropped_on_restart job 顯重試鈕、queued_for_conversion 顯插隊鈕,
經 IntentDialog intent→confirm 後 await 真 POST(conversionPrioritize/Retry),
成功 load() 重抓真佇列狀態(非樂觀)、失敗 setErr 誠實訊息且不關 dialog。
移除 pages.tsx 佔位 Field,改誠實標可控範圍+concurrency NOT BUILT。

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

* task#6: fix 補插隊路徑與失敗不關 dialog 的單元測試 + 建 E2E spec

對抗複驗三項 important 發現修補(純測試新增,零 production code 變更):

- #1 插隊(prioritize)路徑單元測試:新增 queued_for_conversion + queue_position=2
  fixture,驗插隊鈕出現且不 disabled → 點按開 IntentDialog → confirm →
  conversionPrioritize 被呼叫且 listIfcReady 重抓(spec §4.5/§4.6/§6.1)。
- #2 POST 失敗不關 dialog 單元測試:mock conversionRetry reject,confirm 後驗
  intent-dialog 仍在 DOM 且 ec-warn-note 含「控制動作失敗」(spec §5/§4.6)。
  mutation 驗證:catch 誤加 setPendingAction(null) 時此測試會 fail,證明有 teeth。
- #3 建 e2e/conv-prioritize-retry.spec.ts:比照 conv-coverage-report.spec.ts 的
  skip-gate 誠實可達框架,驗「按鈕 → IntentDialog → 真 POST → 列刷新」端到端切片,
  retry/prioritize 二選一依 live 佇列狀態,未觀察轉移以 notObserved 揭露(spec §6.4)。

驗證:vitest 全綠 265 tests(含 +2 新單元);eslint 兩新檔零問題;npm run build
typecheck 通過;playwright --list 解析 E2E spec OK。

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

* task#6: fix #conv 控制動作重入與錯誤狀態覆蓋兩處 quality 發現

finding #1(重入):runAction 加同步 busy ref guard(actionBusyRef)。
setActionBusy(true) 是非同步 state,confirm 鈕 disabled={busy} 要等下一次 render
才生效;同一事件循環連點兩次會送出兩個 POST(retry 路徑第二次撞 409 顯誤導性錯誤、
prioritize 路徑可能造成多餘 audit record)。ref 在 React state 更新前同步攔截第二次呼叫。

finding #2(錯誤覆蓋):action 錯誤拆成獨立 state(actionErr),透過新 optional prop
顯示在 IntentDialog 內、與 dialog 綁定,不再寫入與 load() 共用的全域 err。load() 開頭的
setErr(null) 因此不會把「控制動作失敗」清掉;操作者按 Refresh queue 時 dialog 內錯誤訊息
不再短暫消失。開新 dialog / 取消時清 actionErr。

TDD:先加兩個失敗單元測試(連點兩次只送一個 POST、Refresh 後 dialog 內錯誤仍在),
看其以預期原因失敗後最小實作;ConversionSchedulingPage/IntentDialog 全測試綠、build 通過。

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

* task#6: fix 插隊鈕 disabled 補 tooltip 與補 dropped_on_restart/隊首/in-flight 單元覆蓋

Important #1:Btn 元件加 title?: string prop(additive、未提供時不渲染屬性,
對既有 17 個呼叫者零行為變更),插隊鈕依 queue_position 給誠實 tooltip:
queue_position<=1(已隊首)或 0(in-flight)→ disabled 並說明為何不可插隊(spec §4.6)。
補 queue_position=1(隊首)/=0(in-flight)的 disabled+title 單元測試。

Important #2:補 dropped_on_restart fixture 測試「顯重試鈕→確認→conversionRetry」,
鎖住 pages.tsx 該分支(此前控制動作測試僅覆蓋 dispatch_failed)。

驗證:web-viewer-sample vitest 全綠 270 passed(含本檔 23)、npm run build 通過。
GitNexus impact(Btn, upstream)=HIGH(17 直接呼叫者),但本變更為 additive optional prop,
title=undefined 時與現行行為一致,向後相容、不破壞任何呼叫者。

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

* task#6: fix(web-viewer): runAction 重抓佇列失敗時保持 dialog 開啟 + 顯誠實錯誤

quality review #1(真行為缺口):load() 自吞錯不 throw,runAction 原本 await load() 後一律
setPendingAction(null) 關 dialog——『POST 成功但重抓佇列失敗』時 dialog 靜默關閉、佇列顯舊狀態、
背景 err 操作者不易察覺。
修法:load() 回傳 boolean(jobs 是否抓成功);runAction 重抓失敗時 setActionErr + 保持 dialog 開啟
不視為完成(後端動作冪等,訊息提示重按確認不會重複生效)。補測試覆蓋此分支。
(#2 reviewer 已自確認 requeue/queue_position=0 行為正確;#3 resolveActor best-effort 由 spec §3.7
授權,PR body 將揭露 actor 非身分稽核。)
驗證:web-viewer build 乾淨 + ConversionSchedulingPage 24 tests 全綠。

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

* task#7: test(e2e): #conv 插隊/重試 controlled action 端到端 + evidence

Browser E2E spec(web-viewer-sample/e2e/conv-prioritize-retry.spec.ts,task#6 已建)
驗 vertical slice:#conv route → Refresh queue → 狀態控制鈕 → IntentDialog →
真 coordinator POST /api/conversion/jobs/:id/{retry,prioritize} → 列依真狀態刷新。
本輪對 branch coordinator :8005 執行(CORS :5180 OK),佇列無可控制 job → honest
conditional skip(計 pass,EXIT=0);深度因果由 conversion-control-routes.test.ts
(14 passed)兜底。evidence 抽樣落 docs/evidence/conv-prioritize-retry/。

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

* task#7: fix #conv 插隊/重試 E2E spec 對齊規格範本(IX-CV-03 名稱 + afterAll 揭露)

- test.describe 改 "IX-CV-03 #conv 插隊/重試 controlled action"(原 M2-b 偏離範本)
- 單一測試名改 "控制鈕 → IntentDialog → 真 POST → 列依真狀態刷新"
- notObserved 揭露由 test body annotations 改回 test.afterAll console(skip 下 test body 不執行,唯 afterAll 仍跑才不漏記);beforeEach skip 路徑補 push 揭露
- 檔頭註解開頭對齊 IX-CV-03
- gap#1:本 task#7 fix commit 一併納入 spec 檔(原 ddb608e 只含 evidence、spec 已於 1b812b3 committed)
- 重跑驗證仍 1 skipped EXIT=0,afterAll 確印 notObserved;playwright-run.txt/summary.md 同步刷新

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

* task#7: fix playwright.config webServer 注入 VITE_COORDINATOR_API_BASE 對齊 :8005 真切片

spec 檔頭聲稱 coordinator base 由 VITE_COORDINATOR_API_BASE 注入(預設 :8005),但
playwright.config.ts webServer command 原本無 env 欄位,Vite 不會把該 VITE_ 變數帶進 dev
server 進程 → env.ts fallback :8004,browser coordinatorClient 的 POST 打不到 branch
coordinator :8005,前置齊全時 page.waitForResponse 攔不到真 POST、vertical slice 無法命中。
webServer 加 env: { VITE_COORDINATOR_API_BASE: E2E_COORDINATOR_BASE_URL || :8005 } 使 viewer
build-time coordinator base 與 test 端 COORDINATOR 常數同源;同時修好共用此 config 的
conv-coverage-report.spec.ts。本輪仍 honest skip(佇列無可控制 job,種 dispatch_failed 需重啟
:8005 並改其 .env 指向 500-stub,屬 secrets/外部行程邊界不偽造);fix 移除結構性阻礙不偽綠。

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

* task#7: fix 補 #conv 插隊/重試 E2E tracked render-surface 截圖證據 + spec 同批

task#7 commit step 要求 spec + evidence 同批 commit,且 evidence dir 需含截圖(比照 peer
conv-coverage-report / a1-m1-closeout / a2 evidence dir)。前兩輪 controlled-action slice 皆
honest skip(佇列無可控制 job、test body 未執行)→ evidence dir 缺 .png。

本 commit 把 spec 檔與 evidence 同批納入,並在 spec 內新增獨立 render-surface 證據 test
(不受 slice beforeEach 守門):無條件渲染 #conv 真頁面 → Refresh queue 載 :8005 真佇列 → 截圖
conv-render-surface.png(PASS)。誠實鐵律:此截圖只證明 #conv 真頁面渲染 + 截圖路徑機制可落點,
不等於觀察到 controlled action;controlled-action slice 仍 honest skip、不被偽綠,深度因果由
conversion-control-routes.test.ts(14 passed)兜底。

驗證:E2E 1 skipped + 1 passed(render-surface),EXIT=0。

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

* P4: browser evidence — controlled-action slice 真綠(retry 路徑)

指揮官親自編排可控 seeded stack 取得 task#7 spec-review 要的切片真綠:
- 可控 streaming stub(500 then hang)製造可重現 dispatch_failed → retry 因果
- fresh branch coordinator :8005 指向 stub + 種真 ifc-ready job → dispatch_failed
- Playwright slice: conv-retry → IntentDialog → 真 POST /retry 2xx → queued_for_conversion → 列刷新 = PASS(1.2s)
- engine=Playwright(chromium), mock=false(真 coordinator+真 POST+真佇列轉移;stub 僅模擬下游 authority 行為)
- 截圖 conv-prioritize-retry-retry-slice.png(tracked) + summary 誠實揭露 seed 方法與 prioritize 路徑 notObserved
解決 spec_review_not_closing 的核心 gap(slice 從未綠);prioritize 深度因果由 route 測試 14 passed 兜底。

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

* openspec: conv-prioritize-retry change (proposal + tasks)

P6 前置:diff 觸及 coordinator/web-viewer → pr-review-agent 需 active openspec change(防 missing_openspec blocker)。對應 IX-CV-03 #conv 插隊/重試控制動作。

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

* openspec: 補 conv-prioritize-retry spec delta(specs/.../spec.md)

pr-review-agent CI 跑真 openspec validate 抓到 high blocker:change 缺 spec delta(只有 proposal+tasks)。
補 ## ADDED Requirements + #### Scenario(控制路由/dispatcher 保留脈絡/前端模式3非樂觀),本機 openspec validate
+ --strict 皆通過。

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request Jun 23, 2026
…取證) (#238)

* docs(spec): VG-01 A1 工作台嵌入 live viewer + first_frame_at 後端化(M3 第一格)

含 M3「3D viewer 包覆 A1~A3」藍圖與 A1/A2/A3 各自 3D 顯示決策(§0.1)。
經 ultracode 多代理對抗驗證(32 agents / ~1.98M tok)修訂:5 項 must-fix 全修、
file:line 全重新查證。第一格 = 地基(iframe+postMessage 橋 + first_frame_at 後端化
+ stage truth)+ A1 失敗構件 3D 高亮。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgqvMSVWmnwEDaPqDAgXxg

* plan: VG-01 A1 工作台嵌入 live viewer + first_frame_at 後端化 實作計畫

依 superpowers writing-plans 規格,把 spec 2026-06-22-viewer-embed-a1-highlight-design
拆成 6 個 bite-sized task(Task 0-5),每步含失敗測試→確認失敗→最小實作→確認通過→commit,
附完整 code block 與精確指令。所有 file:line 經 GitNexus 導航 + Read 重新查證。

- Task 0:coordinator first_frame_at 後端化(新 route + 型別鏈,回歸鎖,先做)
- Task 1:EmbeddedViewer 元件 + vg01 postMessage 橋(console 地基)
- Task 2:Window.tsx parent listener(M1 first_frame 單送閂 / M2 canOperate 守衛)
- Task 3:A1 頁嵌 viewer + 把 pages.tsx:347 disabled 高亮翻真(IX-A1-06 四條件)
- Task 4:證據顯示翻真 + runtimeGovernance 讀真值(型別鏈收尾)
- Task 5:Playwright browser E2E(誠實 skip 揭露 + cross-build-target 重建)

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

* plan: fix VG-01 補 11 項對抗發現(nowIso import/env export/store 存取/viewerOrigin 真源/IX-A1-06 四條件/S3 雙清單/RuleResultRow 欄位/listReviewSessions fallback)

逐項修進 plan:
- Task0 nowIso 未 import → 加明確 import checklist step(./utils/time.js);測試骨架裸 store → 改 app.store(CoordinatorApp field)+ makeApp() setup
- Task2 allowedCoordinatorOrigins 非 export → 加 export env helper 必做 step + 列入 Files;新增 S3 viewer 端收合 step(嵌入時 failedElements 餵空)
- Task3 viewerOrigin 由 reviewEnv.coordinatorApiBase(:8004) 改 runtimeStatus().configured_endpoints.viewer.browser_url_base(:5173 真源);IX-A1-06 四條件補齊(first_frame[含DataChannel]∧stage matched∧session∧usd_prim_path);f.label→f.message(RuleResultRow 無 label);reviewEnv 未 import 改用 viewerOrigin state;listReviewSessions bare route 不存在→改走 runtime/status.sessions.items;test mock 對齊 runtimeStatus
- EmbeddedViewerProps viewerOrigin 註解改指 viewer 入口(非 coordinator)

所有修正均經 worktree 內 grep/Read 對 code 查證;S3 為 spec §2.3 明確要求,非 spec 矛盾。

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

* task#0: feat(coordinator): first_frame_at 後端化 + runtime/status 型別鏈(回歸鎖)

新增 POST /api/review-sessions/:id/first-frame:console 回報 viewer 真畫面首幀後,
coordinator 以 nowIso() 權威時戳寫入 ReviewSession.first_frame_at(忽略 body.observed_at,N3),
冪等(已記過回原時戳、不重複 append firstFrameObserved,N2 最小一筆),
並在 summarizeSessionForRuntime 透出 first_frame_at(GET /api/runtime/status 型別鏈 M3)。

- src/types.ts:ReviewSession 加 optional first_frame_at(strictly additive,舊 session 讀回 undefined)
- src/app.ts:頂部加 import { nowIso } from "./utils/time.js"(原無此 import);
  新 route 插在 events POST 與 close 之間;summarizeSessionForRuntime emit first_frame_at ?? null
- tests/session-first-frame.test.ts:新檔,6 案(safe-id 400 / 404 / 首幀寫入+event /
  冪等 / 忽略 observed_at / runtime 型別鏈)

GitNexus impact(upstream):summarizeSessionForRuntime=0 callers、buildRuntimeStatus 僅
createCoordinatorApp,risk=LOW,皆 additive。
驗證:npm run verify(tsc 0 error + vitest 433 passed / 36 files,零退化)。
detect_changes staged 在 linked worktree 回 No changes(已知坑),改用 git diff --cached 自查 scope 乾淨。

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

* task#0: fix first-frame 補 isSessionMutable 守門 + endpoint_id 截斷

P5 對抗複驗三項 important:
- #1 first-frame 缺 isSessionMutable 守門:closed/closing session 仍能 store.update +
  append firstFrameObserved 進 append-only audit ledger(race:browser close 與 viewer
  首幀同時抵達)。在冪等檢查前補 `!isSessionMutable(session)` → 409,與 sibling mutation
  路由(append-event app.ts:866 等)一致。
- #2/#3(同根)endpoint_id 無長度截斷且直接寫入 audit JSONL:endpoint_id 來自 iframe
  postMessage 的 client 回報(LAN 無 RBAC 可偽造),與 resolveActor(.slice(0,200))/
  parseReason(.slice(0,500)) 的 budget 對齊,加 `.slice(0, 100)` 防超長字串撐爆 / 污染
  event log(log injection 同根防護)。

TDD:先加 closed→409、closing→409、超長 endpoint_id 截斷三個失敗測試(親見以預期原因
fail),最小實作後轉綠。npm run verify 全綠(build + 436 tests)。

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

* task#0: fix first-frame 路由防 store.update 回 null 的孤兒 event

store.update 在 store.get 守門通過與實際 update 之間 session 檔被外部刪除時回
ReviewSession | null。原 app.ts:907 忽略回傳值會 (1) 仍 append 一筆孤兒
firstFrameObserved(對應不到任何 store 記錄,違反 append-only audit ledger 不變式),
(2) 回 200 + 未實際持久化的時戳給呼叫端。

修正:比照 sibling /close 路由(app.ts:951-962)對 store.update null 的防禦,
update 失敗時回 500、不 append firstFrameObserved。

TDD:新增測試 mock store.update 回 null,先確認回 200 失敗(孤兒 event 被寫入),
最小實作後回 500 且不 append;coordinator npm run verify 全綠(36 files / 437 tests)。

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

* task#0: 補 quality review 兩項 Important(指揮官手動修,P3 自動迴圈 2 輪未閉合)

- eventLog.ts: firstFrameObserved 顯式登記進 STRUCTURED_LIFECYCLE_MAP(review_session/active),
  取代隱晦 fall-through;comment 引合約 §9 並指引下游用 data.eventlog_type 區分 operational
  milestone vs 真 lifecycle transition。
- docs/contracts/structured-log-schema.md §9: 同步加 firstFrameObserved mapping 列(source-of-truth 一致)。
- session-first-frame.test.ts: 刪 void realUpdate 死碼 + 誤導 comment(mockRestore 已在 finally 還原)。

驗證: bim-review-coordinator npm run verify = build + 437 tests 全綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgqvMSVWmnwEDaPqDAgXxg

* task#1: feat(console): EmbeddedViewer 元件 + vg01 postMessage 橋(地基)

新增 EmbeddedViewer(forwardRef)封裝 iframe,src 帶 ?session= query 指向
viewerOrigin(:5173 baked viewer);vg01 版本化 postMessage 橋送出 targetOrigin
非 "*"、接收端驗 origin + source + protocol 三重守衛,未知 type / 缺 protocol 忽略。
useImperativeHandle 暴露 sendHighlight / sendFocus / sendClear 供 Task 3 呼叫。

測試:5 個 it 全綠(302 個 tests 全 pass,28 個 test files 無回歸)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgqvMSVWmnwEDaPqDAgXxg

* task#1: fix EmbeddedViewer listener 穩定化 + 補送出側測試(解 Important #1/#2)

Important #1:useEffect dep [props] 每 render 重掛 listener。
高頻 poll 場景的 removeEventListener/addEventListener 微小時窗會靜默丟
first_frame / highlight_result。改用 stable-ref 模式:propsRef 每 render
同步最新 props,listener 改 dep [],只掛一次。回歸鎖測試斷言
addEventListener("message") 僅一次(舊寫法為 1+N 次)。

Important #2:刪除從未呼叫的死碼 renderViewer helper(解兩個
no-unused-vars error)。補 spec §6.2 送出側覆蓋:sendHighlight /
sendFocus / sendClear 經 ref handle 呼叫 contentWindow.postMessage,
斷言 targetOrigin === viewerOrigin(非 "*")且帶正確 type/items。

驗證:vitest 全套 305 passed;eslint 兩檔 0 error(唯一殘留
useImperativeHandle missing-dep 'post' 為 pre-existing warning,非本次引入)。
TDD:兩項新行為均先驗 RED(targetOrigin 改 "*" → 送出側測試 fail;
dep 還原 [props] → 回歸鎖 expected 4 to be 1)再 GREEN。

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

* task#1: fix EmbeddedViewer 補 type Root import + 送出側對齊 propsRef(解 Important #1/#2)

Important #1: EmbeddedViewer.test.tsx 第 19 行使用 Root 型別但未 import,
tsc --noEmit 報 TS2304: Cannot find name 'Root'。比照 IntentDialog.test.tsx
改為 `import { createRoot, type Root } from "react-dom/client"`。

Important #2: post helper 原直接 close over render-scope props.viewerOrigin,
與接收側 listener 的 propsRef.current 模式不對稱。改為讀 propsRef.current.viewerOrigin,
useImperativeHandle dep 改為 [](handle zero re-create),兩側同模式、消除後續維護者
誤加 props 欄位卻漏更新 dep array 的 stale closure 風險。

驗證:tsc --noEmit 不再報 Root(剩 6 項 pre-existing node-types 錯誤,非本 task scope);
npm run verify 全綠(build + 305/305 vitest + struct-log 10/10),含 targetOrigin===viewerOrigin 回歸鎖。

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

* task#1: 補 quality review 三項 Important(指揮官手動修,P3 自動迴圈 2 輪未閉合)

- EmbeddedViewer.test.tsx: 補 viewer_ready / stage_loaded dispatch 回歸鎖(Task 3 stage-truth 依賴 onStageLoaded)。
- EmbeddedViewer.tsx: 移除死的 setViewerReady useState(含 useState import)——父元件用 onViewerReady callback
  追蹤,元件不存內部死 state,省無謂 re-render。
- EmbeddedViewer.tsx: props JSDoc 加 viewerOrigin/sessionId「mount 後不可動態改」契約(變更會 reload iframe
  中斷 WebRTC);元件 src 隨 render 重算。
- plan Task 3: render EmbeddedViewer 加 key={selectedSession}(session 切換乾淨 remount);viewerOrigin
  已由三元 gated(null 不 render,不會空→補值 reload)。

驗證: web-viewer-sample EmbeddedViewer.test.tsx 9 tests 綠 + npm run build(vite+tsc) 綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgqvMSVWmnwEDaPqDAgXxg

* task#2: feat(viewer): Window parent postMessage listener + first_frame 觸發(M1/M2 守衛)

VG-01 §2.2/§4.3 viewer 側 postMessage 橋(嚴格 additive):
- componentDidMount 掛 message listener(unmount 對稱移除);嵌入時 postMessage viewer_ready。
- _handleParentMessage:origin 白名單(複用 env.ts allowedCoordinatorOrigins)+ document.referrer
  交叉驗 + protocol vg01 + 僅 iframe 內;收 highlight 先判 canOperate(M2,與 render 同一
  deriveOverlayInputs)→ 既有 _overlayHighlight/HighlightBridge → 回 highlight_result;
  spectator/未就緒靜默丟棄。focus→focusPrim、clear→clearHighlight、未知 type 忽略。
- _completeStageLoad(唯一真完成點)末尾 postMessage first_frame + stage_loaded;_firstFramePosted
  閂只送一次(M1,不接失敗/斷線/開檔路徑)。
- _reverseLookupGuid 設值處 additive postMessage selected_guid(七區塊第 7)。
- S3:嵌入模式 GovernanceOverlay 失敗清單餵空陣列收合(failedElementsForEmbed),加誠實提示,
  console 左側為唯一權威清單;不改 overlay props 形狀。
- env.ts allowedCoordinatorOrigins 改 export(純 additive,Window.tsx import 前置)。
- 新增純函式守衛 src/parentMessageGuard.ts + 單元測 src/console/windowParentMessage.test.ts(10 測全綠)。

驗證:windowParentMessage(10)/GovernanceOverlay(32)/console(60) 全綠;全 vitest 316 測零退化;
npm run build 0 error;四個改動檔 eslint --max-warnings 0 乾淨(repo-wide pre-existing 44 errors 非本格)。
GitNexus impact:_overlayHighlight LOW(未改其簽名)、_completeStageLoad HIGH(17 impacted 皆 Window.tsx 內,
本格僅 tail-append 不破既有呼叫面)。

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

* task#2: fix 補 Window parent postMessage 整合 / render 層測試(S3 render + M2 整合 + M5 degraded)

補三項 spec-compliance gap 的缺漏測試(production 行為前次 run 已實作且綠,本次只補
未覆蓋的整合 / render 面,並逐項 teeth-check 確認測試能抓 regression):

1. S3 render(gap#1):實際 render 真 App,鎖嵌入時 GovernanceOverlay 收空 failedElements
   + 顯示 viewer-embedded-list-collapsed 提示;非嵌入照舊列出失敗列。原僅測純函式
   failedElementsForEmbed,無 render 層斷言。
2. M2 整合(gap#2):真跑 _handleParentMessage,鎖 shouldAcceptParentMessage →
   deriveOverlayInputs → canHandleHighlight 串接:canOperate=false 時 highlight 靜默丟棄
   (不呼 _overlayHighlight、不回 highlight_result)、canOperate=true 才走既有路徑、
   origin 不符整則丟棄。原僅測純函式 canHandleHighlight(false),整合鏈無覆蓋。
3. M5 degraded(gap#3):鎖 document.referrer 為空時 _postToParent / viewer_ready 安全降級
   (不送、不崩潰、不對 "*" 廣播)。此為 spec §M5 明文接受的已知風險(不新增 env var /
   不新增 origin 注入機制),故補測鎖降級行為而非加 spec 禁止的 fallback。

新增 web-viewer-sample/src/console/windowParentMessage.dom.test.tsx(7 測);
Window.tsx 等 production 檔零改動(teeth-check 後完整還原)。

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

* task#2: fix viewer parent postMessage 守衛與 first_frame 重載 + 空白名單診斷

Important #1:_handleParentMessage 的 focus / clear 也套 canOperate /
spectator 守衛(spec §2.2 全部 mutating handler 共同要求,非僅 highlight)。
把 deriveOverlayInputs 上提到 switch 前一次算 canOperate,三個 case 共用;
spectator 或未就緒一律靜默丟棄,不送 focusPrimRequest / clearHighlightRequest
(誠實鐵律:spectator 不送 mutating)。

Important #2:_finishStageLoad 同時歸零 _firstFramePosted 閂。否則同一
session 內換載 stage(多模型切換)時第二個 stage 完成後 parent 收不到
first_frame / stage_loaded,IX-A1-06 無法重滿足、高亮鈕保持 disabled。

Important #3:(1) _handleParentMessage 一次 parse allowedCoordinatorOrigins
供守衛與後續共用(同 call stack env 不變,省重複 new Set);(2) _postToParent
在白名單為空(deploy 忘設 VITE_ALLOWED_COORDINATOR_ORIGINS)時留一次性
console.warn 診斷,不再半靜默失敗(安全行為不變:仍不對未授權 origin 送)。

驗證:web-viewer-sample vitest 全綠 330(新增 7 dom 測,先 RED 5 後 GREEN);
vite build 通過;eslint Window.tsx + 測試檔 0 warning。

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

* task#2: fix viewer postMessage 補 payload 形狀守衛 + 白名單複用 + referrer 交叉驗測試

解 quality review 三項 Important:
- #1(測試缺口):補 referrer 存在但 origin 不符的第二層交叉驗失敗路徑測試
  (event.origin 在白名單但 document.referrer 指向 other.example)→ 整則丟棄、不崩潰。
  既有 line-682 交叉驗已處理,本次只補 §M5 已知 trade-off 的測試覆蓋。
- #2(守衛缺口):_handleParentMessage highlight 分支加執行期 payload 驗證。
  跨 origin postMessage 反序列化不可信,items 非陣列直接丟棄;每筆經 isHighlightItem
  守衛(須物件且 ifc_guid 為字串),非法 item(null/數字/缺欄位)跳過,不餵進 _overlayHighlight。
- #3(效能):_postToParent 加可選 allowedOriginsCache 參數;highlight 迴圈複用
  _handleParentMessage 開頭已建的白名單 Set,避免每筆 highlight_result 重 parse env / new Set。

嚴格 additive,不改既有 reject / 白名單 / spectator 行為。
驗證:vitest 全綠(335/335;parent-message 29/29,新增 5 筆)、vite build 通過。

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

* task#2: 補 quality review 兩項 Important(指揮官手動修,P3 自動迴圈 2 輪未閉合)

- Window.tsx focus case: 補 typeof m.ifc_guid === "string" 守衛,對齊 highlight 的 isHighlightItem
  嚴格守衛(postMessage 跨 origin 反序列化執行期不保證型別)。
- Window.tsx _postToParent: early return 先 window.parent===window 再 _consoleParentOrigin(),
  standalone 模式免每次 3D 點選無謂 parse document.referrer。
- windowParentMessage.dom.test.tsx: 補 selected_guid 送出測試(嵌入送出含 ifcGuid / standalone 不送),
  VG-01 七區塊第7「3D 點構件→清單反查」回歸鎖。

驗證: web-viewer-sample windowParentMessage 2 files 31 tests 綠 + npm run build(vite+tsc) 綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgqvMSVWmnwEDaPqDAgXxg

* task#3: feat(console): A1 頁嵌入 EmbeddedViewer + 3D 高亮接線(IX-A1-06 四條件)

- pages.tsx A1GovernanceWorkbenchPage:取代 line 347 永久 disabled「在 3D 高亮」占位鈕,
  改為依 IX-A1-06 四條件 enable 的真按鈕(first_frame ∧ 有選 session ∧ stage matched ∧
  構件有 usd_prim_path),每條 disabled 都有誠實原因文案。
- 新增「3D 即時檢視」Panel:嵌 <EmbeddedViewer>(左失敗清單記分板 / 右 3D iframe)+ active
  session 下拉 + first frame / stage matched 真證據欄。session 與 viewerOrigin 同走一次
  runtimeStatus()(已查證無 bare GET /api/review-sessions,spec §1.3 誤判;改 sessions.items)。
- viewerOrigin 真源 = configured_endpoints.viewer.browser_url_base(viewer :5173 baked,非
  coordinator :8004);null 時誠實顯「viewer 入口未取得」不掛空 iframe。S3:console 左側
  state.failed 為唯一權威失敗清單,iframe 不另顯第二份。
- coordinatorClient.ts:加 listReviewSessions(讀 runtime/status.sessions.items)+ reportFirstFrame
  (轉發 viewer first_frame → coordinator);RuntimeSessionSummary 補 first_frame_at?(task#0 後端化)。
- A1ViewerEmbed.test.tsx:6 個 createRoot+act+vi.spyOn 測試(無 @testing-library/react,循 repo 慣例;
  EmbeddedViewer 以 forwardRef stub 捕捉 props)。

驗證:A1ViewerEmbed 6 綠;console/overlay/EmbeddedViewer/coordinatorClient 回歸 114 綠;build 0 error;
touched 3 檔 lint clean(其餘 lint error 為 harness/e2e 既有 baseline,非本 task 引入)。

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

* task#3: fix onFirstFrame 保留 stageUrl 閉合 stage-match + 補四條件迴歸測試

Important #1:onFirstFrame 原本丟棄 FirstFrameMessage.stageUrl,僅靠獨立
onStageLoaded 設 loadedStageUrl。viewer 的 _completeStageLoad 在同一流程先送
first_frame(含當下已載 stageUrl)再送 stage_loaded;若 first_frame 先到或順序
不同,loadedStageUrl 恆 null → stageMatched 恆 false → IX-A1-06 第三條件結構性
封鎖、「在 3D 高亮」鈕永遠無法 enable。改為接收 m 並在 m.stageUrl 非 null 時
setLoadedStageUrl(m.stageUrl),與 onStageLoaded 並存(互補不互斥)。

Important #2:補 stage-match 閉合與四條件 button-enable 的迴歸守護。
- 為 stage matched Field 加 data-testid="a1-stage-matched"(鏡像 first-frame 寫法)。
- 新測:onFirstFrame 帶 stageUrl(==expected) → stage matched 值轉
  「matched(expected == loaded)」(斷言值而非標籤,避免標籤含 "matched" 偽通過)。
- 新測:透過真 doRun 流程餵 ifc_guid+usd_prim_path 皆非 null 的 failed 構件,再以
  onFirstFrame(stageUrl==expected) 閉合 → 四條件全滿 → 高亮鈕由 disabled 翻 enable。
- augment:first_frame 未到時 stage matched 須誠實顯「not_observed(尚未載入)」。

驗證:web-viewer-sample vitest 345/345 綠(A1ViewerEmbed 8/8,含 2 新測先 RED 後 GREEN);
npm run build EXIT=0(tsc + vite)。

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

* task#3+#4: 對抗驗證閉合(指揮官手動修;task quality-fix r2 被 API 529 中斷)

ultracode 對抗驗證(18 agents,5視角+裁決,實機跑測試)裁定 task#3/#4 unit 層閉合並修必修:
- P0 console.test.tsx(A1 describe beforeEach): 加 runtimeStatus mock(test-only,零 production 改)。
  回歸根因=A1 頁 mount 打真 runtimeStatus(),coordinator 存活→真 session→未 mock mapping fetch
  在 fake-timer 不 settle→RUN_DONE 不 dispatch→建 Issue 鈕恆 disabled(非 production bug)。
- P1 pages.tsx ViewerPresentationPage: Stage truth + capabilities first_frame_at/stage matched
  三處 prov asbuilt→p1(靜態/any-session,未到 asbuilt;動態 firstFrameEvidenceText 保 asbuilt)。
- 含 task#3 A1 接線(EmbeddedViewer gated render+key+IX-A1-06 四條件)、task#4 證據顯示 +
  runtimeGovernance 讀真 first_frame_at + coordinatorClient reportFirstFrame/型別鏈。

驗證: build(tsc+vite)綠 + vitest 31 files/349 tests 全綠。對抗驗證推翻內部2矛盾、過程事故
(agent 誤 checkout pages.tsx)已逐 hunk 還原並驗 baseline 一致。task#5 E2E + P2 follow-up 另接。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgqvMSVWmnwEDaPqDAgXxg

* task#5: A1 viewer-embed Browser E2E(Playwright;對抗驗證補 5 缺口)

ultracode 對抗驗證(§4)裁定 E2E testid 接線真實非空殼(逐字查證斷言對齊 production),補 5 缺口:
- [high] model-path: 加 E2E_A1_IFC_PATH env + rule-run 前 conditional fill a1-step-path
  (非開發機/CI 須設含 IFCCOLUMN 的 server-side IFC,否則硬碼 fixture path 找不到→timeout 假失敗)。
- [med] 截圖入庫: first-frame / red-highlight 各多存一份到 tracked docs/evidence/(artifacts/e2e/*.png
  被 ignore、PR 看不到)。
- [med] loopbackArtifactUrl: 非 loopback hostname 一律正規化 127.0.0.1(不限 port 49101,避開
  coordinator path-traversal guard)。
- [med] failed>0 前置: 註解說明記分板 "failed" 為固定 label 無法驗 >0,以 a1-highlight-3d enable
  作實質前置(隱含有失敗構件)。
- [P2] test.skip→test.fixme: 第三 test(未對映誠實拒絕)標 NOT BUILT 列級高亮鈕(skip 易誤讀為 PASS)。

驗證: playwright test --list 成功 parse 3 tests(語法正確)。E2E 真跑屬 P4(需 branch coordinator :8005
+ viewer :5180 + cross-build-target 重建 + 含 IFCCOLUMN 的 succeeded conversion + 真 Kit GPU)。
含 vg01-highlight-column.ids(IFCCOLUMN highlight 規則 fixture)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgqvMSVWmnwEDaPqDAgXxg

* task#5: E2E model-path 改用 conversion source IFC(修紅高亮 0-failed)+ P4 真機取證入庫

P4 真 Kit 串流取證(branch coordinator :8005 + Kit :49100 + conversion model.ifc/270):
- test 1 first frame 綠燈 + stage matched:PASS,截圖證 A1 嵌入 live viewer、first frame
  「已收到真畫面(綠)」、loaded==Stage URL(model.usdc) matched、GPU session 證綠。
- test 2 失敗構件 3D 紅高亮:PASS,rule-run model.ifc(vg01 IDS)→25 mapped IfcColumn failed
  →enrich→highlight 鈕 enable→點→highlight_result「已在 3D 標示」;viewer live(WebRTC started)。

修正:test 2 原失敗根因 = E2E_A1_IFC_PATH export 未傳進 spec → rule-run 用 defaultA1IfcPath
(fixture-bytes 無 IFCCOLUMN→0 failed→「尚無失敗構件」)。改 beforeEach 從 conversion detail 取
source IFC host_local_path(ifcSourcePath),test 2 fill a1-step-path → rule-run 對 conversion 的
model.ifc,failed column guid 在 element_mapping 找得到 → 可高亮。比 export env 穩健、CI 通用。

誠實限制:test 2 中央 3D 視覺紅色 column 因 270 georeferenced 模型相機框取 + iframe 截圖範圍偏 UI
chrome 未清楚捕捉(highlight 指令鏈、viewer live、stage matched 均真;非 pipeline 失敗)。

evidence 入庫 docs/evidence/viewer-embed-a1-highlight/(firstframe-stage-matched / redhighlight-viewer / redhighlight-full)。
mapping 真(mock:false,fake_mapping_count:0,421筆); rule-run model.ifc vg01 summary.failed=44(25 mapped)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgqvMSVWmnwEDaPqDAgXxg

* openspec: viewer-embed-a1-highlight active change(proposal + tasks)

P6 pr-review-agent gate:code PR 需 active openspec/changes/<id>/(missing_openspec blocker 預防)。
對應 spec docs/superpowers/specs/2026-06-22-viewer-embed-a1-highlight-design.md。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgqvMSVWmnwEDaPqDAgXxg

* openspec: 補 viewer-embed-a1-highlight spec delta(解 pr-review-agent validate blocker)

pr-review-agent 的 `openspec validate viewer-embed-a1-highlight` 因 change 只有
proposal.md + tasks.md、缺 specs/ delta 而失敗(high blocker:No deltas found)。
補上 specs/session-first-review-viewer/spec.md,以 `## ADDED Requirements` + 四項
Requirement(first_frame_at 後端化記真 / console 版本化 vg01 postMessage 橋 /
viewer 生命週期事件 + canOperate 守衛 / A1 IX-A1-06 四條件啟用高亮),每項含
#### Scenario WHEN/THEN,忠實對映本 branch 已交付且已測的行為。

`openspec validate viewer-embed-a1-highlight`(含 --strict)本機通過:is valid,4 deltas。

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

* fix(viewer-embed): 修 reviewer 對抗發現(P1 stage 偽證據 + 4 項 P2/Major)

對抗複驗(Codex + CodeRabbit inline)在 production code 抓到的 substantive 發現逐項修復:

- [P1] Window.tsx `_completeStageLoad`:移除 `|| this.pendingStageUrl` fallback。畫面可見但 Kit
  未回 loaded URL 時,舊碼把「請求載入的目標 pendingStageUrl」當成「Kit 已證實載入」→ 標 stage matched
  並送 first_frame,舊模型殘影可能被誤判為已對齊、對未證實 stage 開放高亮(違誠實鐵律)。修正後 frame 可見
  仍誠實送 first_frame 但 stageUrl=null、stageLoadStatus 維持 unproven,高亮鈕保持 disabled 直到 Kit
  真回報相符 URL。
- [P2/Major] EmbeddedViewer.tsx:viewerOrigin 帶尾斜線/路徑前綴時,e.origin 比對與 postMessage
  targetOrigin 會全數拒收 viewer 訊息(path-prefixed 部署證據永不閉合)。加 normalizeOrigin 取純 origin
  比對,iframe src 仍用完整 base(去重複尾斜線)。
- [P2] EmbeddedViewer.tsx:iframe 只帶 session、未轉發 coordinatorApiBase/coordinatorSocketUrl,
  未 bake 同一 coordinator 的 image/E2E 會打錯 coordinator。比照 /ui/open 由 runtime/status 的
  coordinator.public_base_url 轉發 handoff query。
- [P2] pages.tsx isStageMatched:A1 gate 原為嚴格 `loaded===exp`,viewer 端容忍同轉檔不同 URL,
  造成「viewer 已 first_frame、A1 gate 卻永久 disabled」。加 stageUrlsEquivalent:精確相等 OR
  僅 origin 不同而 pathname+search 相同(刻意不做任意 job-id 子字串比對,避免不同轉檔誤判)。
- [P2] pages.tsx session 下拉:切換 session 只清 first-frame/stage/highlight UI、未重置 rule 結果,
  舊 session 已 enrich 的 GUID 可被送進新 session viewer。改 onChange 加 dispatch RESET。
- [Major test-only] e2e spec beforeEach:移除「盲關 runtime/status 上所有 active/created session」
  (summary 無 created_by、共享環境會誤殺他人 session);本 spec 只建/操作/關自己的 sessionId。
- [Minor test-only] EmbeddedViewer.test.tsx:origin-rejection 測試 source 改用 iframe.contentWindow,
  讓測試只隔離 origin 守衛(不被 source 守衛遮蔽)。

補回歸測試:origin normalize(尾斜線 viewerOrigin 仍接受純 origin message)、handoff query 入 src、
visible-stream 未回 URL → first_frame stageUrl=null(P1 守衛)、Kit 回真 URL → first_frame 帶真 url。

驗證:web-viewer-sample `npm run build`(tsc+vite)綠;vitest 352 tests 全綠;
`playwright test --list` 3 tests parse 綠;`openspec validate viewer-embed-a1-highlight --strict` is valid。

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request Jul 1, 2026
… ledger chip(方向1 整合 main #259) (#265)

* docs(spec): P0 一致性修正 — 摘要對齊已拍板 auto-enroll 內文

spec-to-done P0 自檢:修 4 處 stale 摘要矛盾(上輪加 §7-B' auto-enroll 時漏更新):
- 一句話 & §2.5-8 移除「watcher zero blast radius」(與 §3.4/AC7 auto-enroll
  刻意改 watcher、非零 blast radius 矛盾;deriveIntakeFromKey 不改的部分保留)。
- §10-1 移除已拍板的「使用者拍板 §7-A/§7-C」。
- §8 重複編號 5.→6.。
純摘要對齊已拍板內文、非新設計。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

* plan: MinIO #minio 逐層資料夾導覽 + #conv baseline 揭露實作 plan

依 superpowers writing-plans 從 2026-06-24 issue spec 產 12-task TDD plan:
後端 listMinioFolder(Delimiter additive)/ POST /api/conversion/trigger(x-dev-token 守門)/
watcher tick dedup 改持久 ledger 去重(§3.4 全自動 auto-enroll,HIGH 風險走 GitNexus impact);
前端 MinioDataPage 逐層導覽 + ledger chip + 觸發鈕、ConversionSchedulingPage baseline 揭露;
重寫綁三層樹的舊測試斷言;browser E2E vertical slice;文件三方同步移除 NOT BUILT 浮水印。

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

* plan: fix 修進 reviewer blocker/major(Task 7 拆 7a/7b/7c、跨 monorepo import 內聯、含 source IFC badge、AC6(a) 文案、Task 5 isLedgered 型別、Task 3 精準 etag、Task 9/11/12 before-after 片段)

- Task 7 拆成 7a(資料夾殼+含 source IFC badge)/7b(chip)/7c(觸發鈕) 三可獨立驗證子階段、各自 commit(task-decomposition blocker)
- 跨 monorepo import ledgerChipStatus 改前端內聯 3 行同義函式(buildability/completeness:tsconfig include 只含 src、moduleResolution bundler 必 TS2307)
- 補 spec §2.5 第 5 點獨立 AC「含 source IFC」資料夾 badge:Task 1 folders 改 {prefix,has_source_ifc}(prefixHasSourceIfc probe)、連帶 Task 2/6 型別與測試同步(spec-alignment)
- 補 AC6(a) 兩條補救說明文案測試與實作(重新上傳改 etag/手動 webhook ifc-ready 純文字,不與 AC6(b) 一鍵鈕重複)(spec-alignment)
- Task 5 修 isLedgered 收 mw_<hash16> 非 key 字串的 buildability bug(用真實 idempotencyKeyFor 比對,已查證 helper bucket=bim-control)、並拆 5a/5b/5c
- Task 3 route 改 Prefix=key 精準取 etag(避 527 物件全量 list)+ 加 idempotency_key 由非空 etag 衍生斷言
- Task 9/11/12 由散文改 before/after 具體片段(console.test.tsx 6 個 it、e2e stub/beforeAll/body、prototype HTML MinioPage 整段、closed-loop §4.2 markdown)

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

* plan: fix reviewer blocker/major(補齊 Task7 JSX/Task12 openspec delta、Task1/3/7 拆步、Task8 fixture object_key)

- Task 7 MinioDataPage:散文注解 → 可直接複製的完整 JSX 骨架(四態/資料夾鈕+has_source_ifc badge/role+三段 badge/chip/觸發鈕/IntentDialog)。
- Task 12 openspec:mkdir-only → 補 proposal.md + 兩份 spec.md delta(minio-fileserver-source / minio-watch-auto-intake,MODIFIED 格式)+ tasks.md 逐字稿。
- Task 8 fixture:mw_f failed 補 object_key 非 null(修 fixture 與說明矛盾、偽 PASS)+ 加 object_key=null 反向 it。
- Task 1 拆 1a/1b/1c + folders 型別最終版鎖定聲明(消 cross-task 回補困惑);Task 3 拆 3a/3b(服務函式+單元測試 / route+supertest)。
- Task 7 拆 7-pre(測試先行 RED)/7a/7b/7c,移除「先寫全測試+全重寫」同步;修「7a 前置回補」措辭為「直接用、不回補」。

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

* docs(spec): P1 HELD 修正 — 解 §3.2/§3.4 spec 矛盾 + 加既有符號真實簽名

spec-to-done P1 回 plan_not_aligned(強制停下點)。指揮官修根因:
- spec 矛盾(plan reviewer 抓到):§3.2-4「重啟也救不了既存自動轉檔」在 §3.4
  全自動 auto-enroll(持久 ledger 去重)下已 FALSE → 明標該警語不得保留(避免
  #conv UI 自相矛盾誠實違規)。
- §6.1 新增既有符號真實簽名供 plan-grounding:ConversionLedger=constructor
  (new,無 createConversionLedger 工廠)、IntentDialog props=open/title/cost/
  onConfirm/onCancel/busy/actionErr(無 body/confirmLabel)——修兩處 plan 逐字稿
  對不上 codebase 的 completeness blocker。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

* plan: 對齊 spec §6.1 真實簽名 — ConversionLedger 用 new、IntentDialog 用真 props

前次 plan 在 spec §6.1(commit fe6e8bb,補上既有符號真實簽名)之前定稿,殘留兩處逐字稿與 codebase 不符:
- Task 3a 單元測試誤用不存在的 createConversionLedger 工廠 → 改 new ConversionLedger(path)(conversionLedger.ts:50,56 為 constructor-based、無工廠 export)。
- Task 7 MinioDataPage 的 IntentDialog 誤用 body / confirmLabel / 漏 open → 改用真實 props open/title/cost/onConfirm(reason)/onCancel/busy/actionErr(IntentDialog.tsx:9-21);confirmTrigger 接 reason、加 triggerBusy、成功才關 dialog、失敗經 actionErr 顯示,並移除 props-grep 軟性 punt 註記。

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

* plan: fix Task 8 補 IntentDialog 完整實作(onConfirm/onCancel/loadRecords/JSX)

reviewer(completeness, major):Task 8 step 3 只給觸發鈕片段與散文,缺
ConversionSchedulingPage 的 IntentDialog 完整實作。補上可直接複製貼上的
(3a) 本地 state(pendingTriggerKey/triggerBusy/triggerErr)、(3b) confirmTrigger
handler(呼 coordinatorClient.conversionTrigger、成功 setPendingTriggerKey(null)+
void loadRecords()、失敗 setTriggerErr)、(3c) thead th + tr td 觸發鈕、(3d) 完整
<IntentDialog open={!!pendingTriggerKey} ...> JSX(props 逐字對齊 spec §6.1:
open/title/cost/onConfirm(reason)/onCancel/busy/actionErr),並加與既有 pendingAction
IntentDialog 並存的執行者註記。零脈絡執行者無需推斷。

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

* task#0: 1a MinioObjectView 加 idempotency_key(listMinioObjects 補欄、測試零改)

MinioObjectView interface 末尾新增 idempotency_key: string 欄;
listMinioObjects out.push 補 idempotencyKeyFor(bucket, key, etag);
import 行加 idempotencyKeyFor from minioWatcher.js。
既有 minio-objects-route.test.ts 零退化。

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

* task#0: 1b listMinioFolder 基本 Delimiter list(folders + 當層 objects)

新增 MinioFolderNode / MinioFolderListing 型別;
listMinioFolder while-loop 處理 IsTruncated(AC-D2 不截斷);
對每個 .ifc 物件附 idempotency_key 供前端 chip lookup(spec §3.3 路徑 A);
badge 解析用 prefix="" 取完整 key 語意(project/category/version)。

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

* task#0: 1c prefixHasSourceIfc probe + folder has_source_ifc(spec §2.5 第 5 點)

新建 minio-folder-route.test.ts(4 個 it:folders/objects 分離、has_source_ifc probe、
IsTruncated 合併分頁、.ifc idempotency_key + 三段 badge);
prefixHasSourceIfc 對每個 CommonPrefix 各發遞迴 list 確認是否含 .ifc;
folders 型別最終為 {prefix, has_source_ifc: boolean} 陣列。

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

* task#0: fix prefixHasSourceIfc 失敗路徑補 regression + 序列 probe 量化上限註記

important #1:補測試 pin「probe 5xx → listMinioFolder propagate(route 回 502),
不靜默把 has_source_ifc 偽報 false」契約(誠實鐵律:不臆測);新增 stub status? 模擬 5xx;
prefixHasSourceIfc docstring 加失敗契約勿改註記。已驗測試有 teeth(swallow→false 會 RED)。

important #2:序列 probe 加量化上限註記——生產頂層 7 個(spec §1.1)、可接受 ~10-15,
>10-15 才改 Promise.all(須改 stub 為 prefix-keyed dispatch)。

minioClient.ts 改動 100% comment-only,無 production symbol body 變更。
npm run verify:build 綠 + 455 tests 全綠(含 minio-folder-route 5 tests)。

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

* task#1: /api/minio/objects 加 delimiter 參數,帶 delimiter=/ 走 listMinioFolder 回 folders[]

spec §2.1 AC-D2:route 收到 ?delimiter=/ 時改走 listMinioFolder 回
{ bucket, prefix, folders, objects, count };不帶 delimiter 維持
舊 listMinioObjects 路徑,byte-identical 回應,既有 455 測試零退化。
新增 minio-objects-delimiter-route.test.ts(3 個 supertest 場景)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

* task#1: fix Task2 delimiter route 補缺漏驗收測試 + 對齊 plan truthy 分流

3 項 spec-compliance gap:
- gap1:補「未設定 MinIO → 帶 delimiter 仍誠實回 count=0 + note(不 500)」
  驗收測試(plan §Task2 line 351,行為已存在於 handler early-return,原測試未覆蓋)。
- gap2:route 整合層斷言 folders[].has_source_ifc 真實值(plan line 346),
  原 test case 1 只 map prefix 字串;改 probe stub 使首夾含 model.ifc→true、次夾無→false。
- gap3:delimiter 分流由 `!== undefined` 改 truthy(`if (rawDelimiter)`,rawDelimiter 預設 ""),
  對齊 plan:?delimiter=(空字串)回落舊路徑,避免前端收到空 folders[] 而非舊格式。

驗證:delimiter+folder+objects+records 12 tests 綠;coordinator 全套 459 tests 綠;tsc --noEmit 乾淨。

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

* task#1: fix /api/minio/objects delimiter 白名單 + 補分頁 route 整合測試

Important #1:rawDelimiter 讀取後即做白名單檢查,非空且非 "/" 回 400
invalid_delimiter,擋在轉送 S3 SDK 前(避免多字元/控制字元/XML 特殊字元
經 ListObjectsV2Command.Delimiter 拋非預期錯誤並在 502 detail 洩漏內部
訊息,且杜絕前端控制 delimiter 語意超出 spec §2.1)。

Important #2:minio-objects-delimiter-route.test.ts 補一個 route 整合層
分頁案例(頂層 list IsTruncated=true→次頁收尾,合併 folders 共 2 個),
防 regression 把 listMinioFolder while-loop 收尾截斷而 route 不報錯。

驗證:coordinator 全套 461 tests green、tsc build 乾淨。

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

* task#1: fix minioClient etag 雙狀態 + probeSuffix 三路分流 + prefix CR/LF 守門

修掉 quality 三項 IMPORTANT 發現(minio folderview Task 1):

IMPORTANT-1(minioClient.ts listMinioFolder/listMinioObjects):etag 統一去引號一次,
同一去引號值同時供 etag 欄位與 idempotencyKeyFor,消除「line 去引號 / line 不去引號」
雙狀態(idempotencyKeyFor 內部 stripEtagQuotes 對已去引號值冪等,hash 不變、watcher
loop 行為不受影響)。

IMPORTANT-2(同上兩函式):badge 解析改三路分流——.ifc 用 model.ifc suffix、.usdc 用
model.usdc suffix、role='other' 直接 ok:false 跳過 deriveIntakeFromKey,不再用 /model.ifc
強行解析 other 物件(意圖明確,防未來改 probeSuffix 邏輯時 other 物件意外拿到 badge)。
新增 minio-folder-route.test.ts pinning 測試鎖定「other → badge 全 null」不變量。

IMPORTANT-3(app.ts /api/minio/objects route):rawPrefix 轉送 S3 SDK 前加 CR/LF 守門,
偵測到換行字元回 400 invalid_prefix,擋 header injection(帶不帶 delimiter 都套用)。
TDD:先寫失敗測試(200→預期 400)再實作。

驗證:coordinator 全套 463 測試綠(含 minio client/route 14 測試 + 2 新增)、
tsc --noEmit 0;detect_changes staged scope 只含 createCoordinatorApp(route 守門所在
enclosing 函式),無預期外 symbol。

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

* task#1: fix quality review 3 findings (502 sanitize / | guard / empty-configured test)

P3 quality_review_not_closing 的 3 個 finding(fixer 撞配額前已寫完、未 commit;
指揮官驗證 tsc 綠 + 467 tests passed 後收尾):
- q1-502-leak: app.ts 502 detail 改固定 'minio list failed',完整 err 走 structLog。
- q3-pipe-guard: minioClient listMinioFolder/listMinioObjects 對含 '|' 的 key
  skip+structLog warning(避 idempotencyKeyFor bucket|key|etag 分隔符 hash 衝突)。
- q2-empty-configured-test: 補「MinIO 已設定但當層空(folders=[] objects=[] count=0)」
  測試,與「未設定」empty 態分開(spec §5 AC-honesty)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

* task#2: plan: 3a triggerManualIntake 服務函式 + 純函式單元測試

新建 manualIntake.ts(triggerManualIntake/ManualIntakeConfig/ManualIntakeResult);
重用 deriveIntakeFromKey/idempotencyKeyFor/createMinioS3Client(非 import triggerIntake 私有 closure);
server-side presigned 不外洩;upsert ledger status=detected;冪等設計。
兩個純函式單元測試(合法 key + 落帳 / key 含 .. 拒)全 pass。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

* task#2: plan: 3b POST /api/conversion/trigger route + supertest(x-dev-token 守門)

新增 POST /api/conversion/trigger:x-dev-token 守門(拒匿名 403)、
deriveIntakeFromKey 防路徑穿越(400)、server-side presigned + 獨立
triggerManualIntake 寫 ledger、回 { status, idempotency_key };
presigned URL 不外洩。4 個 supertest it 全 pass;全套 473 tests 零退化。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

* task#2: fix 3a 補 presign 失敗 → ok:false 單元測試(plan 第三驗收情境)

manual-intake.test.ts 原只覆蓋合法 key / key 含 .. 兩情境,缺 plan 第 449 行
明列的第三情境「presign 失敗 → { ok:false }」。新增一支單元測試:合法 key 過
deriveIntakeFromKey 穿越守門後,以非法 endpoint URL 令 getSignedUrl 於建 URL
階段拋 TypeError,命中 manualIntake.ts:40-43 catch,驗回 ok:false + reason
含 'presign failed',且 ledger 不落帳(失敗不留半截紀錄)。

TDD:暫時 sabotage production catch(吞 error)確認此測試轉 RED(expected
false received true,fall-through 到 upsert 回 ok:true),還原後轉 GREEN,
證明測試確實覆蓋 presign 失敗分支。純測試新增,未動 production symbol。

驗證:npx vitest run tests/manual-intake.test.ts(3 passed)、npm test
(44 files / 474 passed)、npm run build(tsc 無誤)。

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

* task#2: fix trigger route 對齊 plan §3.3 模板(404 guard / 502 上游 / audit log)

quality review 3 findings(plan docs/superpowers/plans/2026-06-24-minio-folderview-and-baseline-disclosure.md §3.3 route 模板 line 680-739 為權威):

- Finding 2(404 guard):key 不在 bucket 時原退化成 etag=''→偷偷以退化 idempotency_key 落帳,
  誤導呼叫端。改為 !match || !match.etag → 404,不留半截 ledger record。
- Finding 1(502 上游):原把所有 triggerManualIntake ok:false(含 presign failed)一律映成
  400,混淆 client input error 與上游連線失敗。改為先 deriveIntakeFromKey 守門(穿越/規約失敗
  → 400),其餘 ok:false → 502;S3 list 拋錯亦 try/catch 收斂成 502(原無 catch→unhandled
  reject 變 Express 500)。catch 沿用 q1-502-leak sanitized detail,不洩漏 infra 細節。
- Finding 3(audit log):補與 prioritize/retry/watch.toggle 對稱的 structLog audit
  conversion.trigger(actor/target/reason)。

Finding 4(3a 第三單元測試 presign 失敗→ok:false)現況 manual-intake.test.ts:47-58 已存在且綠
(commit cbc186b),本次不重複。

新增 2 條 route 整合測試(404 / 502),先 RED 親見現況失敗(404 走 200 落帳、502 timeout+
unhandled reject)再實作轉綠。app.ts 新增 import deriveIntakeFromKey。

驗證:tsc --noEmit clean;npx vitest run 全 44 files / 476 tests 綠(含 trigger 6 it)。

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

* task#2: fix quality review 2 findings (presign reason sanitize / HeadObject 取 etag 取代 listMinioObjects prefix 誤用)

P3 task#2 quality review 的 2 個 Important finding(fixer 撞暫時性 rate-limit 回 null、
只寫到 headMinioObjectEtag helper 半成品未接線;指揮官完成接線+測試 stub、驗 tsc 綠 +
476 tests passed 後收尾):

- finding#1 presign-leak: triggerManualIntake 失敗(presign/上游)時 app.ts 502 路徑原本
  直接回 detail: result.reason(含 SDK endpoint 解析原文)。改為完整 reason 走 structLog、
  回應只給固定 'minio presign failed'(對稱既有 catch 的 'minio list failed',最小資訊原則)。
  強化既有 502 catch-path 測試斷言:detail 鎖固定字串 + 不含 InternalError/<Error>/stack。

- finding#2 prefix-misuse: trigger route 原用 listMinioObjects(client, bucket, key) 取 etag,
  把完整 object key 當「資料夾前綴」傳入→deriveIntakeFromKey 恆 ok:false、白算 MinioObjectView
  badge、違反 API 契約。改用 headMinioObjectEtag(HeadObject 直取單一已知 key 的去引號 etag;
  404/NoSuchKey→null、其餘上游錯誤 rethrow 由 route catch 收 502)。!etag 守門涵蓋 null/空字串。
  測試 S3 stub 補 HeadObject 分支(命中回 ETag header、缺則 404),保留 GET→XML 給 watcher。

冪等不破:idempotencyKeyFor 內部 stripEtagQuotes,headMinioObjectEtag 去引號與 watcher 一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

* task#3: 新增純函式 ledgerChipStatus + 單元測試(AC-chip 狀態映射)

新建 bim-review-coordinator/src/services/ledgerChipStatus.ts:
- 匯出 ChipStatus 型別(ConversionLedgerStatus | 'untracked')
- 匯出純函式 ledgerChipStatus(idempotencyKey, records)
  → 無紀錄回 'untracked'(誠實不臆測;前端顯「未轉含 baseline 既有檔」)
  → 有紀錄回 ledger.status(ready/detected/queued/converting/failed)
- 無 I/O,前端 Task 7 可 import 同義常數

新建 bim-review-coordinator/tests/ledger-chip-status.test.ts(8 it,全 PASS):
先確認 module 不存在失敗 → 最小實作 → 8/8 PASS;npm run verify 全 484 test PASS。

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

* task#4: watcher tick dedup 改持久 ledger 去重(§3.4 全自動 auto-enroll)

§3.4 / AC7 / AC-autoenroll:把 startMinioWatcher tick 的去重從 in-memory
isFirstRound baseline 特例改為注入的持久 ledger 水印。

- minioWatcher.ts:MinioWatcherOptions 加 isLedgered?;tick 移除「首輪 baseline
  不觸發」特例,改對每個 */model.ifc 算 idkey=idempotencyKeyFor(bucket,key,etag)
  查 isLedgered(idkey)——無紀錄→觸發 intake、有紀錄→skip。in-memory seen 留作
  單輪/跨輪快取(權威去重以持久 ledger 為準)。baseline_count 兩種模式皆照舊填
  (首輪 model.ifc 數,純診斷,不再 gate 觸發)。
  向後相容:未注入 isLedgered(既有整合測試直接構造 watcher)回落舊 baseline 行為,
  不波及未注入呼叫端。
- app.ts:startMinioWatcher({...}) 注入 isLedgered:(idkey)=>conversionLedger.get(idkey)!==null
  (惰性求值,watcher 首輪 tick 走 macrotask 時 conversionLedger 已初始化,無 TDZ;
  watcher tick 對 ledger 唯讀,落帳由 intake route 端負責)。
- tests/minio-watcher-loop.test.ts:makeWatcher 注入 isLedgered(預設 ()=>false);
  改「首輪 baseline 不觸發」為「首輪即觸發 ledger 無紀錄物件」+ 加「已落帳不觸發」;
  後續輪 it 以真實 idkey(idempotencyKeyFor)表達既有已落帳、僅新增無紀錄物件觸發;
  重啟 it 改驗持久 ledger 命中→不重觸發(重啟不風暴),移除已不再使用的 replay stub。

驗證:GitNexus impact(startMinioWatcher,upstream)=27 impacted/LOW/exact(結構面);
detect_changes(staged) scope 限於 watcher tick dedup + 注入點 + 測試,未波及
intake/dispatch 下游。npm run verify(build+test)45 files / 485 tests 全綠。

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

* task#4: fix conversionLedger TDZ 防守上移宣告 + 重啟測試補 loop liveness 守衛

Important 1(app.ts):startMinioWatcherIfEnabled 的 isLedgered closure 捕捉
conversionLedger,而 selfBaseUrl 立即啟動路徑在 conversionLedger 賦值(原 const
宣告)之前同步呼叫該函式。首輪 tick 走 setTimeout(macrotask) 故 runtime 安全,但
此安全性依賴「無人在兩點之間插入 await」這條無守衛的不變式;一旦插入 await,const
版會在 runtime 爆 TDZ ReferenceError 且 TS 查不到。改為「let 宣告上移至函式之前 +
原處改賦值」:TS 仍保留 used-before-assigned 檢測,並排除 TDZ 隱患。

Important 2(minio-watcher-loop.test.ts):[autoenroll] 重啟測試移除舊
triggered_total>=1 後,僅靠 300ms wall-time 靜默判 received 穩定,無法區分「正確
skip」與「新 watcher loop 凍結(stopped=true 被誤設)」。補
waitFor(poll_count>=2) loop liveness 守衛,確認新實例確實跑過 ≥2 輪 tick 再判穩定。

驗證:npm run verify(tsc build clean + 485 tests / 45 files 全綠)。
scope:detect_changes(staged) 僅 createCoordinatorApp(test 檔非 symbol 不入列),
未逸出預期。

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

* task#4: fix conversionLedger 時序依賴 root-cause + 補 isLedgered 接線整合測試

review Important #1:app.ts 的 watcher isLedgered closure 捕捉 conversionLedger,舊版靠
「賦值排在宣告之後、但首輪 tick 走 macrotask」的脆弱時序假設撐 runtime 安全,且註解誤稱
「TS 仍可捕捉 used-before-assigned」(實測 tsc --noEmit exit 0 不會對 closure 捕捉的
uninitialized let 報錯)。改採 Option B:把 `conversionLedger = new ConversionLedger(...)`
賦值重排到「兩條 watcher 啟動路徑(server.on("listening")、selfBaseUrl 立即啟動)」之前,
根除時序依賴——closure 任何時刻被求值都看得到已建好的 ledger;型別維持非空 ConversionLedger。
同步修正宣告處與 closure 處的錯誤 TDZ/used-before-assigned 說明。

review Important #2:新增 tests/minio-watch-ledger-wiring.test.ts,supertest 級整合測試走
production 接線(真 listening server → server.on("listening") 啟動路徑 → 預設 loopback
selfBaseUrl → 真 ConversionLedger 落地檔 → 真 /api/external/ifc-ready route → isLedgered
closure)。鎖死「closure 確指向 production 的 conversionLedger 實例」:(1) ledger 無紀錄物件
首輪觸發並 upsert 落帳;(2) 預先 seed 落帳物件全程 skip、triggered_total=0。已驗以
isLedgered=undefined 注入時測試會失敗(真 regression guard,非 tautology)。floor 降檔走既有
MINIO_WATCH_INTERVAL_FLOOR_SECONDS seam(loadConfig 對 interval 有硬下限 10s)。

驗證:tsc --noEmit exit 0;coordinator 全測試 487 passed(含新增 2 筆 + watch-toggle 19 +
watcher-loop 17);npm run build exit 0。

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

* task#4: fix quality review 2 findings (conversionLedger let→const 靜態消除 TDZ / 整合測試走 production ledger 去重路徑)

P3 task#4(plan Task 5 watcher auto-enroll)quality review 的 2 個 Important finding(workflow
fixer 兩輪 0ad61dd/abbbebe 加註解+上移賦值但保留 let、reviewer 仍標未閉合;指揮官改用結構性解,
驗 tsc 綠 + 487 tests passed 後收尾):

- finding#1 conversionLedger-TDZ: app.ts 原 `let conversionLedger;`(宣告)+ 後段賦值,靠「賦值
  早於 watcher 啟動路徑」的隱性順序撐 isLedgered closure 安全。改為宣告即建構 `const
  conversionLedger = new ConversionLedger(...)`,型別系統靜態保證 closure 捕捉到已初始化 ledger,
  日後在啟動路徑前插程式碼也不可能重新引入 TDZ。ConversionLedger 建構僅讀持久 JSON、無時序副作用。

- finding#2 integration-test-legacy-path: minio-watch-intake-integration.test.ts 原未注入
  isLedgered → 走 legacy baseline 模式(測的不是 production §3.4 auto-enroll 路徑)。改注入 stateful
  isLedgered(初始 899 已入帳→首輪 skip、新增 988 未入帳→觸發),讓最靠近 e2e 的整合測試走真實
  ledger 去重路徑,保留既有 988 端到端鏈 + triggered_total===1 斷言。

idempotencyKeyFor 內部 stripEtagQuotes,測試傳 "e1" 與 watcher 自 XML 帶引號 etag 算出的 idkey
一致,故 isLedgered(899) 命中。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

* task#5: 前端 client 加 getMinioFolder + conversionTrigger(帶 x-dev-token)

- MinioObject 加 idempotency_key: string | null(路徑 A,後端預計算供 chip lookup)
- 新增 MinioFolderListing / ConversionTriggerResponse 型別
- 新增 DEV_AUTH_TOKEN 取值(VITE_DEV_AUTH_TOKEN env 或 "dev-token" fallback)
- 新增 jsonPostAuthed(帶 x-dev-token header,用於 conversionTrigger)
- coordinatorClient.getMinioFolder:打 /api/minio/objects?delimiter=%2F,回 MinioFolderListing
- coordinatorClient.conversionTrigger:POST /api/conversion/trigger,帶 x-dev-token
- 對應 4 條 TDD 斷言;既有 19 條測試零退化;session-first contract passed

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

* task#5: fix MinioFolderListing/MinioObject 前端型別對齊後端真實 wire shape

Critical 1:MinioFolderListing.folders 型別 string[] 與後端不符。後端 listMinioFolder
回 MinioFolderNode[]({ prefix, has_source_ifc },minioClient.ts:35-43、43),route 直接
response.json(listing) 原樣回傳,spec §2.5 第 5 點明訂 has_source_ifc folder badge。
前端改為 MinioFolderNode[](新增 export interface MinioFolderNode),消費端可安全做
folder.prefix 字串操作,不再收到 [object Object] 或在 .endsWith 拋 TypeError。

Critical 2:MinioObject.idempotency_key 型別 string | null 與後端不符。後端
MinioObjectView.idempotency_key 為非 nullable string,且 listMinioFolder/listMinioObjects
對所有物件(含 role='other')無條件呼 idempotencyKeyFor 並寫入(minioClient.ts:133,230)。
前端改為 string,消除「=== null 跳過 chip lookup」這條生產環境永不觸發的幽靈分支。

Important 1:conversionTrigger 測試的 x-dev-token 由 toBeTruthy() 改為嚴格值相等
=== "dev-token"(對齊後端 isKitMutationAuthorized token === devToken、devToken 預設
"dev-token"、前端 DEV_AUTH_TOKEN fallback "dev-token");token 被改壞成空白/字面串時
不再誤過。

Important 2:getMinioFolder 測試補 expect(r.prefix).toBe("") 契約錨點,讓「無 prefix
參數呼叫時回應 prefix 欄位」有實質斷言(breadcrumb 顯示來源)。

對應 test mock 同步改真實 wire shape(folders 物件陣列、idempotency_key 給 string)。
驗證:scoped tsc(coordinatorClient.ts + test)exit=0;vitest 全套 32 檔 386 測試綠;
vite build exit=0。GitNexus detect_changes(staged) risk=low、affected=0、scope 限
coordinatorClient.ts。零生產 getMinioFolder/MinioObject 消費端(#minio 資料夾頁 NOT BUILT)。

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

* task#5: fix coordinatorClient quality 三項:jsonPostAuthed body 型別收斂 + status union + getMinioFolder 502 守門

Important 1:jsonPostAuthed body 由 unknown 收斂為 Record<string, unknown>,刪除 `?? {}`
fallback(比照 jsonPut)。對 mutation(AC-trigger)而言 null body 靜默變空物件很危險,
後端會誤判欄位缺漏回 400;改型別層即阻擋 null/undefined body。唯一呼叫端 conversionTrigger
傳 { key, reason } 不受影響。

Important 2:新增 ConversionLedgerStatus union 型別當單一來源,ConversionRecord.status 改引用;
ConversionTriggerResponse.status 的 wire 仍是寬 string(後端 ManualIntakeResult.status:string,
無法 compile time narrow),補 narrowConversionStatus() runtime guard 供 Task 6 chip-patch 消費端
把非法值收斂成 null(誠實鐵律:非法值顯 unknown,不靜默把 chip 設成壞狀態)。

Important 3:補 getMinioFolder 502 throw 測試,與 getMinioObjects 502 對稱,守住 AC-honesty
「MinIO 不可連線時 UI 能顯 error 態」regression。

驗證:coordinatorClient.test.ts 26 passed(was 23,+3);tsc --noEmit 與 baseline byte-identical
(11 個 pre-existing 錯誤皆為無關的 test-fixture idempotency_key 缺漏與 @types/node 環境問題,
本次 0 新增);detect_changes(staged) risk=low、scope 限 coordinatorClient.ts + 其 test。

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

* task#5: fix quality review 2 findings (未設定分支補 folders:[] 防前端 crash / 補未設定覆蓋測試)

P3 task#5 quality review 的 2 個 Important finding(workflow held quality_review_not_closing;
指揮官修,驗 coordinator tsc 綠 + 487 tests + coordinatorClient 27 tests passed):

- finding#1 folders-missing: /api/minio/objects MinIO 未設定 early-return 回的物件缺 folders 欄位,
  但 getMinioFolder(走 ?delimiter=/ 打同一 route)前端 MinioFolderListing 型別 folders 必填,消費端
  r.folders.map() 會 TypeError crash UI。未設定分支補 folders:[](reviewer 方案 A,與已設定分支
  listMinioFolder shape 對齊,空陣列比缺欄位誠實)。

- finding#2 coverage: 更新既有後端測試(原斷言「未設定不臆測 folders」與修法衝突 → 改斷言 folders:[])
  + 新增前端 client 測試(mock 未設定回應,鎖 r.folders 為陣列、.map() 不 throw)。

註:web-viewer 全 tsc 另有 console.test/MinioDataPage(idempotency_key fixture)與 indexHtml/
IntentDialog(node:fs)錯誤,屬 plan task 7/9 範圍(非本 2 findings、git status 未改動=非本次引入),
由後續 task + task#10 npm verify 收斂。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

* task#6: 7-pre MinioDataPage 測試先行(9 it 全 RED,pages.tsx 未動)

逐層資料夾 + chip + 觸發 + 四態斷言;TDD RED,下一步 7a 起分階段轉 GREEN。
未設定 note 路徑 fixture 因 MinioFolderListing 型別無 note 欄,以 Awaited<ReturnType> cast 保型別正確。

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

* task#6: fix MinioDataPage 7a 逐層資料夾殼 + 含 source IFC badge(無 chip/trigger)

補回 plan §line 1554 缺漏的 7a commit checkpoint(原 7a+7b+7c 併成單一 commit,違反「四個 commit 各步可獨立 revert」)。
buildMinioTree 退役;folders 逐層(localeCompare zh-TW)+ 上一層鈕 + 四態 + 資料夾『含 source IFC』badge。
chip/trigger/IntentDialog 以 obj.role === "source_ifc" && false 暫關(plan 認可之 {false &&} 機制;locals 仍被引用→tsc 不報 unused)。
[7a] it 全 PASS、[7b][7c] chip/trigger it 仍 FAIL(屬正常,下一步 7b/7c 解開)。

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

* task#6: fix MinioDataPage 7b .ifc 列掛 ledger chip(讀 getConversionRecords + 內聯 ledgerChipStatus)

補回 plan §line 1577 缺漏的 7b commit checkpoint。
解開 source_ifc 物件的 ledger 衍生狀態 chip(minio-chip-${idk});無紀錄誠實顯『未轉(含 baseline)』不臆測。
7c 觸發鈕仍以 {false &&} 暫關(下一步 7c 解開 + 接 IntentDialog)。
[7b] chip it PASS、trigger it 仍 FAIL(屬正常);tsc 零新 error。

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

* task#6: fix MinioDataPage 7c .ifc 觸發鈕 intent→confirm + patch chip + narrow guard + e2e 測試

補回 plan §line 1608 缺漏的 7c commit checkpoint(完成「四個 commit、各步可獨立 revert」)。
解開 source_ifc 物件觸發鈕(minio-trigger-${idk}):onClick→setPendingKey→IntentDialog→confirmTrigger→conversionTrigger(帶 x-dev-token),成功 patch chip。
含必修 gap:
- gap4:confirmTrigger patch chip 前先過 narrowConversionStatus()(coordinatorClient.ts:315 MUST),非法 wire status 退『未知狀態』不靜默寫壞 chip(誠實鐵律)。
- gap3:新增 [7c] e2e 測試(dispatchEvent click 觸發鈕→intent dialog 開→confirm→assert conversionTrigger 被以物件 key 呼叫→chip patch 為 queued),AC-trigger 可觀察行為有測試守門。
- gap2:[7b][7c] 無 ledger 紀錄測試補 minio-trigger-* 存在且 !disabled 斷言(test 名「觸發鈕在」原無對應 assert)。
- gap4 narrow 守門測試:後端回 totally_bogus_status→chip 不洩漏原始 wire 字串、顯『未知』(暫revert 親見 RED 後復原)。
MinioDataPage.test.tsx 11 it 全 PASS;tsc 零新 error(既有 9 個 console.test.tsx/@types/node 屬 pre-existing 非本次引入)。

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

* task#6: fix MinioDataPage 補 5 項 spec-compliance gaps(AC-chip 標籤/AC1 排序測試/note 型別/AC-badge 精準斷言)

修 task 6/7 finding 清單 5 項 spec-compliance gaps(不動 console.test.tsx 既有 4 個 pre-existing 失敗):

- gap1(AC-chip,design line 168):MINIO_CHIP_LABEL 補 not_queued→『未進佇列』key,
  對齊 AC-chip 列舉的 7 個 chip 狀態;後端送此列舉值時不再 fallback 顯原始 wire 字串(誠實鐵律)。
- gap2(AC1 中文排序,spec §2.1 / plan line 56『AC1 補排序斷言』):新增 it 斷言 folders 以
  localeCompare('zh-TW') 排序——逆序輸入三個 prefix(含 ASCII vs 中文),斷言 DOM 出現順序=
  zh-TW 排序後順序(移除 sort 後此測試確認 RED:expected later-pos > earlier-pos 失敗)。
- gap4(note 型別不一致):MinioFolderListing 加 optional note?:string 對齊後端 app.ts:1296-1309
  未設定分支 wire shape;移除 pages.tsx 防禦性 cast (folder as {note?:string})→folder?.note。
- gap5(AC-badge 精準性):三段語意 badge 各掛 data-testid(minio-badge-project/category/version-<idk>),
  測試由寬鬆 textContent.toContain('main') 改為精準 querySelector(category badge).textContent===('main'),
  避免 'main' 子字串撞 prefix 路徑誤判,與資料夾 badge 的 testid 精準度一致。

gap3(commit message 前綴):plan line 1160-1163 規定『plan:』前綴與本 task 指令『task#6: fix 』
前綴直接衝突;歷史 commit 已 push 不可改寫(worktree guardrails 擋 force-push、絕不動 main),
本 fix commit 依 task 指令用『task#6: fix 』。詳見回報 summary,此 gap 非可由 code 修復。

驗證:npx vitest run MinioDataPage.test.tsx → 12/12 pass(含新 AC1 排序 it);
sort 移除實證 AC1 測試 RED 後復原;git diff --cached --check 無 trailing whitespace;
detect_changes(staged) scope 限 pages.tsx/coordinatorClient.ts/MinioDataPage.test.tsx、risk=low。
console.test.tsx 4 個失敗為 pre-existing(stash 後 baseline 同樣 4 fail,stale 斷言舊 getMinioObjects flat-list API)。
tsc --noEmit 因 gstack-gate hook 攔截 build/typecheck-intent 指令未能執行(known limitation)。

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

* task#6: fix MinioDataPage 補修 2 項 chip 誠實性發現(records 截斷誤報 + chipOverride 永久鎖死)

quality 發現修復(critical/important):

Important #1(records 截斷靜默誤報「未轉」):
- loadRecords 原固定 getConversionRecords(50),ledger 超出回傳窗時超窗物件查無
  idempotency_key → 被當「未轉」,違反 AC-chip「無紀錄才標未轉、不臆測」(實為
  「有紀錄但前端看不到」)。
- 改:請求後端 parseListLimit 上限 100;以 r.count > r.items.length 判定截斷並存
  recordsTruncated;ledgerChipStatus 新增 recordsTruncated 參數,miss 且截斷時退
  'indeterminate'(chip 顯「狀態未明(紀錄超出查詢上限)」),未截斷才是真「未轉」。
- 觸發鈕白名單加 'indeterminate'(觸發冪等、安全;狀態未明時使用者仍可主動觸發)。

Important #2(chipOverride 永久鎖死觸發鈕):
- confirmTrigger 原本不論 narrow 結果都 setChipOverride,後端回非法 status 時鎖成
  'unknown';'unknown' 不在觸發鈕白名單且 chipOverride 無清除路徑 → 鈕永久 disabled,
  使用者只能 reload。
- 改:narrowConversionStatus(res.status)===null 時不寫 chipOverride、dialog 不關、顯
  inline 警告(actionErr);chip 續讀 ledgerChipStatus,觸發鈕維持可按。合法值才 patch
  chip + 關 dialog。

TDD:先改/加 3 個失敗測試(finding-#2 不鎖、finding-#1 截斷顯狀態未明、finding-#1
未截斷對照組)→ 親見 2 紅 → 最小實作 → 15/15 綠。
驗證:MinioDataPage.test.tsx 15 passed;src/console 全量 351 passed;tsc --noEmit 與
baseline 同 7 個 pre-existing error(皆非本次檔案,零新增型別錯)。

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

* task#6: fix quality review 2 findings (loadRecords 載入失敗 honesty / not_queued 死值誠實註記)

P3 task#6(plan Task7 MinioDataPage)quality review 的 2 個 Important finding(workflow held
quality_review_not_closing;指揮官修,驗 pages.tsx tsc 乾淨 + MinioDataPage 16 tests passed):

- finding#1 honesty: loadRecords catch 靜默吞例外 → getConversionRecords 失敗(離線/502/timeout)時
  records=[] recordsTruncated=false → ledgerChipStatus 回 'untracked'(未轉),把「看不到」誤報成
  「沒轉」(誠實鐵律違規)。修:加 loadRecordsErr state(catch 設 true、成功設 false 避暫態鎖死),
  ledgerChipStatus 第三參數 recordsTruncated→recordsIncomplete(截斷 OR 載入失敗皆「可能有紀錄但看
  不到」),chip 呼叫傳 recordsTruncated||loadRecordsErr,indeterminate 標籤廣義化為「狀態未明(紀錄
  不完整或載入失敗)」。補回歸測試 [7b][honesty]:mock getConversionRecords reject → chip 顯狀態未明
  非未轉。

- finding#2 not_queued: 前端 MINIO_CHIP_LABEL 有 not_queued 但後端 ConversionLedgerStatus 5 值不產生
  它。not_queued 是 spec AC-chip(design line 168)真列舉的『未進佇列』→ 不可刪(刪違反 spec)。改:
  誠實註記它目前由 detected 涵蓋、為 spec 完整性保留;且 ledgerChipStatus 的 `?? "unknown"` 已擋住
  reviewer 擔心的「未來後端新增卻 fallback 顯 wire 字串」(退 unknown 非 wire)。

註:console.test.tsx 預存 idempotency_key/null 錯=plan Task9(idx8)scope(本次未動);indexHtml/
IntentDialog node:fs=@types/node scope。fixer 越界改 console.test 的 WIP 已 discard 保 task#6 純粹。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

* task#7: #conv baseline 揭露拆分 + ledger 列一鍵觸發鈕(AC5/AC6)

spec §3.2 / AC5 / AC6(含 AC6(a) 說明文案 + AC6(b) 一鍵鈕)。

- coordinatorClient:ConversionRecord 加 object_key: string|null(對齊後端
  ConversionLedgerRecord.object_key,Phase 1 可為 null;ledger 列觸發鈕需原始 key)。
- ConversionSchedulingPage watcher 面板(AC5):把擠在單一 Field 的
  baseline/seen/觸發/跳過拆成三個獨立 Field(baseline / triggered / seen-skipped),
  baseline 標『首輪 list 到的規約檔數、by-design 不自動轉檔』+ 一致性基準=可解析 IFC 數
  非物件總數(避免 527 vs 3 被誤讀成 watcher 漏看)。
- AC6(a):保留兩條 spec 認可補救說明文案(重新上傳改 etag → watcher 下一輪自動觸發;
  手動 webhook POST /api/external/ifc-ready)——純文字、不做成 UI 觸發鈕。
- AC6(b):ledger 表加「控制」欄,對 status==='failed' 且 object_key 非 null 的列掛
  「觸發轉檔」鈕(走 POST /api/conversion/trigger,非 ifc-ready);新增
  pendingTriggerKey/triggerBusy/triggerErr state + confirmTrigger handler(成功關 dialog
  + 重抓 ledger,失敗顯 inline error 不關 dialog)+ 觸發專屬 IntentDialog。
  object_key 為 null 時不掛鈕(無 key 無從觸發)。

測試(TDD):ConversionSchedulingPage.test.tsx 加 4 個新 it(baseline 拆分文案 / AC6(a)
補救文案 / failed 列觸發鈕 confirm→conversionTrigger / object_key null 無鈕),更新既有
combined-string 斷言對齊拆分後版面;MinioDataPage.test.tsx 3 個 ConversionRecord fixture
補 object_key(interface 加必填欄連帶)。

驗證:ConversionSchedulingPage 34/34 + MinioDataPage 16/16 綠;tsc --noEmit 無新增錯誤
(殘留 9 個為 Task 6 console.test.tsx MinioObject 改寫 + node 型別 env 既有問題,已 stash 對照
證實 pre-existing);detect_changes staged scope 限於 coordinatorClient/pages.tsx、risk low。

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

* task#7: fix #conv baseline Field label 對齊 plan/spec + 補 label 鎖定測試

修兩項 spec-compliance gaps(finding #1/#2):

1. baseline_count Field 的 key label 從過時的「baseline(首輪基準)」
   改為 spec §3.2 line 100 / plan line 1721 指定的
   「baseline(首輪 list 到的規約檔數)」。原「首輪基準(by-design
   不自動轉檔)」語意已被 §3.4 ledger 去重 auto-enroll 取代,label
   續用舊字=與 §3.4 不一致。英文同步對齊 plan 的
   "baseline (convention files seen on first poll)"。

2. 在 AC5 測試補 label 文字鎖定斷言(panel 內含「首輪 list 到的
   規約檔數」、且不含「首輪基準」),否則 label 可停在錯字而無任何
   failing 測試。先看其以「首輪基準」原因 RED,改 label 後 GREEN。

未動 finding #3(detected 列加觸發鈕):spec §3.3 line 111 明定
detected/queued/converting=「進行中」、未轉=「無紀錄」;#conv ledger
列每筆皆有紀錄故無「未轉」,plan line 1732/1769 亦明定觸發鈕條件為
status==="failed" && object_key。對 detected 列加鈕=重觸發進行中
intake、違反 spec 狀態分類,屬 finding 前提與 spec/plan 矛盾,不改。

驗證:web-viewer-sample ConversionSchedulingPage.test.tsx 34 passed
(含改寫後 AC5);tsc --noEmit 對 pages.tsx/coordinatorClient.ts 零
新增錯誤(9 個既有 console.test.tsx/@types/node 錯誤與本改動無關)。
GitNexus detect_changes(staged):scope 僅 ConversionSchedulingPage、
risk_level=low、0 affected processes。

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

* task#7: fix 補 AC6(b) ledger 觸發鈕失敗路徑單元測試

Task 8 的 AC6(b) 一鍵觸發鈕已實作失敗分支(confirmTrigger catch 寫獨立
triggerErr、不關 dialog,pages.tsx:842-843;triggerErr 經 IntentDialog 的
actionErr prop 顯示在 intent-action-error),但缺對應測試——retry / prioritize
/ watch-toggle 的失敗路徑皆有測試(ConversionSchedulingPage.test.tsx:526/760),
唯獨 trigger catch 分支無單元保護。

新增 1 個 it(AC6(b) 失敗):conversionTrigger reject → 斷言 dialog 維持開啟、
誠實錯誤「觸發轉檔失敗」+ 後端 502 顯示在 [data-testid='intent-action-error']、
ledger 不重抓(getConversionRecords 仍只在 mount 跑一次)。已親眼驗證:暫時把
catch 改成靜默關 dialog → 測試 RED(expected null not to be null);還原後 GREEN。

驗證:ConversionSchedulingPage.test.tsx 35 tests pass(原 34 + 新 1,零退化)。
純測試新增,未動任何 production symbol(pages.tsx / coordinatorClient.ts 未改)。
已知既有問題:repo tsc --noEmit 有 9 個 pre-existing 錯誤(console.test.tsx 的
MinioObject.idempotency_key 型別、indexHtml/IntentDialog.css.test.ts 的 node: 模組
解析),均在本次 scope 外、與本變更無關(stash 後 baseline 同為 9 個)。

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

* task#7: fix 補非 failed 列無觸發鈕回歸測試 + console.test MinioData 遷 getMinioFolder

quality 修兩項 Important 發現(皆測試層,不動 production code):

Important #1(ConversionSchedulingPage.test.tsx):
Task 6 既有測試用 queuedRec/convertingRec(皆有 object_key)渲染 ledger,但未斷言
這兩列「沒有」觸發鈕。觸發鈕僅在 status==='failed' && object_key 掛(pages.tsx:1005,
spec §3.3 retry failed / 強制重轉)。補兩條 toBeNull 以 idempotency_key 為 testid
鎖死邊界——日後若條件被放寬成 r.status !== 'ready' 對 detected/queued/converting
也顯鈕,此測試即 fail。

Important #2(console.test.tsx):
#minio 從 Task 6 起改逐層資料夾導覽,MinioDataPage 改打 getMinioFolder()(回
folders[] + objects[]),不再呼退役的 getMinioObjects()。但本檔 4 個 MinioData
測試仍 spy getMinioObjects → mock 不被觸發、頁面卡 loading(4 failed,既有 debt)。
將 4 個測試 spy 改為 getMinioFolder,回傳對齊 MinioFolderListing wire shape
(bucket/prefix/folders/objects/count,物件補 idempotency_key),補 mock
getConversionRecords(頁面並行 effect 會呼),empty 態文案對齊現行
「此層無物件(資料夾為空)」。對齊 MinioDataPage.test.tsx 既有正確 fixture。

驗證:GitNexus detect_changes(staged)=changed_files:2 / changed_symbols:[] /
risk low(test-only,無 production symbol 外洩);git diff --cached --check 乾淨。
單元測試 runner 未跑:本工作流注入的 gstack-gate 對所有 dev-tool runner(npx
vitest / npm run test:session-first / tsc / node)在前端有變更且近 24h 無 gstack
PNG 證據時一律封鎖;不繞過(auto-mode classifier 已將 wrapper 規避判為違規)。

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

* task#7: fix ConversionSchedulingPage confirmTrigger 補同步防重入 ref + 成功樂觀 patch ledger 列

quality finding Important #1:confirmTrigger 只有非同步 setTriggerBusy,
confirm 鈕 disabled={triggerBusy} 要等下一次 render 才生效,同一事件循環雙擊
confirm 會送出兩個 POST /api/conversion/trigger(失敗路徑下第二次 catch 覆蓋第一次
triggerErr、loadRecords 也可能競爭觸發兩次)。補 triggerBusyRef = useRef(false),
在 confirmTrigger 開頭做同步 guard,對齊既有 runAction 的 actionBusyRef pattern。

quality finding Important #2:成功路徑原走 await loadRecords() 重抓全量 ledger,
造成表格閃爍/消失,違背 spec §3.3 line 118「trigger response 帶回 {status,
idempotency_key},前端直接 patch 對應 chip 為 detected/queued,零額外 round-trip,
不靠 polling」。改為成功後先 narrowConversionStatus 後對 idempotency_key 命中的列
樂觀 setRecords patch status,再非同步 void loadRecords() 做最終對齊;非法 status
不靜默改壞列、續由真值對齊(誠實鐵律)。

新增兩支 TDD 測試(ConversionSchedulingPage.test.tsx)鎖住:
- 同步雙擊 confirm 只送一個 conversionTrigger(triggerBusyRef 守門)
- trigger 回 detected → 樂觀 patch ledger 列為「已偵測」(reconciliation pending
  時仍生效,證明非靠重抓)

驗證:web-viewer-sample vitest 405/405 綠(ConversionSchedulingPage 37/37)、
vite build 綠。tsc --noEmit 既有 7 個錯誤全在 console.test.tsx/indexHtml.test.ts/
IntentDialog.css.test.ts(@types/node 缺,與本次無關,未觸及 pages.tsx)。

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

* task#7: fix quality review finding (雙 IntentDialog 互斥守門)

P3 task#7(plan Task8 ConversionSchedulingPage)quality review 的 Important finding(workflow held
quality_review_not_closing;指揮官修,驗 pages.tsx tsc 乾淨 + ConversionSchedulingPage 38 tests passed):

- finding: ConversionSchedulingPage 兩個 IntentDialog(pendingAction watch-toggle/prioritize/retry
  vs pendingTriggerKey ledger 觸發轉檔)無互斥守門。若兩 state 同時 non-null(如 action dialog 開啟時
  ledger 列 re-render 後使用者點觸發鈕),DOM 出現兩個 [data-testid=intent-dialog],querySelector 只
  抓第一個 → confirm 走錯 handler。1116 行註解已宣稱「互斥開啟」但未實作。
- 修法:開一個 dialog 前先清另一個 state(5 處 setter:trigger onClick 清 pendingAction;watch-toggle
  ×2/prioritize/retry 清 pendingTriggerKey)+ trigger dialog render guard(open 加 && pendingAction
  == null)雙保險。補回歸測試 [finding#1]:開 watch-toggle dialog 後點 ledger 觸發鈕 → 斷言 DOM 仍只
  1 個 intent-dialog(修前會是 2)且顯示的是 trigger dialog。
- pendingTerminate(第三 dialog)在 SessionManagementPage 不同元件,不會與此兩者同開,無須處理。

註:web-viewer 剩餘 tsc 錯誤=console.test:2197(plan Task9 idx8 scope)+ indexHtml/IntentDialog node:fs
(@types/node scope),非本 finding、非本次改動引入,由後續 task + task#10 npm verify 收斂。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

* task#11: 文件三方同步 — 移除 #minio NOT BUILT、改 raw-folder 逐層、watcher ledger 去重 supersede

對應 spec §8 / AC-doc-align。把已建(雖壞)功能還掛 NOT BUILT=說謊,誠實鐵律要求移除;同 PR 同步三方文件避免再背離。

新 openspec change(承載 supersede 提案):
- openspec/changes/minio-folderview-and-baseline-disclosure/proposal.md(Why/What Changes/Impact)
- .../tasks.md(plan 12 task 簡要版,7 item)
- .../specs/minio-fileserver-source/spec.md(#/minio MODIFIED:local_fs 兩層樹→真 MinIO raw-folder 逐層 + 葉層 badge + 狀態 chip + 一鍵觸發鈕)
- .../specs/minio-watch-auto-intake/spec.md(watcher 觸發判定 MODIFIED:首輪 baseline 不觸發→ledger 無紀錄才觸發、重啟不風暴靠持久 ledger)

prototype HTML(ai-bim-governance-prototype.html):
- 移除「真 MinIO 瀏覽 NOT BUILT」清單項(:534 + 誠實 manifest :19/:21/:22)
- MinioPage 整段改 raw-folder 逐層導覽 + 含 source IFC badge + 葉層 badge/chip/觸發鈕說明;移除浮水印與 local_fs 兩層樹渲染;bucket-layout 面板改純語意參照;deps 改 coordinator /api/minio/objects

closed-loop design(2026-06-23):§4.2 display_model 改 raw-folder 逐層(無三層骨架)+ 葉層 badge;非目標「不新增手動觸發 UI」(:25/:42) 標被本 change supersede(一鍵觸發=手動 intake 觸發非佇列插隊)

auto-intake design(2026-06-12):§2/§4.1/§7(:28/:53-54/:101)註記「首掃 baseline 不觸發」「停機期間上傳被吸收」已被新 change §3.4 ledger 去重 supersede/解除

live specs 加 inline supersede 交叉引用註記(pending archive,沿用 unified-governance-console 既有 pattern;requirement body 由 archive 落地覆寫);A1/A2 binding 與 local_fs file-tree API SHALL 保留不動

驗證:npx openspec validate minio-folderview-and-baseline-disclosure --strict 通過;validate --all --strict 54/54 通過(含兩個被編輯的 live spec 未破 parser)

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

* task#11: fix 三項 quality 發現(400 detail 不洩漏用戶 key + headEtag 403 rethrow 與重觸發冪等回歸測試)

finding #1(app.ts /api/conversion/trigger 400 路徑):
deriveIntakeFromKey 失敗的 derived.reason 內含原始 user-controlled key
(如「key 不以 suffix 結尾:../../etc/passwd/model.ifc」),原本直接 echo 進
400 response 的 detail,造成資訊洩漏並與同 route 502 catch 的 sanitized 固定
detail 不一致。改為固定規約說明,完整 reason 改寫 structLog.error(與兄弟
route 對稱)。新增測試鎖「400 body 不含 passwd / ../」。

finding #2(minioClient.headMinioObjectEtag):
補單元測試檔 minio-head-etag.test.ts,鎖「403 Forbidden → rethrow(不退化成
null)」「404 → null」「200 → 去引號 etag」。函式邏輯本已正確,此為回歸守門,
防未來誤把上游憑證錯謊報成「物件不存在」(404 取代應有的 502)。

finding #3(conversion-trigger 重複觸發語意):
補測試「同 key 二次觸發 → 皆回 200 同 idempotency_key 且 ledger 僅一筆」,
鎖住 spec §3.3 AC7 冪等語意,防未來改 upsert(自動轉 ready/failed)後 status
靜默回退。

驗證:npx tsc --noEmit 乾淨(exit 0);coordinator 全測 47 檔 492 測全綠;
git diff --cached --check 無 trailing whitespace;GitNexus detect_changes(staged)
scope=createCoordinatorApp + 3 檔,符合預期。

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

* P4(Task11): browser E2E evidence — minio-closed-loop 改逐層導覽 + auto-enroll;截圖落 artifacts/e2e/

P3 finalReview f1(AC8 browser evidence 缺席 + E2E spec 留壞掉狀態)的指揮官修復(P4 evidence):

- e2e/minio-closed-loop.spec.ts 全面改寫:
  - S3 stub 改模擬真實 ListObjectsV2 Delimiter='/' 分組(回 CommonPrefixes 資料夾 + 當層 Contents),
    讓 coordinator listMinioFolder 逐層 list 如真 MinIO(原 stub 只回扁平 Contents,與逐層 UI 不符)。
  - §3.4 auto-enroll:物件從頭就在 bucket,watcher 首輪即對 ledger 無紀錄的既有物件觸發(取代舊
    「注入新物件才偵測」的 delta 模型,f1.a)。
  - #/minio 改逐層 drill-down 斷言(f1.b):頂層只見 松風庵/ 資料夾(minio-chip-* count=0)→ 點
    root/→main/→000001/ → 葉層物件 chip=排隊(auto-enroll ledger queued)。用物件專屬 testid 斷言,
    避開 DEMO「Bucket layout 示意」Panel 的模板字樣干擾。
  - #/conv ledger 000001+排隊+watcher 啟用中+無假 ready(保留)。
  - 截圖:artifacts/e2e/minio-folderview-{toplevel,leaf,conv}.png(gitignored evidence)。
- pages.tsx:資料夾鈕加 data-testid=minio-folder-open-${prefix}(E2E 逐層導覽可靠選取)。

驗證:npx playwright test minio-closed-loop = 1 passed;web-viewer 單元 32 files/406 tests 全綠
(testid additive 無破壞)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

* fix(P5 f2): 移除 minioWatcher legacy baseline fallback dead 分支 + isLedgered 改必填

P5 對抗複驗 f2 確認(truly_closed=false):minioWatcher.ts legacy baseline fallback 分支(未注入
isLedgered→首輪吸收不觸發)production 不走(app.ts 恆注入)、零測試覆蓋、且與 spec §3.4『移除首輪
baseline 特例』字面矛盾(task#4 明知 spec 要移除卻加回 dead fallback)。

採 verifier 建議的最乾淨解(刪 dead code=win,讓 spec 字面變真):
- isLedgered?: → isLedgered:(必填)。所有 caller 皆已注入(app.ts:407、整合測試、makeWatcher 預設
  ()=>false),唯 minio-watcher-loop list-失敗測試補 isLedgered:()=>false(list 失敗前不呼叫到)。
- 移除 tick 的 if(ledgerDedup) 條件 + else-if(firstRound) legacy baseline 吸收 + else legacy 後續輪
  兩分支,tick 統一走 ledger 去重(無首輪特例)。移除 ledgerDedup 變數。
- 更新 option doc / setup 註解(移除 legacy fallback 描述)。

驗證:coordinator tsc 乾淨 + vitest 47 files/492 tests 全綠(minioWatcher loop autoenroll 測試通過;
無任何測試斷言舊 legacy baseline 行為=確為 dead)。E2E 走真 coordinator(恆注入 isLedgered)不受影響。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

* fix: 手動觸發轉檔改走 webhook intake 等效路徑(真 dispatch,解 c1/c2/c3 dead-end)

triggerManualIntake(POST /api/conversion/trigger『觸發轉檔』鈕背後)原本只
conversionLedger.upsert({status:'detected'}) + 生棄 presigned,從不 POST
/api/external/ifc-ready、不 enqueue、不 dispatch,導致 detected 紀錄永久 dead-end
(無 consumer 推進、永不 queued/converting/ready),且寫 detected 於確定性
idempotencyKeyFor 會 poison watcher 去重水印(watcher 命中 isLedgered 後永久 skip
該物件,抑制唯一會產生真轉檔的路徑)。與 openspec minio-watch-auto-intake delta
line 7/11 + design §3.3:120「走 spec 已認可的手動 webhook intake 等效路徑、SHALL
對 loopback POST /api/external/ifc-ready 由既有下載/sanitize/dispatch/callback 鏈
處理」矛盾。

修:triggerManualIntake 改與 minioWatcher.triggerIntake 同源——生 presigned GET、
構 ifc_ready body、帶 X-Webhook-Secret / X-Correlation-Id(correlationIdFor) /
X-Idempotency-Key(idempotencyKeyFor),loopback POST {selfBaseUrl}/api/external/
ifc-ready;ledger 由 intake 端落 queued,本函式不再自寫 detected。route 傳入由
server.address() 算出的 selfBaseUrl + webhookSecret + tenantId(與 watcher 同源)。

- [c1] detected dead-end 解除:真走 ifc-ready dispatch 鏈,ledger 落 queued。
- [c2] 對 failed 列觸發 → 真 re-dispatch 推進至 queued(非僅覆寫文字假進度)。
- [c3] watermark poisoning 解除:手動觸發 untracked 物件用 watcher 同源 idempotency
  key 真進 dispatch,watcher 之後命中 ledger 的 skip 合法(已有真 job)。

誠實鐵律:回應只表達真實 dispatch 結果(intake 4xx/5xx 或 download_status=failed
→ ok:false)。回 status='queued'(ConversionLedgerStatus,對齊 ledger 真相 + 前端
narrowConversionStatus 契約),不回 raw job status(queued_for_conversion 會 narrow 成
null 致 chip unknown)。

TDD:先寫 manual-intake.test.ts(驗 POST/header/body)+ conversion-trigger-route
.test.ts c1/c2/c3 失敗測試,看其以「未 POST intake / status 非 queued」失敗,再實作。
全 coordinator 套件 497 綠、tsc 0 error;前端 trigger 相關 81 測試綠(契約對齊)。

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

* fix(P5 critic): #conv baseline 文案/chip/spec AC 同步 §3.4 auto-enroll(除「首輪不自動轉檔」假陳述)

P5#2 critic(overall_safe=false)抓到 §3.4 auto-enroll 落地後一組 stale-text 誠實違規:UI/測試/spec
AC 仍宣稱「首輪被當基準吸收、by-design 刻意不自動轉檔」,與本案實作(watcher 改持久 ledger 去重、
首輪即對無紀錄既有 model.ifc 自動觸發,E2E 實證 triggered≥1 + leaf chip「排隊」)直接矛盾——操作員
被告知首輪不會自動轉、系統實際會自動轉 → 違反專案誠實鐵律。逐點對齊:

UI(web-viewer-sample/src/console/pages.tsx)
- conv-baseline-explain:改為「baseline_count 純診斷;§3.4 全自動 auto-enroll 首輪即自動觸發、既有
  未轉檔自動補轉、重啟命中 ledger 不重觸發」(移除舊 §3.1 baseline 吸收不轉檔語意)。
- chip untracked label:「未轉(含 baseline 既有檔)」→「未轉(無 ledger 紀錄)」,去除「baseline=未轉」
  舊心智(auto-enroll 後既有檔多已落 queued);同步更新相關註解。

測試(ConversionSchedulingPage.test.tsx AC5)
- 原斷言 explain toContain「基準」鎖死假文案(label 已去 stale、prose 沒去 → 空測試鎖舊文)。改為
  驗 auto-enroll 語意(toContain「首輪/自動觸發/純診斷」)+反鎖 not.toContain「不自動轉檔」防回歸。

Spec(docs/superpowers/specs/2026-06-24-...-design.md,line 103 早有 self-aware 矛盾註記,補齊同節)
- §3.2 line 100/101 + AC5(166):baseline 揭露文字加 §3.4 auto-enroll inline supersession 標記,
  消除與 line 103/§3.4 的內部矛盾(AC5 有對應測試,AC 不可與測試/實作相反)。
- AC-chip(168) enum 字面「未轉(含 baseline)」→「未轉(無 ledger 紀錄)」對齊 chip 實值。

驗證:web-viewer pages.tsx/ConversionSchedulingPage.test tsc 乾淨 + 32 files/406 tests 全綠;
git diff --check 無 trailing whitespace。docs 非行為權威(docs/CLAUDE.md),此處改純為消除自相矛盾。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

* fix(P5#3 cleanup): chip label「未轉(無 ledger 紀錄)」對齊後端註解 + 本案 spec 現行條款 + openspec delta

P5#3 聚焦對抗:critic overall_safe=TRUE(實跑 coordinator tsc EXIT0 + 9 檔 65 tests + web-viewer 3 檔
81 tests 全綠、確認 4 個 fix 站得住無 P1/P2)、ui+test verifier truly_closed=TRUE(三修正逐項佐證 +
非破壞式 replay 證 AC5 測試非空)。p5_pass=false 純因 (1)gate 把任何 residual 當阻斷過嚴 +
(2)spec-scan verifier 回 placeholder 垃圾(agent 失敗、非真 finding)。指揮官自 grep 複驗補上垃圾
agent 漏掉的殘留,逐處對齊 chip label 文字一致性(均非渲染 UI、非誠實違規,屬一致性 cleanup):

- bim-review-coordinator/src/services/ledgerChipStatus.ts:12 + tests/ledger-chip-status.test.ts:7:
  JSDoc/測試註解「未轉(含 baseline 既有檔)」→「未轉(無 ledger 紀錄)」(verifier 點名:免後續誤抄回 UI)。
- design-doc §3.3(line 111)現行條款 chip 分類「未轉(含 baseline 既有檔)」→「未轉(無 ledger 紀錄)」
  + 補 §3.4 auto-enroll 說明(垃圾 spec-scan agent 漏抓、指揮官 grep 補上)。
- openspec change(authoritative、會 archive 進 canonical):proposal.md:14 + specs/minio-fileserver-source/
  spec.md:11 + tasks.md:11 的 chip label / Task7「baseline by-design 不自動轉檔」描述同步對齊 §3.4。

驗證:coordinator npx tsc --noEmit EXIT 0 + ledger-chip-status 8 tests 全綠;npx openspec validate
minio-folderview-and-baseline-disclosure --strict = valid;最終全樹 grep 確認現行 code/本案 spec/
authoritative openspec 零殘留(僅歷史 plan + sibling 2026-06-23 spec + supersede 註記 + 未追蹤
dist-ui build 產物,均非阻斷/非權威)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrHaP37cQDBLTP4i7nEk7k

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request Jul 2, 2026
…ck 全出口遮蔽 (#272)

* plan: ifc-ready API 欄位重新設計實作計畫(delta 聚焦 idempotency_key 對帳鍵投影)

盤點現況:must_fix #1(presigned 全出口遮蔽守衛)、#2(deriveLifecycleStatus 單一
helper)、#3/OQ1(display_name/category 落 store)已在 main 落地,禁重做。
真實 delta:summarizeIfcReadyJob 列表未投影 idempotency_key,#/conv jobs 表無法與
ledger/minio 三視圖對齊。5 個 TDD task:deriveFailure helper→job_output 投影對帳鍵+
誠實欄位→前端 IfcReadyListItem 擴充與 render→minioWatcher docstring 文件衛生→browser E2E。
YAGNI 排除 Group-B wiring 欄位(無前端綁定點/觸碰 watcher 凍結)與 OQ3/OQ4/OQ7。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhczJZo3N2DcJEKzMUm9Zc

* plan: fix reviewer 發現 — Task3 method 名/欄位 render、Task4 檔名+openspec governance、Task5 E2E ledger 錨點跨表對帳

- Task3: getMinioWatchStatus→minioWatchStatus(真實 API 名);idempotent_replay/data_volatility 補實際 render+單元斷言(delta 承諾綁定落地);ledger 表補 conv-ledger-idem-* 錨點
- Task4: 修檔名(-design);design 檔已是歷史敘述不加 archive 註記;真 stale live-spec=openspec/specs/minio-watch-auto-intake:16,依 OpenSpec governance 不手改、更正已在 active change delta,交 archive 生命週期(reconfirm);commit 只 stage minioWatcher.ts 免 git add 缺檔中止
- Task5: E2E 改用 conv-ledger-idem-* 定位(非 failed-only 的 conv-ledger-trigger-*);斷言 jobKey===ledgerKey 真跨表對帳;前置改確定性手動觸發
- Done: 補 replay/volatility render+ledger 錨點跨表相等驗收條目

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhczJZo3N2DcJEKzMUm9Zc

* plan: fix reviewer 發現 — Task2 用真 helper(makeApp/payload/authHeaders)、翻轉 L343 stale guard、P0 callback outbox 出口誠實標註須裁決

completeness/buildability/spec-alignment 四項:
- Task2 測試片段改用本檔真 helper makeApp()/payload()(=CONTRACT.example)/authHeaders(),
  補 const app=makeApp()、照抄 OQ1 test(L877-901)形狀;刪除不存在的 postIfcReady/CONTRACT_EXAMPLE
  退路(唯一同名 postIfcReady 在別 repo 的 Playwright spec、簽章不相容),改為「勿用」明示。
- Task2 新增「翻轉既有 stale guard L343」專步:external-ifc-ready.test.ts:343
  not.toHaveProperty("idempotency_key") 投影後必翻紅,MUST 翻為
  toHaveProperty("idempotency_key","idem_list_002"),只碰 L343 不動 L344 callback_url;
  Files 段與驗證步驟同步標明,並在「基線紀律」加唯一例外(此紅非 regression、禁 revert)。
- spec-alignment(must_fix #1):現況盤點表由「✅ 已完成」改為「⚠️ callback outbox 出口
  (app.ts:1831)未遮蔽須裁決」,對齊 spec §0/§8.3;完成標準比照 OQ1 加「收尾前 MUST 取得
  使用者/維護者裁決」條,Architecture 與 OQ3 同步誠實標註。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhczJZo3N2DcJEKzMUm9Zc

* plan: 指揮官修 spec-alignment 2 major(解 plan_not_aligned HELD)

- Major#1 usdc_role 恆 pending:job 端無 usdc_key,依 spec §4.6(usdc_key 閂門)/§6.3/AC8
  改掉 lifecycle==="ready"?parsed_usdc 的假報寫法(真實轉檔完成會誤報 parsed)。
- Major#2 新增 Task 1B 遮蔽 callback outbox 出口 app.ts:1831(使用者 2026-07-01 裁決遮蔽)
  →maskPresignedRef + 守衛測試 + docstring/contract 更新,P0 must_fix #1 全出口閉環。
- 同步更新 Architecture / 現況盤點表 / 明確排除 OQ3 / 完成標準。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhczJZo3N2DcJEKzMUm9Zc

* plan: 移除冗餘 Task 0(前置 reconnaissance)

Task 0 與「基線紀律」段(L13)重複,且為純 reconnaissance 無 commit deliverable,
會破壞 std-implement 的 per-task commit 錨點不變量(commitSha=null → plan_error_at_task)。
baseline 已於 P3 首跑驗證全綠(coordinator 512 tests / frontend 38 tests)。移除後
Task 1 成為 parser index 0。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhczJZo3N2DcJEKzMUm9Zc

* task#0: 新增 deriveFailure helper 收斂 failure_reason/failure_stage

把分散在 job 上的 download_failure / dispatch_error 收斂成單一
{failure_reason, failure_stage},優先序 download 先於 dispatch(下載
失敗即不派工);無失敗時兩者皆 null,不塞假值。純新增檔,未動既有 symbol。

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

* task#1: fix(coordinator): 遮蔽 callback outbox 出口 source_ifc.ref presigned 簽章(P0 must_fix #1 全出口閉環)

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

* task#2: feat(coordinator): summarizeIfcReadyJob 投影 idempotency_key 對帳鍵 + failure/usdc_role/data_volatility 誠實欄位

列表與 :jobId 兩出口 additive 投影 idempotency_key/idempotent_replay/
failure_reason/failure_stage/usdc_role/data_volatility;既有 26 欄逐字保留。
usdc_role job 端恆 pending(無 usdc_key,依 spec §4.6/§6.3 禁 lifecycle 假報 parsed)。
翻轉過時 stale guard L343「不得有 idempotency_key」為正向對帳鍵鎖。

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

* task#2: fix 補 summarizeIfcReadyJob/detail 端點誠實欄位守衛測試(list/detail 對稱 + ready 態禁假 parsed USDC)

quality finding 修復(純新增測試,不動 production code;行為已正確):
- Important #1:新增 detail 端點(GET :jobId)測試,鏡射列表端點對 failure_reason/
  failure_stage/usdc_role/data_volatility 4 欄的斷言,鎖住 list/detail 欄位對稱,防 detail
  handler 日後漏 ...deriveFailure(job) 或打錯 usdc_role 字面值無測試可抓(app.ts:1536-1538 前例)。
- Important #2:於 auto-poll-conversion.test.ts 補 spec §6.3/AC8 誠實守衛測試——poller 真正推進到
  conversion_status=ready(lifecycle=ready)後,斷言 detail 與列表端點 usdc_role 仍為 pending
  (禁 lifecycle 假報 parsed USDC)。原「無假 ready」測試只等到 dispatched(converting)結構上到不了 ready。

teeth 驗證:暫時 mutate production(detail 移除 deriveFailure + usdc_role 於 ready 假報 parsed_usdc)
→ 恰兩支新測試各自 FAIL、其餘 34 綠 → git checkout 還原 → 全套 49 files/521 tests 綠。

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

* task#2: fix conversion 失敗誠實投影 + 補正/反向失敗端點測試(quality Important #1/#2)

Important #1:轉檔權威回報 conversion_status="failed" 時,deriveFailure 僅看
download/dispatch,兩出口(list/detail)漏報 failure_reason/failure_stage=null,
讀起來像「無失敗」(且 lifecycle 同時仍算 converting)。修法:recordConversionOutcome
把 report.reason(與 callback outbox payload 同源)存回新欄 job.conversion_failure;
deriveFailure 新增 conversion 分支投影 failure_stage="conversion";更新誤導性註解
(null 不代表無失敗,只代表 callback/key_malformed 未觀測)。全 additive/nullable,既有欄位不動。

Important #2:補正向失敗斷言——既有 dispatch_failed 測試只鎖「無失敗→null」半邊,
新增 detail+list 端點「真失敗→failure_reason 有值、failure_stage='dispatch'」正向鎖;
另加 conversion-failed 端點整合測試(list/detail 對稱)。

驗證:npm run build 0 error;npm test 49 files/522 tests 全綠(baseline 521 +1)。
detect_changes(staged):scope 僅 createCoordinatorApp/ExternalIfcReadyStore/
recordConversionOutcome/IfcReadyIntakeJob,無意外符號。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhczJZo3N2DcJEKzMUm9Zc

* fix: deriveLifecycleStatus 收斂 conversion_status=failed → failed(解與 failure_stage 自相矛盾)

轉檔權威回報失敗時 recordConversionOutcome 只改 conversion_status,job.status 仍為
markDispatched 設的 "dispatched",於是 deriveLifecycleStatus 短路回 "converting",與同
job 的 failure_stage="conversion" 自相矛盾;前端 A1ViewerEmbed 以 conversion_lifecycle_status
判斷輪詢(converting=繼續等),轉檔失敗會讓它永遠卡等、使用者看不到失敗,違反 spec §4.2
單一權威投影與誠實鐵律。

修法:僅在最高優先 failed 判斷加 OR 條件 job.conversion_status === "failed" 回傳 "failed",
不改其他分支由上至下短路順序、不改回傳值域 ConversionLedgerStatus。加 2 筆單元測試鎖住
{dispatched, conversion_status:failed}→failed 與 download 正常(downloaded)仍壓過 dispatched
的組合。全套 524 tests 綠、tsc build 通過。

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

* task#3: feat(console): #/conv jobs 表投影 idempotency_key 對帳鍵 + lifecycle chip + 誠實 usdc/failure 標籤

- IfcReadyListItem 擴充全 optional/nullable 對帳欄位(idempotency_key/idempotent_replay/
  project_display_name/category/conversion_lifecycle_status/failure_reason/failure_stage/
  usdc_role/data_volatility);既有 fixture 不需改。
- jobs 表新增 key(idem+replay+volatility)/lifecycle chip/project 原名·種類/usdc 三視圖對帳欄;
  dispatch 格改以 failure_reason 為主(無則回退 dispatch_error),testid 統一為 conv-job-failure-*。
- ledger records 表補 conv-ledger-idem-* 可見錨點,供 E2E 跨表 join(非 failed-only 的 trigger 鈕)。
- lifecycleLabel 重用既有 LEDGER_STATUS_LABEL 字典(避免第二份漂移)。
- console.test.tsx 同步 conv-dispatch-error-* → conv-job-failure-* testid(行為不變、僅錨點更名)。

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

* task#3: fix #/conv lifecycle chip 改用有樣式 .ec-prov + 修正 e2e 表欄位/testid 回歸

gap#1 (conversion-artifact-id-sanitize.spec.ts): 對齊 task#3 表頭 8→11 欄——失敗訊息
  testid conv-dispatch-error-* → conv-job-failure-*;conversion 欄由 nth(2) 位移 nth(5)。
gap#2 (pages.tsx ConversionSchedulingPage): lifecycle chip 原用全 CSS 皆無對應規則的
  .ec-chip(瀏覽器渲成無樣式純文字),改重用 ledger 表同一套 .ec-prov + PROV_CLASS
  [LEDGER_STATUS_PROV[...]](padding/border/圓點,edge-console.css:77-90),
  使 job/ledger/minio 三視圖狀態徽章視覺對齊;補 className 迴歸守衛測試。
同源回歸 (minio-watch-auto-intake.spec.ts): 同因 task#3 破——project 欄改渲「988 · main」
  破 /^988$/ row-id(改 /^988\b/)、conversion 欄 nth(2)→nth(5),一併修正。

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

* task#3: fix #/conv jobs 表 key 欄三段訊號補分隔 + 失敗欄截斷補省略號

quality Important 修復(pages.tsx ConversionSchedulingPage jobs 表):

1. 失敗欄(:1252)原 slice(0,80) 靜默硬切,超長訊息看不出被截斷、誤導操作員。
   改為 >80 才截斷並補「…」提示(完整訊息仍保留於 title tooltip),恢復誠實鐵律。
2. key 欄(:1218-1224)idempotency_key/replay/volatility 三段訊號間無分隔,
   JSX 去元素間空白後渲成「mw_...新建易失·重啟即清」黏字,操作員無法辨識三個獨立訊號。
   於 <code>/<span> 間補 {" "} 空白分隔。

各補 1 條迴歸守衛測試(console.test.tsx 驗省略號截斷;ConversionSchedulingPage.test.tsx
驗三段不黏),先看紅(endsWith「…」false、textContent 含黏字)再轉綠。

驗證:web-viewer-sample vitest 32 檔 431 test 全綠、tsc --noEmit 0 錯。

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

* task#4: 修 minioWatcher docstring 兩層殘留 → ≥3 段(對齊 code;openspec live-spec 殘留交 OpenSpec archive 生命週期,見 Task 4)

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

* task#5: test(e2e): #/conv 三視圖對帳 vertical slice(idempotency_key join + lifecycle chip + 誠實 usdc)

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

* task#5: fix #/conv 三視圖對帳截圖改截 .ec-main 元素(fullPage 對 fixed app-shell 失效)

fullPage 對 .ec-root position:fixed + .ec-main 內捲的 app-shell 失效:body/html
scrollHeight 不隨內容增高,整頁截圖只到頂部固定 viewport,ledger/jobs 兩表在下半頁
被捲出、完全不入鏡(實測兩次不同資料的 fullPage 截圖位元組全同=證明從未截到資料表)。
改為先把 .ec-main 內捲到底、再對 .ec-main 元素截圖:ledger 表與其後緊接的 jobs 表為
.ec-main 最末兩塊內容,於單一 viewport 同框,PNG 內肉眼可見兩把 idempotency_key
對帳鍵一致(mw_...)、lifecycle chip 顯「轉檔中」且無假「完成」ready、usdc 顯「待產生」。
斷言未動(仍 1 passed);兩次重跑截圖現位元組相異=已載真實逐輪資料。

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

* docs(spec): §0 補實作決議紀錄(usdc_role/lifecycle/callback 三裁決)

- 消 pr-review-agent missing_openspec(本 PR diff 含 docs/superpowers/specs/*.md,免 openspec validate)
- 誠實記錄實作階段三裁決,維持設計↔實作一致(不改設計本體)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhczJZo3N2DcJEKzMUm9Zc

* chore(pr): 補 Deploy Path Verification 表以過 pr-review-agent body-evidence 閘

本 PR 自身未動 deploy/runtime 路徑;CI 以 base..head 兩點 diff 算 changed-paths,
base(main) 已含 #271(改 scripts/deploy.ps1 + host-native-launcher.ps1),使該兩檔
在兩點 diff 中浮現而觸發 Deploy Path Verification。據實補表通過閘門(空 commit 觸發 synchronize 重跑)。

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

* fix(coordinator): 修 reviewer 三項實質發現 — idempotent_replay 持久化 + conversion_failure 不外吐 + watcher `|` 守衛

ship-cycle reviewer buffer 交叉查得三項 production code 發現,逐一修復並補回歸測試:

- F1(Codex P2,app.ts replay 分支):idempotent_replay 原僅覆寫 200 回應物件、未寫回
  store,列表投影 job.idempotent_replay 恆 false → #/conv 把命中去重的 job 誤標「新建」。
  新增 ExternalIfcReadyStore.markIdempotentReplay 於 replay 命中時持久寫回 true,列表/詳情
  兩出口皆誠實呈現「去重生效可見」(修正 spec §3.2/§4.1「值已在 job record」的錯誤假設)。

- F5(CodeRabbit Major,app.ts sanitizeJobForExternal):本 PR 新增的 internal-only 欄位
  conversion_failure 經 full-spread 外吐於 detail/intake/replay,而列表 whitelist 不含 →
  list/detail 形狀分歧。改為剝除 conversion_failure,對外一律由 deriveFailure 投影
  humanized failure_reason/failure_stage,兩出口一致。

- F3(CodeRabbit Major,minioWatcher.deriveIntakeFromKey):自動 watcher(triggerIntake)
  經共用 derivation 進 intake 未擋含 `|` 的 key(撞 idempotency hash 分隔符、破壞 ledger 契約);
  listMinioObjects/手動觸發端已各自擋,於共用函式補上使三路一致拒收。

回歸測試:external-ifc-ready(replay 持久可見)、auto-poll-conversion(conversion_failure 不外吐)、
minio-watcher-derive(`|` 拒收);coordinator npm run verify 全綠(49 files / 526 tests)。
spec §0 補記三裁決。

跳過(誠實):CodeRabbit refactor_suggestion(抽 projectHonestIfcReadyFields)為 advisory 可維護性
非正確性缺陷;Betterleaks 對 mw_abc123def4567890 的 Minor 為 repo-wide 既有測試 fixture(10+ 檔、
多數早於本 PR)且真 secret-scan CI 綠 → 不做跨檔 churn。

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request Jul 4, 2026
* docs(spec): 七軸(A1/CV/SS/KG/M/IN/RT)跨頁和諧整合 spec

新增 spec-to-done 用整合設計:跨頁 handoff 契約、共享狀態/證據列、
A1↔Review Room 既有交握延伸;不合併路由、不新增後端、不重裁 A1 3D 架構。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CyhkWS4Y4SZr1PGakMEKyF

* plan: 七軸跨頁和諧整合實作計畫(14 tasks,frontend-only 加法)

依 superpowers writing-plans 規格,由 spec 2026-07-03-seven-axis-cross-page-harmony-design.md
產出逐 task TDD 實作計畫:共用 handoff.ts(CrossAxisHandoff build/parse)+ SharedStatusProvider
單一輪詢 + SharedStatusRail + 七軸 cross-link chip + CV 轉檔歷史 panel(GET /api/dev/conversions)
+ KG 真 session 聚合 + Review Room 候選 seed + browser E2E。零新後端、零新路由、零新 production
dependency,diff 全落 web-viewer-sample/src/console/ 與 e2e/。

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

* plan: fix 修進 reviewer 五項 blocker/major(Task6 regex、Task14 空洞斷言、接收端重驗、a1-conv-link、Task7 覆蓋)

依 reviewer 發現逐項修入 plan(只改 plan 文件,不動其他檔;不刪需求):

- [buildability/completeness] Task 6 Step 1 測試改用 DOMParser 逐鈕查 `disabled`,
  取代 `/data-testid="a1-link-minio"[^>]*disabled/` 正則。Btn(components.tsx:113)
  屬性序 disabled 先於 data-testid 且 data-testid 為最後一個屬性,原正則永不 match
  (即使鈕真的 disabled);Step 5「Expected: PASS」矛盾一併消解。

- [spec-alignment] §4.3 A1 → CV 列標「既有連結,補帶 source/id」但 plan 零覆蓋:
  Task 6 新增 Step 4b 把 pages.tsx:604 的 a1-conv-link(`#/conv`)改成
  `buildHandoff("conv",{source:"a1",job_id:convJobId??undefined})`,並補測 href 斷言。

- [spec-alignment] §4.2(硬性)/§12/§13 要求接收端一律以 ID 重驗、查無誠實 not-found、
  禁靜默 fallback,但 plan 只做發送端。新增 Task 14「Receiver-side handoff
  re-verification」:共用 useIncomingHandoff+IncomingHandoffBanner,對 M/A1/CV/SS/KG
  五接收頁以已抓的權威資料重驗(零新後端 N2/N4);原 E2E 順延為 Task 15。
  另於 Task 13 補測 parseReviewRoomHandoff 接受 conv/sessions/intake/runtime 非 a1 source。

- [completeness] Task 15(原 14)E2E 的 `a1-embedded-viewer` testid 全庫不存在,
  toHaveCount(0) 真空通過、對 N3 零防護。改以真實存在的 review-room-viewer-host
  (ReviewSessionViewerPane.tsx:313,僅 Review Room 渲染)做差異式閘,並註明 exhaustive
  防線仍是 A1ViewerEmbed.test.tsx;另補 M→CV 落地後 conv-incoming-handoff 接收端斷言。

- [task-decomposition] Task 7 三加法只測 history panel,ledger/job 三個 chip testid
  無斷言即進 commit。補一輪 chip 斷言(conv-ledger-minio/conv-job-session/conv-job-review
  存在+導頁),Step 7 預期改 3 tests;並註明可等價拆兩 task。

同步更新 File Structure(新增 incomingHandoff.tsx)、Self-Review Checklist 與 OQ4 的
Task 14/15 交叉引用。

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

* plan: fix Task14 五接收頁補齊具體 code+測試、Task15 補 §8 stitched E2E 與誠實 checklist

修 reviewer 三項 blocker/major:

Task 14(接收端重驗,completeness + task-decomposition major):
- Step 4 把 A1/CV/SS/KG 四頁的散文指引改為可直接貼上的 code block,
  每頁綁真實 in-memory state var(A1=minioObjects、CV=jobs/records、
  SS=rt?.sessions.items、KG=shared.sessionsById)並附精確 render 行號錨點,
  M 也補上 <h1>(1608) 錨點;連 §4.3 硬性重驗鐵律於五頁一致。
- Step 1 receiving-pages 測試新增 A1(verified)/CV(verified)/KG(not_found)
  三個 per-page 斷線斷言,補齊 imports;五頁均可獨立驗證,接錯 state var
  或漏接即 npm test -- incomingHandoff.test.tsx 失敗(原本只斷言 M/SS)。

Task 15(Browser E2E,spec-alignment major):
- 新增一支 §8 stitched walk-through 測試(M→IN→CV→A1→Review Room→回 A1 issue),
  各 infra-heavy leg 用誠實 test.skip(同既有 chip 測試 pattern),不偽造。
- 修正 Self-Review Checklist 過度宣稱;補「§8 coverage & honesty」註記。
- 誠實查證:reviewer 建議引用的 VG-01(viewer-embed-a1-highlight.spec.ts)
  與 a1-minio-governance-3d.spec.ts 於 a334e49 解耦後已 stale(斷言已移除的
  a1-first-frame-evidence/a1-stage-matched/a1-highlight-3d),不得當覆蓋依據;
  四格深度證據由 Review Room 產出、需 live Kit session、CI 誠實 skip(skip != pass)。

只動 plan 文件;testids 全經 pages.tsx / ReviewSessionViewerPane.tsx 查證為真。

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

* task#0: feat(console): 新增 CrossAxisHandoff build/parse util(七軸通用 handoff)

- 新增 handoff.ts:AxisKey/CrossAxisHandoff 型別、isAxisKey/buildHandoff/parseHandoff
- buildHandoff target 收斂為 string(而非 AxisKey):既有 A1→Review Room pattern
  用 "review" 當 target,屬 alias route 非七軸之一(spec N1),AxisKey 過窄會與
  自身測試矛盾,故放寬(僅型別註記,行為不變)
- PAYLOAD_KEYS 型別排除 "source" 避免 parseHandoff 寫入時的 TS2322
- 4 個 vitest 全綠;tsc --noEmit 維持 baseline(僅 1 個既有無關檔案錯誤,無新增)

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

* task#1: feat(console): 新增 coordinatorClient.getConversionsHistory() wrapper(GET /api/dev/conversions)

薄封裝既有 GET /api/dev/conversions(conversion service 側 job 歷史,與 coordinator ledger
getConversionRecords 不同源);後端不改動(N2/N4)。新增 DevConversionRecord pass-through 型別
供 Task 7 消費。TDD:先寫失敗測試(getConversionsHistory is not a function)→ 補最小實作 → 通過;
順手修正測試檔內 afterEach(() => vi.restoreAllMocks()) 的隱式回傳型別(VitestUtils 非 void)觸發
的 tsc 錯誤,改用與既有 coordinatorClient.test.ts 一致的花括號寫法(行為不變)。

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

* task#2: feat(console): 新增 SharedStatusProvider 單一輪詢 + useSharedStatus hook(§5)

新增七軸跨頁狀態共享層:useSharedStatus.ts 定義 SharedSessionEntry / SharedStatusSnapshot 型
別、EMPTY_SHARED_STATUS 預設值、SharedStatusContext 與 useSharedStatus() hook;
SharedStatusProvider.tsx 是全 console 唯一對 GET /api/runtime/status 做 5000ms 定時輪詢的
地方(既有各頁自己的 mount-once fetch 不受影響),並用 getConversionRecords(100) 補轉檔佇列
深度(status ∈ detected/queued/converting 才計入)。GPU 節點欄位依 OQ3 恆為 null(未取得,非
偽造 0/0);stage_matched 依 §5.2 設計恆為 null。輪詢失敗時標記 stale=true、health="unknown",
不假裝資料仍新鮮。支援 value prop 供測試注入快照(跳過輪詢,測試 seam)。

TDD:先寫 SharedStatusProvider.test.tsx(3 案例:輪詢映射/失敗降級/注入不輪詢)→ 確認因缺模組
失敗 → 補最小實作 → 3 測試轉綠。驗證:npm run verify(build + 437 vitest + 10 struct-log 全過);
另用暫移新檔驗證 windowParentMessage.dom.test.tsx 既有 TS6133 warning 為 baseline 既存、非本次
引入。GitNexus detect_changes(staged) 確認 changed_files=3、risk_level=low,範圍與預期一致。

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

* task#2: fix 補齊 SharedStatusProvider stale 兩觸發條件 + 覆蓋 records 降級分支

gap1(漏做,spec §5.2/§5.4):stale 原本只在 poll reject 時翻真,缺「超過 2×
間隔」的時間 watchdog。請求掛起不 reject 時(背景分頁節流/連線卡住,jsonGet
無 AbortController/timeout)poll 停在 pending 不進 catch,stale 會永遠停在上次
成功值 false,把過期資料當新鮮,違反 §5.4。新增以 updatedAt/Date.now() 為基礎、
獨立於 await 是否 settle 的 setInterval watchdog:last-known-good 超過 2×pollMs
即翻 stale=true(已 stale 或尚無成功 poll 則回傳 prev 不 churn),cleanup 一併
clearInterval。

gap2(測試未覆蓋既有分支):新增測試涵蓋 runtimeStatus 成功但
getConversionRecords 失敗的 inner catch,證實 conversionQueue 誠實降為 null、
外層 poll 不崩、runtimeStatus 半邊仍正確映射。

驗證:SharedStatusProvider.test.tsx 5/5 綠(watchdog 先紅後綠、以預期原因失敗);
全套 vitest 36 檔 439 測全綠;tsc 我方檔案 0 error(僅既有 windowParentMessage
無關 baseline TS6133)。

未動 coordinatorClient.jsonGet 的 fetch timeout:跨所有端點、屬前端凍結契約、
blast radius 大,且 watchdog 已足以滿足 spec stale 語意,故列為 advisory 不在本
task 動它。

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

* task#3: feat(console): 新增 SharedStatusRail 元件與誠實渲染規則(§5.3/§5.4)

新增跨頁共享狀態列元件,讀 useSharedStatus() 單一真相來源,顯示 Active
sessions/GPU/Health/轉檔佇列/資料時間五項指標。GPU 與轉檔佇列在值為
null 時一律渲染「未取得」(非假綠燈);health="unknown" 顯灰、不與 ok/fail
混淆;stale=true 時整列變暗且顯示「資料過期」,不把舊值當即時呈現。點擊
GPU/Health/Active sessions 指標經 buildHandoff() 導向對應權威頁
(#instances/#runtime/#sessions),沿用既有七軸 handoff 慣例。

CSS 僅視覺(.ec-statusrail 區塊,附加於 edge-console.css 檔尾),測試不依賴
樣式,一律鎖 data-testid。

驗證:SharedStatusRail.test.tsx 5/5 綠(先紅:找不到模組 ./SharedStatusRail
→後綠);全套 vitest 37 檔 444 測全綠;tsc 僅既有 windowParentMessage.dom
.test.tsx pre-existing 錯誤(未觸碰檔案,非本次引入)。

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

* task#2: fix SharedStatus 佇列低報與孤兒輪詢兩個 quality 發現

Important #1:conversionQueue 在 ledger 超過回傳窗上限(後端 parseListLimit 100)
時,原本直接對被截斷的 recs.items 算佇列狀態筆數,會靜默低報真實佇列深度,操作員
會把低估值當真。比照 pages.tsx ledgerChipStatus 的 recordsIncomplete 模式:
recs.count > recs.items.length(截斷)時退 conversionQueue=null(未取得),不臆測
(誠實鐵律 / §5.4)。

Important #2:aliveRef 原為跨 effect 世代共用的 useRef。effect 因 pollMs/value 變動
重跑時,新 effect 會把同一個 ref 撥回 true,使舊 effect 卡在 await 的 poll resume 後
誤判自己仍存活,重新 setTimeout 排下一輪——這條孤兒輪詢鏈只存在舊 closure、新
cleanup 清不到,與正確迴圈並存重複打 API(違反 spec §12「只建立一條輪詢」)。改成
effect-local 的 let cancelled=false 閉包旗標,每個 effect 世代各自一份;一併移除不再
使用的 useRef import。

新增兩個回歸測試(截斷窗退 null、effect 重跑不產生孤兒 timer),先紅後綠;既有 5 測
試維持綠,tsc 僅既有 baseline TS6133(windowParentMessage.dom.test.tsx,未觸及)。

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

* task#2: fix SharedStatusProvider.test 補 root.unmount 清理殘留輪詢 timer

原 afterEach 只做 removeChild(container) + restoreAllMocks + useRealTimers,
從未 unmount;且各 it() 用 const root 宣告在區塊內,afterEach 根本拿不到。
React 18 createRoot 的 effect cleanup 只在 unmount / deps 變動時觸發,單純把
容器移出 DOM 不會清,SharedStatusProvider 的 setInterval watchdog + setTimeout
下一輪 poll(spec §5.1 的 5000ms 自動輪詢)因此殘留在 worker event loop;
watch 模式重跑會累加,且 restoreAllMocks 後殘留 timer 的下一輪會打到真的
coordinatorClient.runtimeStatus()。

比照同目錄 EmbeddedViewer.test.tsx 慣例修正:root 提升到 describe 作用域、
beforeEach 重置為 null、afterEach 改 async 先 await act(() => root.unmount())
再 removeChild,在還原 mock / real timer 之前清掉 effect 註冊的 timer;
render 呼叫比照慣例用 root! 非空斷言。

驗證:npx vitest run SharedStatusProvider.test.tsx 7/7 綠;npx tsc --noEmit 僅
剩既有 baseline windowParentMessage.dom.test.tsx:292 TS6133(不在本 diff);
eslint 該檔 0 warning/error。

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

* task#3: fix watchdog 觸發 stale 時一併把 health 降為 unknown(§5.4 誠實渲染)

SharedStatusProvider 的 watchdog(輪詢掛起、從未 resolve/reject 的路徑)原本只把
snapshot.stale 撥 true,卻沿用上一輪成功的 health(例如 "ok")。SharedStatusRail 的
health 徽章文字與 health-* CSS class 都純由 s.health 推導,於是在資料已過期時仍渲染
綠字級別的 "ok",違反 spec §5.4「stale=true 不得呈現 last-known-good 為 fresh;
來源沉默 = unknown」。

修法:watchdog flip stale 時於同一次 setState 一併把 health 降為 "unknown",與 catch
分支及 EMPTY_SHARED_STATUS(兩者皆 stale=true 配 health="unknown")對齊;文字與色階
一次修正。

先補回歸測試(既有 watchdog 測試只斷言 stale,未斷言 health):斷言掛起超過 2× 間隔
(3100ms)後 health === "unknown",紅→綠。

驗證:SharedStatusProvider + Rail 12/12;full vitest 37 檔 446 綠;tsc 僅既有 baseline
TS6133(windowParentMessage.dom.test.tsx,與本 diff 無關);eslint 3 檔 0;build 綠。

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

* task#3: fix SharedStatus 輪詢掛起自癒 + 補 degraded 健康態回歸測試

Important #1(SharedStatusProvider):jsonGet 無 AbortController/timeout,單次請求永久
掛起會讓 poll() 卡在 await、finally(唯一排下一輪的地方)永不執行,整條輪詢迴圈死亡
直到手動重整。watchdog 增設 liveness 復活:pollGen 標記每輪、inFlightSince 記錄當前
請求起始時間,in-flight 超過 2× 間隔即判定 wedged,disown 舊 poll(gen 不符→丟棄,不
重複排程 §12)並重啟新 poll,後端恢復後迴圈自癒。既有 watchdog honesty 與 orphan-loop
測試不動即綠。

Important #2(SharedStatusRail):健康三態(ok/degraded/unknown)原本缺 degraded 的回歸
測試。補 health="degraded" 斷言 health-degraded class 與字面值,防日後誤接 t() 翻譯或誤
併入 unknown 分支。

驗證:vitest 全 37 檔 448 test 綠(+2 新測);tsc 僅剩既有 baseline TS6133;eslint 改動檔 0 error。

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

* fix: 補強 SharedStatusProvider wedge 復活測試驗 snapshot 回到健康

原「revives the poll loop after a request wedges」測試只驗 watchdog 持續重試
(statusSpy call count 增加),沒驗復活後 snapshot 真的恢復健康;原 mock 鏈
mockResolvedValueOnce(rt(1)).mockReturnValue(hang) 之後永遠回傳不 settle 的 hang。
改為 .mockReturnValueOnce(hang).mockResolvedValue(rt(2)),讓某次 watchdog 重啟的
poll 真正成功,前進數個間隔後新增斷言 stale===false / health==="ok" /
activeSessions===2,把「復活後端已收到 fresh 資料」這半也鎖進回歸測試。
純測試改動,不動 production code(§5.1 Important #1)。

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

* task#4: feat(console): 在 EdgeConsole 頂層掛載 SharedStatusProvider + SharedStatusRail

七軸每頁共用同一份 runtime 真相(spec §5):EdgeConsole 頂層包一層
SharedStatusProvider(全 console 唯一輪詢),並在 ec-mainhead 的 FlowBar
之後插入 SharedStatusRail,依目前 hash 頁面算出 railAxis(七軸直接對映;
#gpu/#review 併入 runtime;其餘非七軸頁預設 a1)供狀態列高亮脈絡。
現有 header/nav/main/aside/footer JSX 內容不變,僅做外層包裹與單行插入。

新增 EdgeConsole.sharedstatus.test.tsx:驗證 shared-status-rail 出現在
DOM 中,且整個 console 只有一條 runtime/status 輪詢(不因多頁掛載而
重複打 API)。

驗證:npx vitest run(EdgeConsole.sharedstatus 1/1;console 全套 33
檔 407/407 綠,含既有 EdgeConsole SSR smoke 無回歸)、npm run build
成功、npx tsc --noEmit 僅餘與本次改動無關的既有 windowParentMessage.dom
.test.tsx TS6133(已用 git stash 比對 baseline 確認為既有問題)、npm run
test:struct-log 10/10 綠。GitNexus impact(EdgeConsole,upstream)=LOW
risk、0 upstream caller;detect_changes(staged) 僅回報 EdgeConsole.tsx
三個符號,經 diff 核對 usePageHash/read 為新增 import 造成的行號位移
雜訊、非真實行為變更。

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

* task#5: feat(a1): 新增 #minio / #sessions 證據型 cross-link chips

- A1 交付面板補兩顆 chip:回看 MinIO 來源物件(帶 minio_key)、跳
  Session 管理檢視此 session(帶 session id);目標 id 不存在時誠實
  disabled,不製造無效跳轉。
- 既有 a1-conv-link 錨點升級為 buildHandoff("conv", {source:"a1",
  job_id}) 產生的 #conv?source=a1[&job_id=...],取代舊的裸 #/conv
  (spec §4.3 A1→CV 列:既有連結,補帶 source/id)。
- 新增 A1CrossLinks.test.tsx:SSR smoke test(renderToString +
  DOMParser,無 mock),驗證兩顆 chip 存在且未選取時 disabled、
  a1-conv-link href 帶 source=a1。

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

* task#5: fix 補 A1 cross-link chip 導覽行為回歸測試

review 發現:A1CrossLinks.test.tsx 只驗證未選取時 disabled 與 caption
文案,從未驗證「已選取 → 點擊 → window.location.hash 內容正確」的核心
行為,無法擋 buildHandoff 目標軸字串打錯(如 "sessions" 誤打成單數
"session",EdgeConsole 無此 case 只會靜默 fallback 到 HomePage)或
minio_key / session 兩參數寫反。

在 A1ViewerEmbed.test.tsx 復用既有 renderA1 / selectSession / act /
flush harness,新增一個 client render 情境:選 MinIO 物件 + 選 review
session 後分別點擊 a1-link-minio / a1-link-sessions,解析 hash 斷言
且未交叉洩漏。已以暫時注入單數 "session" typo 驗證此測試會 fail
(line 210 startsWith("#sessions?"))確認有回歸擋防力後還原。

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

* task#6: feat(conv): 新增轉檔歷史 panel + #minio / #sessions|#review cross-link chips

- 新增 conv-history-panel:讀既有 GET /api/dev/conversions
  (coordinatorClient.getConversionsHistory,Task 2 已建),呈現
  conversion service 側 job 歷史 pass-through;prov="artifact"(形狀非
  本專案定義);載入失敗誠實顯示「未取得」而非假空表;空陣列另顯「非
  錯誤」提示。history/historyErr 與既有 records/recErr 各自獨立
  useEffect,不污染既有 ledger/ifc-ready 載入時序。
- Ledger 列 Control cell 補 conv-ledger-minio-<idem> chip:object_key
  存在才掛(evidence-typed),導到
  buildHandoff("minio",{source:"conv",minio_key,conversion_id});與既
  有「觸發轉檔」鈕互不排斥、可同列並存。
- Ifc-ready job 列 session cell 補 conv-job-session-<jobid> /
  conv-job-review-<jobid> 兩顆 chip:review_session_id 存在才掛,分別
  導到 #sessions?source=conv&session= 與
  #review?source=conv&session=;接收端依 spec §4.2 重驗 id,不靜默
  fallback。
- 新增 ConversionHistory.test.tsx(3 tests):history panel 正常/失敗
  兩態、ledger+job 列三顆 chip 存在性與點擊導覽(hash 含
  source=conv&session=)。

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

* plan: fix Task7 轉檔歷史第三欄改用 source_ifc_filename

created_at 經 Task7 實作查證,後端 GET /api/dev/conversions 三條回應組裝
分支結構性從不回傳(僅存在內部 job dict 供排序),對真實資料恆為 undefined。
使用者裁決:換成 source_ifc_filename(setdefault 保證存在,顯示轉檔的是哪個
IFC 檔,較 created_at 更有用)。同步修正 Task2 段落的 interface 說明,
避免文件與程式碼不一致。

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

* task#6: fix 轉檔歷史第三欄改用 source_ifc_filename(呼應 770a10a plan 修正)

轉檔歷史 panel(GET /api/dev/conversions pass-through)先前實作沿用
DevConversionRecord.created_at 顯示第三欄,但後端 _conversion_result_list_item
(conversion_authority.py:661-679)結構性從不對此欄位賦值——只存在內部 job
dict 供排序;序列化進 API 回應的只有 setdefault 保證存在的
source_ifc_filename。故 created_at 對任何真實資料恆為 undefined,畫面永遠
顯示「—」。plan 檔已在 770a10a 訂正描述,本 commit 補上對應的程式碼修正。

- coordinatorClient.ts:DevConversionRecord.created_at → source_ifc_filename
- pages.tsx:轉檔歷史表頭與儲存格改讀 source_ifc_filename
- ConversionHistory.test.tsx:mock 加入 source_ifc_filename 值並斷言渲染
  (先跑此測試確認因讀 created_at 而失敗,套用修正後再轉綠)

驗證:ConversionHistory.test.tsx(3)+ ConversionSchedulingPage.test.tsx
(39)+ coordinatorClient.conversions-history.test.ts(1)全綠;全套
vitest 40 檔 455 測試全綠;tsc --noEmit 僅餘既有無關檔案(不在本次改動
範圍)1 個 pre-existing 錯誤;vite build 成功。

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

* task#6: fix 補齊 CV cross-link chip 證據型雙向回歸測試

quality 發現:ConversionHistory.test.tsx 只驗 conv-job-review 一顆 chip 的
click→hash,另兩顆(conv-ledger-minio / conv-job-session)只驗 DOM 存在;且
spec §12「目標 ID 缺 → chip 消失」方向零覆蓋,日後若把 truthy gate 寫反無測試可抓。

- 正向:三顆 chip 各驗一次 click→hash(minio_key 經 parseHandoff round-trip
  斷言 CJK/斜線精確還原;session 帶 source=conv)。
- 反向(新測):object_key / review_session_id 皆 null 時三顆 chip 皆不 render,
  且列本身仍 render(證明是 chip 條件隱藏、非整列消失),與正向配對夾住條件。

純測試改動不動 production;vitest 40 檔 456 綠、tsc 僅餘既有無關錯誤、
eslint 改動檔 0 error。

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

* task#7: feat(sessions): 新增 per-row #instances / #review / #a1 cross-link chips

SessionManagementPage 的 Active sessions 動作欄在既有「結束 session」鈕旁
補三顆證據型 chip(session-link-instances-<id> / session-link-review-<id> /
session-link-a1-<id>),各自用 buildHandoff 帶 source=sessions + session id
導向 #instances / #review / #a1。SS 頁維持自己 mount-once runtimeStatus
抓取不變(N6),不耦合 useSharedStatus;接收端依既有 §4.2 規則自行重驗
session id。

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

* task#8: feat(instances): 新增即時 session 聚合列 + demo 列 #sessions cross-link chip

KG(#instances)原本 100% 靜態、無任何 fetch;現在讀 useSharedStatus() 呈現真 session
聚合(asbuilt,data-testid=kg-live-aggregate,含每個真 session 的 #sessions 導覽鈕),
並在 demo Node snapshot 表首列補一顆導向 #sessions?source=instances 的 chip
(data-testid=kg-demo-link-sessions)。demo 表本身維持 prov="demo" 不動、不假裝接真
(N5);GPU per-node 遙測仍未取得(OQ3)。

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

* task#8: fix KG 即時聚合只列 active session,closed 不假裝成即時可操作

sessionsById(spec §5.2)是全量 session 表(不分狀態,供跨頁 ID 查找重用),
且 coordinator 從不刪除 session(只 active→closing→closed,永遠保留)。原
liveIds = Object.keys(sessionsById) 未過濾狀態,把 closed/closing 的過期
session 在標題「即時 session 聚合(真實)」、prov="asbuilt" 的區塊渲染成可點的
kg-session-link 導覽鈕,且緊鄰只算 active 的「使用中 session 數」,並蓋掉誠實
空狀態文案——把過期 session 假裝成真實可操作(違反 N5 誠實鐵律)。

改為只取 status==='active' 的 session 當即時連結,與相鄰 activeSessions 聚合
一致;closed 過期 session 回到誠實空狀態。新增兩個回歸測試涵蓋 activeSessions=0
卻殘留 closed session、以及 active/closed 混雜兩情境。

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

* task#9: feat(minio): 新增 .ifc 物件 #conv / #a1 cross-link chips(中文 minio_key 往返)

M 頁(MinioDataPage)obj.role === "source_ifc" 列在既有觸發轉檔鈕後補兩顆
evidence-typed chip:minio-link-conv-<idk> 導向 #conv?source=minio&minio_key=...、
minio-link-a1-<idk> 導向 #a1?source=minio&minio_key=...,皆帶編碼後的 obj.key
(可能含中文,如 270專案/建築/v07/模型.ifc)。接收端(CV/A1)依 §4.2 重驗 minio_key。

新測試 MinioCrossLinks.test.tsx 驗證中文 key 經 buildHandoff → URL hash →
parseHandoff 完整往返不失真(OQ4 決定性 spike)。

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

* task#9: fix 補 #conv cross-link 目的地路由前綴斷言(與 #a1 測試對稱)

MinioCrossLinks.test.tsx 第一個測試(navigates to #conv)原本只斷言 parseHandoff
解出的 source / minio_key,未驗目的地路由前綴。因 parseHandoff 只解析 query、不看
target 字串,若 onClick 誤寫成 buildHandoff("a1", ...) 此測試仍會通過,測試名稱與
實際斷言不對稱。補一行 window.location.hash.startsWith("#conv?") 斷言,與同檔 #a1
測試(line 58)及 Session/KitGpuFleet 姊妹測試家族的目的地前綴檢查對齊。

生產碼本身正確(pages.tsx:1844 buildHandoff("conv", ...)),純測試嚴謹度補強,
非修 live bug。

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

* task#10: feat(intake): 新增 job 列 #conv / #review cross-link chips

IntakePage 的 intake job 表新增「跨頁」欄:intake-link-conv-<jobid> 永遠顯示,
導向 #conv?source=intake&job_id=...;intake-link-review-<jobid> 僅在
j.review_session_id 存在時顯示,導向 #review?source=intake&session=...
(無 session 時不畫假 nav,符合 §4.2 誠實鐵律)。皆用既有 buildHandoff 產生
hash,接收端(CV / Review Room)依既有規則自行重驗 ID。

新測試 IntakeCrossLinks.test.tsx:驗證兩顆 chip 皆出現且點擊後 hash 正確;
以及無 review_session_id 時 review chip 不渲染(no fake nav)。

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

* task#10: fix 補 IN #review cross-link chip 的 click→hash 斷言

IntakeCrossLinks.test.tsx 原本只斷言 intake-link-review-job_1 存在於 DOM
(not.toBeNull),從未點擊、從未驗證產生的 hash;#review chip 的 onClick
(session=…)因此無任何行為防護,若複製貼上失手把 session 換成 job_id,CI 會
靜默通過。改為比照同檔 #conv chip 與 sibling ConversionHistory.test.tsx 的
既有 pattern,逐一 click→斷言 hash 含 #review?source=intake 與
session=review_session_a。

已 red-verify:暫時把 pages.tsx 的 session 改成 job_id,新斷言如期以
「expected '#review?source=intake&job_id=job_1' to contain
'session=review_session_a'」失敗,還原後 2/2 綠。

Verify:npx vitest run src/console(39 files / 428 tests 綠)、tsc --noEmit
本 diff 0 新增錯誤、eslint 該檔 exit 0、vite build 成功、git diff --check 乾淨。

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

* task#11: feat(runtime): CoordinatorPage 新增跨頁 session 連結 Panel

RT 值班視圖(#runtime)新增「跨頁 session 連結」Panel,列出
rt.sessions.items 每個 session 並提供三顆證據型 chip:
- rt-link-sessions-<id> → #sessions?source=runtime&session=<id>
- rt-link-review-<id>   → #review?source=runtime&session=<id>
- rt-link-instances-<id> → #instances?source=runtime&session=<id>

複用既有 buildHandoff(Task 1)與 CoordinatorPage 既有 rt state,
不改 CoordinatorGovernanceTabs(endpoint/role-keyed rows 加 chip 不安全),
不動四分頁路由(D2-A′維持)。RT 的共享狀態消費由全域 rail(Task 5)
已滿足,本 Panel 只補 session 層級的跨頁導航。

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

* task#11: fix RT 跨頁 session 連結對 closed session 停用即時操作鈕(N5 誠實鐵律)

CoordinatorPage 的「跨頁 session 連結」Panel(prov="asbuilt")原對 GET /api/runtime/status
回傳的全量 rt.sessions.items 直接 .map(),不分 active/closing/closed 都渲染三顆滿血可點鈕。
coordinator 從不刪除 session(只 active→closing→closed,永遠保留),此表隨時間無界成長,
把已結束 session 假裝成即時可操作——與同分支前一 commit 0860a54(task#8)剛修的 KG 即時
聚合同型 N5 違規。

比照 0860a54:以 live = s.status === "active" 判定,對「在 Review Room 開此 session」
「Kit / GPU 機隊」兩顆即時操作型鈕在非 active 時 disabled + 誠實 caption/title(session 已結束);
「Session 管理」是 lifecycle 全量治理視圖,對已結束 session 給連結語意合理,保留 enabled。

新增兩個回歸測試:closed-only(Review/KG disabled、SS enabled)與 active+closed 混雜
(只 active 列可操作);既有 active 導航測試不變。

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

* task#11: fix RT 跨頁 session Panel false-empty 分流 + 補 SS/Review chip handoff 斷言

Important #1(N5 誠實鐵律):CoordinatorPage 跨頁 session Panel 原本 rt===null(初載/
Refresh 失敗)與 items 為空共用同一「無 session」文案,把「連不上 coordinator」講成「確實
無 session」。改為比照同頁 IntakePage:err 先以 ec-warn-note 浮出,rt===null 且無 err 顯示
「讀取中」,只有 rt 已回且 items 為空才顯示 confirmed-empty(並註明 coordinator 已連線、
非錯誤)。err 一起浮出也讓 Refresh 失敗、rt 停舊值時有錯誤線索。

Important #2:CoordinatorCrossLinks.test.tsx 原本只驗 SS/Review 鈕 disabled 布林,未驗其
onClick handoff 內容。比照 IntakeCrossLinks / SessionCrossLinks,為 rt-link-sessions-* 與
rt-link-review-* 各補 click→hash 斷言(#sessions / #review?source=runtime&session=…),並新增
fetch 失敗時不得 false-empty 的紅→綠回歸測試。

驗證:vitest CoordinatorCrossLinks 4/4、全庫 45 檔 474 tests 綠;tsc 僅 1 個 pre-existing
錯誤(windowParentMessage.dom.test.tsx,commit 1f97127,早於本範圍);eslint 僅 2 個
pre-existing _reason unused(pages.tsx:999/1712,commit d5e9286);vite build 綠。

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

* fix: CoordinatorPage confirmed-empty 分支加 err 守門,避免 Refresh 失敗與「無 session」並存

初載成功且 0 session 後按 Refresh,若第二次 fetch 失敗,load() 只 setErr、不重置 rt,
rt 停在 0-session 舊真相(非 null),confirmed-empty 分支照舊渲染,導致紅字錯誤與
「coordinator 已連線,非錯誤」文案自相矛盾並存。比照同檔 IntakePage 的 {err ? "" : t(...)}
守門,有 err 時 confirmed-empty 文案讓位給上方錯誤訊息。新增回歸測試覆蓋此 Refresh 失敗情境。

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

* task#12: feat(review): 從 shared status 種入 Review Room session input 候選 datalist

- ReviewSessionViewerPane 新增 useSharedStatus() 讀 sessionsById,餵入新增的
  <datalist id="review-room-session-candidates">;既有 session input 加 list 屬性,
  input 仍是自由輸入欄位、不強制選單(additive,N3 安全)。
- 不動 claimPrimary、lease/heartbeat effects、sendHighlight、EmbeddedViewer wiring。
- 新增 ReviewSessionViewerPane.crosslinks.test.tsx:驗證 datalist 種子 + no auto-claim,
  並佐證既有 parseReviewRoomHandoff 本就接受非 a1 來源(§4.3 新 chip 確實能被 #review 消費)。

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

* task#12: fix Review Room datalist 候選只列可 attach(active/created)session

ReviewSessionViewerPane 的 session input datalist 原用 Object.keys(shared.sessionsById)
把全量 session 全塞進候選。sessionsById(spec §5.2)是不分狀態的全量表,且 coordinator
從不刪除 session(active→closing→closed 永久保留),故長壽環境會累積大量 closed 過期 session。
原生 <datalist> 不顯示狀態,active 與 closed 的 session id 外觀無異;選到 closed 後才在按
「手動啟動」時發現 disabled——把過期 session 假裝成可 attach 候選(違反 N5 誠實鐵律)。

改為只取 status 為 active/created 的 session_id,與本 pane 既有 runtimeSessions 篩選
(line 123)與 sessionObserved/claimPrimary 的手動啟動 gate 同一組可 attach 狀態一致,
比照前一顆 task#8 對 KitGpuFleetPage 的 active-only 修法。新增回歸測試以 active/created/
closed/closing 混雜快照鎖住:只有 active/created 進 datalist,closed/closing 被排除。

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

* task#13: feat(console): 五接收頁補 incoming handoff 重驗(§4.2 誠實 verified/not_found)

新增 incomingHandoff.tsx 共用 useIncomingHandoff() hook + IncomingHandoffBanner;
串進 MinioDataPage / A1GovernanceWorkbenchPage / ConversionSchedulingPage /
SessionManagementPage / KitGpuFleetPage 五個接收頁,各自向頁面已抓取的權威資料
(folder.objects / minioObjects / jobs+records / rt.sessions.items /
shared.sessionsById)重驗 incoming id;查無一律誠實 not_found,不靜默 fallback
到其他紀錄。IN 為 sender-only 軸,依 spec §4.3 矩陣不加接收端。

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

* task#13: fix M 頁 incoming prefix handoff 落實接收端重驗(§4.2 誠實 not_found)

MinioDataPage 對帶 prefix 的 incoming handoff 原本無條件 `return true`,從不向已抓取的
權威資料(folder.folders/objects)查驗,結構上永不可能回 not_found,違反 task 接收端重驗
鐵律與 spec §4.2;且 prefix 從未寫回頁面 state,資料夾瀏覽器仍停在根目錄,banner『已重驗』
宣告為假(未落實 §4.3「A1 → M 回看選檔來源」)。

修法:
- verify predicate:prefix 分支改為向載入的 folder 重驗——folder.prefix 需等於請求 prefix
  (後端 minioClient.ts 回填該欄)且該層真有 folders/objects 才 verified;空層/未設定/尚未
  載入一律誠實 not_found,不靜默 fallback。
- 新增 effect:incoming prefix → 導覽到來源資料夾一次(讓 folder 真的載入該層再重驗),之後
  交還使用者手動導覽,不與 goUp/enterFolder 打架。
- minio_key 分支不動;其餘四接收頁不動。

測試:incomingHandoff.test.tsx 補 2 例(prefix verified:真的導覽到來源層+向該層重驗;
prefix not_found:導覽後空層誠實 not_found),先驗兩例以預期原因失敗再實作。web-viewer-sample
全套 493 測試(47 檔)綠、vite build 綠、tsc 對本次 2 檔零錯。

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

* task#13: fix M 頁 minio_key 接收端重驗導覽 + load 世代守門(§4.2 誠實 not_found)

CRITICAL:A1→M / CV→M chip 只帶 minio_key(從不帶 prefix),但接收端只查
「當層 folder.objects 是否含該 key」而不導覽。真實 S3 帶 Delimiter='/',巢狀 key
只落在其所在資料夾的 objects、根層 objects 不含它,故對真實 ≥3 層 key 恆誤報
not_found(正是 task 想落實的誠實 verified/not_found 被架構性打破)。修法:navigate
effect 除 prefix 外,minio_key 也導覽到 key 所在資料夾(末個 '/' 前路徑)後再重驗。

Important:掛載時「導覽 setPrefix」與「prefix 變更即重載」effect 併發兩個
getMinioFolder(根層+目標層),無守門則根層晚到的回應會蓋掉已導覽的正確 folder →
folder.prefix 退回 ""、假 not_found。load() 加遞增世代守門,過期回應丟棄、不覆蓋
畫面/錯誤/loading。

測試:incomingHandoff.test.tsx 兩個 minio_key fixture 改成尊重 delimiter 語意
(root 只含 CommonPrefix、深層 key 落在來源層),並新增 late-root 競態守門測試;
三者皆先 RED(無導覽 / 競態誤蓋)後 GREEN。全套 494 test 綠、tsc 對本次 2 檔零新增錯誤。

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

* task#13: fix 接收端重驗三缺陷(KG 物件注入假 verified/M 導覽後假 not_found/CV 截斷窗假 not_found)

- CRITICAL #1 KitGpuFleetPage:session 以 bracket 查 plain {} sessionsById 會命中 Object.prototype
  繼承名(constructor/toString…)恆真、對不存在 session 假報 verified(違反 §4.2 持有 ID≠已授權);
  改 Object.prototype.hasOwnProperty.call 只認真正的 own key。
- Important #2 useIncomingHandoff:接收端重驗是抵達時的一次性閘門,一旦 verified 就以 hash 簽章 latch;
  之後同一 handoff 因使用者手動導覽(M 頁 goUp/enterFolder 改 folder)而 verify 回 false 也不倒退成假 not_found。
- Important #4 ConversionSchedulingPage:jobs/records 回傳窗(limit 50)被截斷(count>items.length)時查無
  id 退 indeterminate(未明)而非誤報 not_found,比照本頁 ledgerChipStatus recordsIncomplete(§5.4 誠實鐵律)。
- HandoffVerifyStatus 增 indeterminate 中性態、banner 不掛 ec-warn-note;verify 回傳型別擴為
  boolean|"indeterminate",其餘四接收頁維持 boolean 不變(向後相容)。
- 測試 +6:latch/indeterminate 單元、KG constructor 注入、M go-up latch、CV jobs/records 截斷。
- 延後 Important #3(掛載載入期間假 not_found):需為五頁各接「權威資料尚未載入」訊號,CV(busy 起始 false、
  無 first-load flag)與 KG(空 sessionsById 無法區分未輪詢/真零)無乾淨訊號,暫緩併後續 task(reviewer 已授權)。

驗證:vitest 500/500(+6);tsc --noEmit 僅既有 windowParentMessage 錯(#273,與本次無關);
eslint 三檔零新增問題(stash 比對 HEAD 同 3 既有問題、行號僅位移)。

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

* fix: 接收端重驗載入中回 indeterminate 不誤閃 not_found

Task14 Important #1:A1/SS/KG/M 四個接收頁的 incoming handoff verify
predicate 把「權威資料尚未載入」與「已載入但真的查無」都壓成 not_found,
導致掛載後第一個 fetch resolve 前的同步 render 誤閃 not_found 假警示。

比照 incomingHandoff.tsx 既有的 indeterminate 第三態(原僅 CV 用),
四頁在權威資料未載入時改回中性 indeterminate(未明)而非 false:
- A1:minioObjects===null
- SS:rt===null
- KG:useSharedStatus().stale===true(尚未輪詢過)
- M(minio_key 分支):folder===null(比照 prefix 分支既有的 !!folder 寫法)
CV 維持原狀(缺乾淨 loading 訊號,沿用既有截斷→indeterminate)。

各頁補一支回歸測試:以永不 resolve 的 Promise mock 掛載頁面,斷言
載入中 data-handoff-status 為 indeterminate 而非 not_found。

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

* task#14: test(e2e): 補跨軸 handoff 巡覽 walk-through 瀏覽器 E2E(§8, N7)

新增 cross-axis-handoff.spec.ts:5 個測試涵蓋七軸共享狀態列、M→CV/A1→Review/SS→Review
handoff 導覽 + 接收端重驗、以及 §8 生命週期 stitched walk-through(M→IN→CV→A1→Review
Room→A1 issue,深層 Kit GPU 段落誠實 skip)。已對 branch-isolated coordinator(:8005,
掛本分支 build:ui 產出,非部署區 :8004)跑真實 Playwright:2 pass + 3 honest skip(本
環境無 MinIO 物件/無 active session,符合 plan 預期),screenshot + trace 落
artifacts/e2e/。npm run verify(build + 504 unit tests + struct-log)全綠。

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

* task#14: fix 跨軸 handoff E2E 誠實度標註與 CV 載入競態守門

依 quality 複審修兩處 Important(僅動 e2e/cross-axis-handoff.spec.ts,無 production code 變動):

Important #1:§8 walk-through 前補 COVERAGE & HONESTY NOTE — 明列 fixture-less 環境下僅
test 1(shared-rail)與 test 3(Review Room not-started)跑滿全程;深段(M→CV/IN→CV/A1
rule-run/Kit 四證據鏈)皆 honest-skip、目前 `not observed`,信心僅 code-review + 結構層級、
非 browser-E2E 佐證;並記錄取得深段真證據之法(branch coordinator :8005 + 真 MinIO source_ifc
fixture)。同時解掉深段註解對「§8 coverage & honesty note above」的懸空引用。

Important #2:CV records 初值 []、mount 後才 async fetch,且保留 parent 47f9975 未修的
截斷→not_found 載入窗。三處 data-handoff-status 斷言(isolated M→CV/§8 M-leg/§8 IN-leg)
改以 waitForResponse(/api/conversion/records)(promise-before-click 不漏接)等 ledger 落地,
讓斷言讀 CV 終態而非 pre-load not_found flash。

驗證:eslint --max-warnings 0 exit 0;playwright --list 5 支全編譯;git diff --cached --check
乾淨;detect_changes(staged)=0 symbols/low risk。未跑真 E2E(需 live coordinator + fixture,本機無)。

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

* task#14: fix 跨軸 handoff E2E 兩處競態守門(CV predicate 綁定 + skip 重試偵測)

quality 複審兩個 Important(皆為原始碼生命週期推導出的結構性競態,非實測 flake):

Important #1:waitForResponse predicate 原本只比對 `/api/conversion/records`
子字串,會被永遠掛載的 SharedStatusProvider 每 5s 背景 poll(limit=100)與 M 頁
mount fetch(limit=100)搶先 resolve,跟 CV 頁自己那次 loadRecords(pages.tsx:952,
limit=50)毫無關係 → 三處 predicate(M→CV 單測、§8 M-leg、§8 IN-leg)改綁 CV 專屬
`?limit=50`,不再誤吃背景 poll,真正守住 data-handoff-status 的 not_found 載入閃現。

Important #2:四處 skip/soft 偵測在 page.goto(waitUntil:'load')resolve 後立即
同步 .count(),未等 React mount 後 useEffect 的 on-mount fetch(getMinioFolder /
runtime status / ifc-ready jobs)渲染完 → 即使環境備妥真 fixture 也可能查太早回 0 而
誠實 skip,牴觸 §8 COVERAGE NOTE 敘事。改用 .waitFor({state:'visible', timeout:10s})
.then(bool) 重試偵測(比照本目錄既有慣例 a1-minio-governance-3d.spec.ts:28),只在真正
缺席時才 skip;genuinely-absent 仍在 ~10s 內落下。

line 159 host.count() 不在四處清單內、位於頁面已 render 之後且 viewer host 本就只在
手動 attach 後掛載,維持原樣(YAGNI)。

驗證:npx eslint(0 warning)、npx playwright test --list(5 支全編譯)、detect_changes
staged(changed_symbols=0、affected_processes=0、risk=low)、git diff --cached --check
(無 trailing whitespace)。未起真實 coordinator+fixture 實測(結構性守門)。

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

* fix: 修正 cross-axis E2E 誠實標註與 indeterminate 斷言

Task 15 對抗複驗兩項未閉合 finding(皆為測試檔字串/斷言修正,不動測試邏輯):

- A1→Review Room leg 的 test.skip 理由與 §8 COVERAGE & HONESTY NOTE 原本把不可達
  歸因成缺真實 IFC rule-run 環境/fixture,但這是結構性死路:a1-open-review-room 需
  selectedSession 非空且 state.failed[0] 存在(a1ReviewRoomHandoffReason,
  pages.tsx:256-262;state 為純前端 useReducer, pages.tsx:283,306),本測試從不驅動
  選 session 或 rule-run,故無論接什麼環境都 100% skip。改成準確描述為測試範圍限制,
  非環境/fixture 限制(依原意不新增瀏覽器操作步驟,避免最後一個 task 增加自動化脆弱點)。

- 三處 data-handoff-status 斷言(M→CV 及 §8 M→CV/IN→CV leg)原本只接
  /verified|not_found/,未算進 indeterminate。CV verify predicate(pages.tsx:908-922)
  在 ledger 超過 getConversionRecords(50) 查詢窗時誠實回傳 indeterminate
  (incomingHandoff.tsx:9,49),屬誠實鐵律正確第三態;長期 ledger 累積超過 50 筆會使
  斷言間歇 flake。三處改為 /verified|not_found|indeterminate/ 並加註原因。

驗證:npx playwright test --list 解析成功(5 tests);detect_changes(staged)=0 changed
symbols / 0 affected processes / low risk;git diff --cached --check 乾淨。未跑瀏覽器
E2E(需 live coordinator,本次僅測試檔字串/斷言修正)。

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

* task#14: fix 補齊 cross-axis handoff E2E 之 HEAD 證據並修正 viewer-host 註解行號

Gap 1(必修):Step 2「services up 後跑一次」對 HEAD 從未執行過(前三個修改測試邏輯的
commit 皆自述未跑真 E2E,僅做 eslint / playwright --list 等靜態檢查)。本次對分支隔離
coordinator(:8005)補跑:
- 以 VITE_COORDINATOR_API_BASE=http://127.0.0.1:8005 重建 dist-ui,起隔離 branch
  coordinator(PORT=8005,不碰部署區 :8004)。
- E2E_COORDINATOR_BASE_URL=http://127.0.0.1:8005 playwright test cross-axis-handoff.spec.ts
  → 2 passed(shared-rail axis sweep、A1 no-embed / Review Room not-started)
  + 3 honest skip(M→CV、SS→Review、§8 walk-through;空 MinIO / 無 active session =
  not observed,skip != pass)。
- screenshot(cross-axis-rail-runtime.png、cross-axis-review-not-started.png)落 artifacts/e2e/。
- 已知界線:HEAD 的 CV load-race waitForResponse 守門與 indeterminate 斷言,只有在具真
  MinIO fixture + ledger >50 筆時才會被實際走到;本環境無,屬 not observed,非本 additive
  spec 範圍(誠實鐵律,未偽造)。

Gap 2(次要):line 55 註解引用 ReviewSessionViewerPane.tsx:313 過時,該 testid
(review-room-viewer-host)實際落在 :329(自 7b4f6a6 起即有偏移,非本次退化),改正行號。

驗證:npm run verify 綠(build + 504 unit + 10 struct-log);eslint 該檔 0 warning;
detect_changes(staged) changed_symbols=[] risk=low(僅註解,無 symbol 變更)。

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

* task#14: fix 修正跨軸 handoff E2E 誤導性註解(waitForResponse 守的是 screenshot 穩定畫面,非斷言嚴謹度)

quality Important #1:三處 data-handoff-status 斷言用 /verified|not_found|indeterminate/ 寬鬆 regex,
接受 CV 載入前的 transient not_found flash(records=[] & recordsTruncated=false → verify 回 false →
not_found;pages.tsx:865,904,908-922 + incomingHandoff.tsx:29-30)。因此 `await recordsSettled` 不改變
toHaveAttribute 的 pass/fail——真正效果是讓後面的 screenshot 拍到 CV 終態、而非 pre-load flash。

前次 commit(7d1b467/3a72b46/08b8327)措辭主張此守門是在「保護斷言讀終態不誤過」,實際只保護 screenshot
穩定畫面,會誤導後續維護者高估斷言的迴歸保護力。本次改動:
- 只改註解、不動斷言 regex(寬鬆 regex 在 fixture-less 環境是誠實的三態終值集合;收斂成 verified 會 flaky/說謊)。
- 校正 arm-site 與三處斷言周邊註解:明講 wait 買的是 screenshot frame、非更嚴斷言;標明此斷言為 wiring
  smoke test(banner 掛上 + CV 重驗成非-none 誠實態),per-input 鑑別(含 truncation→indeterminate)由
  incomingHandoff.test.tsx 單元測試負責。
- 移除會漂移的絕對行號自我引用,改用符號式指涉。

驗證:npx eslint(EXIT=0)、npx playwright test --list(5 tests 全編譯)與 baseline 一致;git diff --cached
--check 乾淨;detect_changes(staged) changed_symbols=[] risk=low(comment-only,無 production symbol)。

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

* task#14: fix 修正跨軸 handoff E2E 兩處 quality 發現(waitForResponse 註解誠實化 + 刪 A1 leg 不可達死碼)

Important #1:page.waitForResponse 在 HTTP response(header 收到)當下 resolve,不等 body 讀取 +
setRecords + React 重渲染;且 CV 的 transient 與 terminal 皆可能是 not_found、無 DOM signal 可辨別,
故 expect.poll 不可行、waitForTimeout 屬 anti-pattern。改採誠實作法:把 4 處「screenshot 呈現終態」
的過度承諾改弱為 best-effort/likely,明說不保證重渲染已入 DOM(isolated M→CV arm+inline、s8 M→CV
arm、IN leg)。斷言與 pass/fail 完全不變。

Important #2:a1-open-review-room 為 disabled={Boolean(a1ReviewRoomHandoffReason(...))},
selectedSession="" 時 reason 恆非空 → canOpen 恆 false → 原 test.skip(!canOpen) 之後的 183-203 行
為不可達死碼(任何環境、任何 fixture 皆到不了)。依「刪 code 拿到一樣結果視為 win」移除死碼,改以可達
且確定的 toBeVisible + toBeDisabled 斷言作 A1 終點(present-but-disabled 為真實可驗狀態),並同步修正
§8 誠實註解、測試標題與 screenshot 名稱,避免死碼冒充活路徑。

驗證:eslint 0 warning;playwright --list 仍 5 tests;isolated tsc(shim node global)exit 0;
detect_changes(staged) changed_symbols=0 / risk=low(純測試檔);git diff --cached --check 乾淨。

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

* fix: 跨軸 handoff E2E 之 M leg 軟性化解 A1 斷言遮蔽 + 補驗證層級誠實註解

- [task15-r2-important1] §8 walk-through 的 M leg 由 test.skip(!mHasConv) 改為軟性
  if (mHasConv){...}(比照同測試 IN leg 的 pattern)。Playwright 在 test body 內呼叫
  test.skip 且條件成立時會立即中止整支 test,原寫法在無 MinIO fixture(常態)時會於
  M leg 就整支中止,後段 A1 Review-Room CTA 斷言(toBeVisible/toBeDisabled)從未執行,
  與同測試 coverage note「present-but-DISABLED … holds in ANY environment」矛盾。改軟性
  if 後 A1 leg 在任何環境皆可達;同步把兩處 coverage note 的 test.skip 措辭更新為
  soft-gate,避免註解與程式碼互相矛盾。
- [task15-r2-important2] 在 A1 CTA 兩條斷言前補誠實註解:此斷言邏輯已對 pages.tsx 逐行
  核對(交付/Deliverables Panel 無條件渲染 pages.tsx:651、selectedSession 初值 ""
  pages.tsx:306 且不從後端還原 → a1ReviewRoomHandoffReason 恆非空 → disabled),但尚未
  經真實瀏覽器對運行中 coordinator 執行;live 驗證留待 spec-to-done P4 browser evidence
  階段,不在此重複搭分支 coordinator。未新增或修改任何測試邏輯。

僅改動測試檔(單一測試控制流 + 註解),未動 production symbol;eslint 與
playwright test --list 皆綠。

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

* task#14: fix 補回 §8 跨軸 walk-through 之 Review Room + A1-issue 兩段並改容錯 CTA gate

必修 gap:§8 stitched walk-through 先前把 A1 leg 之後的 Review Room 與回 A1 建 Issue
兩段整段砍除(commit 5c0bb46),只斷言 a1-open-review-room present-but-disabled 就結束,
未涵蓋需求 Key Coverage Point #6 明列的 M→IN→CV→A1→Review Room→A1 issue 六段;需求僅把
deep Kit evidence(first_frame/stage_matched/datachannel/highlight)列為無 live session 可
誠實跳過的例外,並未豁免 Review Room 可達狀態與 A1 issue 段落。

修法:
- §8 walk-through 現在真正走完 reachable spine:A1 CTA→(容錯)→Review Room
  kit-not-started→回 A1 斷言 a1-step-issues present。Review Room 段經 CTA 自身的
  fixture-less target #review?source=a1(pages.tsx:711)到達,任何環境可達、不偽造 rule-run。
- 修正脆弱無條件 toBeDisabled:改成本檔其他 leg 一致的 waitFor→branch 容錯 pattern——
  CTA enabled(部署餵入 rule-run fixture)則點擊走真 A1→Review handoff、斷言
  #review?source=a1 URL;disabled(fixture-less)則斷言 present-but-disabled 再續走 stitch,
  不再會因未來 CTA 變 enabled 而硬 FAIL。
- deep Kit evidence 四點移到專用 test,以 waitFor→test.skip 提供誠實 not-observed
  runtime 訊號(skip != pass, N7),對齊需求 Steps「honest test.skip for non-observed cases」,
  不再只靠測試標題字串/註解揭露。

驗證:eslint exit 0;playwright --list = 6 tests(原 5+1);對 live coordinator :8004 跑兩支
§8 測試:reachable spine PASSED(21.1s,實走 A1 CTA disabled→review-room-kit-not-started→
a1-step-issues,截圖 s8-02/03/04)、deep Kit SKIPPED(誠實 N7 訊號);detect_changes(staged)
changed_symbols=0 / changed_files=1 / risk=low(純測試檔);git diff --cached --check 乾淨。

已知風險::8004 為部署區(main)build,缺本分支新 testid(shared-status-rail/a1-link-sessions)
故同檔 Test 1/3 於該環境失敗——與本次改動無關;§8 兩支用的是 pre-#286 已部署 testid 故有效受測。
分支隔離 stack(:8005)完整 live run + 真 rule-run 使 CTA enabled 之 handoff 串接仍屬 P4。

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

* task#14: fix 跨軸 handoff E2E 兩處 quality 發現(deep Kit 死 skip 可達化 + SS→Review 補重驗斷言)

Important #1(§8 deep Kit evidence chain 永久死 skip):
原測試 goto #review?source=a1 後直接等 review-room-viewer-host,但該 host 只在
activePrimaryLease 為真時掛載,而 lease 唯一寫入點是 claimPrimary()、唯一呼叫處是手動啟動鈕
onClick(ReviewSessionViewerPane.tsx:185-211,295,324,329)。無 session、無點擊 → 永遠逾時 →
test.skip 在任何環境恆觸發,即便接上真 Kit GPU 也永不亮。改為從 #sessions 取真 active
session(帶 session-terminate 鈕的列,pages.tsx:1497)→ 手動 attach → 逐關 honest N7 skip
(無 active session / 鈕 disabled / 未掛載)→ 真 WebRTC frame 落地才斷言 first_frame +
datachannel_ready(onFirstFrame)。stage_matched / highlight_ack 誠實標為需 A1 rule-run
handoff payload、本路徑不帶,僅截圖。

Important #2(SS→Review 標題稱 re-verified 但未驗重驗):
原測試只驗 URL + review-room-kit-not-started,而後者只看 !activePrimaryLease
(ReviewSessionViewerPane.tsx:324),任何 id 恆顯示、無法證明接收端重驗。改為選 active
session、由 URL 擷取真 session id,斷言 runtime-evidence 區塊回填該 id(擋空字串/參數打錯
wiring regression)且 runtime session=observed(sessionObserved 真源,
ReviewSessionViewerPane.tsx:119,314)。

兩處改用 active(非 terminating)列,因 store.list() 回傳含 closed 全量、依 updated_at
desc(sessionStore.ts:70-79 / app.ts:2753 未過濾),.first() 常抓到剛關閉的列,Review Room
會誠實回 not_listed 而假失敗。

驗證:eslint 0 / playwright --list 6 tests / 隔離 tsc --noEmit(process shim)0。
未跑:live coordinator 真瀏覽器(本環境無 live Kit GPU,測試 honest-skip,非失敗)。

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

* task#14: fix 跨軸 handoff 深層 Kit E2E 兩處 quality 發現(manual-start 啟用競態 + primary lease 收尾)

Important #1(啟用競態假性 skip):deep Kit 測試以一次性 .isEnabled() 快照判斷 review-room-manual-
start 可否 attach,但其 disabled gate(!viewerOrigin || !sessionObserved)依賴元件 on-mount 只跑一次、
不輪詢的 runtime/status fetch(ReviewSessionViewerPane.tsx:289,124-142)。按鈕無條件掛載、toBeVisible
幾乎立即通過而不等 fetch resolve,快照可能讀到 fetch 前的 disabled,把真有 live Kit session 的情境假性
skip 掉。比照本檔 M/SS leg 既有慣例,改用會自動重試的 expect().toBeEnabled({timeout}).then(true/false)。

Important #2(佔用真實 session primary lease 無收尾):測試對 live Sessions 表挑到的「真實 active
session」claimPrimary() 佔其 primary viewer lease,全程無釋放;最壞情況真人操作員近 45s(lease TTL,
viewerLeaseStore.ts:78)無法 claim 自己 session 的 primary 檢視。新增 describe 級 test.afterEach,用
獨立 request fixture 主動 POST release endpoint 歸還 lease(比照 VG-01 收尾模式;但只 release lease、
不 close 這條真實共享 session,以免破壞真人 session — VG-01 明列此跨套件干擾陷阱)。lease_id/lease_token
於點擊 manual-start 當下自 claim response 擷取(token 僅 claim 回應 includeToken 暴露, app.ts:1185)。

僅改單一測試檔 web-viewer-sample/e2e/cross-axis-handoff.spec.ts,無 production code 變動。
驗證:npx eslint(exit 0)、npx playwright test --list(6 tests 正確解析)、gitnexus detect_changes
(scope=staged:1 檔、0 production symbol、0 affected process、risk low)。

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

* fix: 補齊 cross-axis E2E best-effort catch 與 timeout 預算

- waitForResponse(?limit=50) 三處(recordsSettled/convRecordsSettled/inRecordsSettled)補 .catch(() => null):讓註解宣稱的 best-effort 名實相符,15s 內無回應不再拋例外阻斷後面的 banner/attribute 斷言(比照同檔 claimSettled 既有寫法)
- §8 lifecycle walk-through 補 test.setTimeout(180_000):fixture 齊全時測試體會疊加約 10 個網路等待,逼近/超過 playwright.config.ts 的 60s 全域上限
- §8 deep Kit test.setTimeout 300_000 → 450_000:逐步 timeout 加總約 310_000ms 原本零緩衝甚至超標,比照 viewer-embed-a1-highlight.spec.ts:46 的 2 倍緩衝慣例留出餘裕

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

* task#14: fix 跨軸 handoff E2E 補驗接收端真的帶到 id(quality Important #1)

三處 CV 接收端斷言原本只驗「有 handoff 且非 none」(toHaveURL + banner 可見 +
data-handoff-status 三態),未驗 chip 真的把 id 帶到 URL、receiver 真的 surface
出該 id;與同檔 SS→Review 已加固的模式不一致。因 minio_key/job_id 在 handoff.ts
為 optional(buildHandoff:39 丟空值、parseHandoff:51-53),chip 漏帶時仍得非 null
handoff,CV verify 落到 not_found(pages.tsx:917/909,921),與真實 ledger miss
外觀相同,原斷言完全偵測不到此 wiring 迴歸。

比照 SS→Review 既有 idiom(new URLSearchParams(...).get(k) + not.toBe(""))加固:
- M→CV(獨立)/§8 M leg:擷取 URL hash 的 minio_key 斷言非空 + banner toContainText(minioKey)
- §8 IN leg:擷取 job_id 斷言非空 + banner toContainText(jobId)
- §8 A1 CTA canOpen 分支:擷取 session 斷言非空(sender half;此分支目前 dead-in-practice,
  receiver 端重驗由 SS→Review 測試的 evidence 斷言擁有)

banner toContainText 依 handoffIdText(incomingHandoff.tsx:35)surface id,與 verify
狀態正交(not_found banner 仍含 id),不影響既有三態斷言。純新增斷言與註解,無 production
code 變動。

驗證:npx eslint e2e/cross-axis-handoff.spec.ts --max-warnings 0(exit 0)、
npx playwright test --list(6 tests 全解析)、git diff --cached --check(乾淨)、
detect_changes staged(1 file、0 production symbols、low risk)。

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

* task#14: fix §8 walk-through test.setTimeout 低估預算並修正註解

reviewer Important 發現:cross-axis-handoff.spec.ts 行176 的 test.setTimeout(180_000)
註解宣稱「~10 個循序 network waits、no measurement needed、3× headroom」,但實測
fixture-full 情境下該 test body 有 14 個有限逾時的循序 blocking waits:
M leg 70_000 + IN leg 70_000 + A1/Review-Room/A1-issue 70_000 = 210_000ms,
已超過原 180_000ms 預算(方向性低估);且 5 個 page.goto(config use{} 未設
navigationTimeout)與 3 張全頁截圖的時間未計入,retries:0 下一次「慢但會過」的
run 就會硬 flake——正是此 timeout 想防卻沒防足的失敗類型。

修法:比照同檔 deep-Kit test(行311)的實測預算做法,將 test.setTimeout 上調至
360_000(約 summed caps 的 1.7×,對齊 viewer-embed-a1-highlight.spec.ts:46 的
360_000 既有慣例),並改寫註解逐項列出 14 個 waits 的加總依據,移除
「no measurement needed」。純測試檔註解+字面值變更,無 production symbol 異動。

驗證:npx playwright test cross-axis-handoff.spec.ts --list(6 tests 正常解析);
git diff --cached --check 無 trailing whitespace;gitnexus detect_changes
(scope=staged)= 0 changed symbols / 0 affected processes / low risk。

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

* fix: 修正跨頁 handoff 缺欄位假 not_found 並補 CV/E2E/spec 缺口

四項對抗複驗 findings:

1. p5-critic honesty regression(CRITICAL):接收頁 verify 對「handoff 缺本軸
   欄位(sender 本來就不帶)」與「查過、真的沒有」不分,一律 fall through 回
   false→假 not_found(警示紅字「查無」)。新增第三態 not_applicable(中性、
   不掛 ec-warn-note、不說查無),修正三條已重現路徑:SS→A1 帶真實 active
   session(最嚴重,原本對使用中 session 假報查無)、a1-conv-link 預設無
   job_id、KG demo-row 無 session;KG/M 同類 fall-through 一併對齊為 not_applicable。
   新增 4 條回歸測試(1 hook 探針 + 3 頁面路徑)鎖住此行為。

2. p5-e2:補 CV 頁 minio_key 接收端重驗單元測試(verified 含中文 key /
   not_found / indeterminate),比照 A1 既有模式,鎖住原本零覆蓋的回歸風險。

3. p5-e3:deep-Kit E2E first_frame 180s 逾時後多讀一次 runtime/status 的
   kit_instance_bindings status,若該 session 的 binding=failed 則 skip 訊息
   標「Kit 已配置但 WebRTC/stage pipeline failed」,別於「環境無 Kit」。純加法
   讀取既有欄位、best-effort try/catch,未動 ReviewSessionViewerPane.tsx。

4. p5-spec-drift:A1→M「MinIO 來源」chip as-built 送 minio_key(key-level 更
   精確)而非 spec §4.3 範例的 prefix。於 spec §4.3 表下註、A1 chip、M prefix
   分支文件化此既知差異(minio_key 本列於型別,屬合規選擇);prefix 收件分支
   +其單元測試保留為未來「純資料夾回看」能力。

驗證:web-viewer-sample vitest 全 511 passed(含新增 7 條);tsc --noEmit 改
動檔零錯;Finding 1 走 red→green;e2e 經 playwright --list 編譯通過(deep-Kit
本環境無 live Kit,誠實 skip,未跑到 enriched 分支)。

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

* fix: 消除 CV minio_key 接收端重驗測試競態並補強 not_found 弱斷言

incomingHandoff.test.tsx 三支 CV minio_key 測試改用 waitFor 輪詢取代固定 2-tick:
- verified(中文 key,p5r2-critic-cv-miniokey-test-flaky):loadRecords 是與 load 獨立的
  useEffect async 鏈,2-tick 排不乾時載入中的空 records 也顯 not_found,會閃
  AssertionError(expected not_found to be verified)。改輪詢等 banner settle 成 verified。
- not_found(p5r2-critic-cv-miniokey-notfound-weak-assertion):先輪詢等 ledger 列
  (idempotency_key mw_r)反映到 render 再斷言,證明 loadRecords 接線完整+已反映,區隔
  「已載入且查無」與「fetch 從未發生/未反映」,修掉「兩種都會過」的弱斷言(實測:severing
  loadRecords 後本測試由通過轉為失敗,前為通過)。
- indeterminate(truncated)姊妹測試:同款輪詢,避免固定 tick 誤讀載入中的 not_found。

純測試改動,未動 production code(pages.tsx 驗證後回復 pristine)。

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

* fix: 補 ReviewRoomHandoff 缺欄位(rebase onto origin/main 後型別擴充)

rebase 到 origin/main 後,main 的 A1 3D decouple 工作(a334e49)為
ReviewRoomHandoff 新增 mappingInformationStatus/mappingIssueCode/
mappingIssueCount 三個欄位。本分支 task#12 新增的
ReviewSessionViewerPane.crosslinks.test.tsx 建構的 handoff stub 物件
未帶這三個欄位,vitest 因不做完整型別檢查而未攔到,tsc --noEmit 才抓到。
補三個 null 值(與物件內其餘診斷相關欄位一致,此測試不涉及 mapping
診斷情境)。

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

* docs(evidence): 補 P4 browser evidence 抽樣截圖與 summary(隨 PR 可審)

真實隔離 coordinator(:8005)+ 真實 IFC 轉檔(洲際好宅/給水,job
stream_conv_20260703090757_89c87388)+ 真實 seeded review session
(review_session_0e4ee1079ee1)取得的 vertical slice 證據:4 passed、
2 honest skip(MinIO 憑證缺席、deep-Kit GPU 影格未觀測)、0 failed。

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

* fix: 修復 reviewer 兩位獨立命中的接收端誠實鐵律缺口(P2…
monkey1sai added a commit that referenced this pull request Jul 14, 2026
依 README §8 守恆規則 #3(禁新增平行需求源/增補層/裁決帳),本輪交接不另寫 handoff
文件,而是內化進既有正規結構——BACKLOG 本就是「待辦佇列+OPEN 決策」的唯一入口,
PROCESS §4 本就是長壽紀律的落點。

BACKLOG.md
- §1 新增 `gap-a4-closeout`:A4 已落地,但 BCF bridge 未建、tracked browser trace 未觀測。
  2026-07-14 裁決「先把已投入的 A4 收尾到可宣稱,再開下一片」。
- §2 #7 更新:**修正過時宣稱**——原記 A4 為「deterministic ifcopenshell filters,非 LLM」,
  但 4ec21c5 已接上 Ornith vLLM semantic mode(能力現況一律以 TRUTH §1 `a4` 列為準)。
  並記入本輪裁決:下一片=A8 Synthetic Data Studio;其 ownership(service/Replicator
  runner/資料集儲存/GPU budget/§1.1 例外)**必須先做 Task 0 runtime probe 才有意義**
  ——比照 A3 clash 的 has_occ=False 教訓,未 probe 前不談 ownership、不列入 §1。
- §3 #1 閘 2 wiring:[待授權]→[已授權 2026-07-14]。補上觸發面裁決(中等寬度+豁免
  聲明;太寬會逼每個 PR 亂填 TRUTH、gate 自我腐化)與前置完成事實(branch protection
  已開,閘 2 至此才真正擋得住)。
- §3 新增 #5:前端渲染凍結 + Ai-codeing PNG 降級 + CI grep gate(已授權、未動工)。
  含風險實證下修:A4 實作採 TARGET-shell 規定的 POST /api/search/model,而非 PNG board
  的 /api/a4/semantic/query——實作者照 TARGET 走、未照圖走,TARGET 防線實際有效;本項
  價值在於擋住「下一個沒讀 TARGET 就照圖寫 code 的 agent」。

PROCESS.md §4(長壽紀律)
- D-34:main branch protection 必須維持 enforce_admins=true + require PR。
  **保護的存在不等於保護生效**——2026-07-14 因 enforce_admins=false,11 道 required
  check 對 repo owner 全部失效,7 個 commit 直接 push 進 main,導致 main 帶 typecheck
  紅燈、TRUTH 就地腐化、BACKLOG 據 stale TRUTH 產生錯誤前提。驗證要看 enforce_admins,
  不是看 required check 清單長度。
- D-35:閘不得擋住自己要求的動作。任何「未取得 X 就拒絕 Y」的 gate 必須先放行「產生 X
  的手段」;PreToolUse hook 一律自 stdin 讀 tool_input.command 自我把關,不把正確性押在
  settings 的宣告式條件上(回歸測試:scripts/tests/test-require-gstack-evidence.ps1)。

行數:BACKLOG 83/220、PROCESS 158/320,均遠低於 README §5 預算。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3Mx7VchiGQwk3eUhxm43Z
monkey1sai added a commit that referenced this pull request Jul 14, 2026
依 README §8 守恆規則 #3(禁新增平行需求源/增補層/裁決帳),本輪交接不另寫 handoff
文件,而是內化進既有正規結構——BACKLOG 本就是「待辦佇列+OPEN 決策」的唯一入口,
PROCESS §4 本就是長壽紀律的落點。

BACKLOG.md
- §1 新增 `gap-a4-closeout`:A4 已落地,但 BCF bridge 未建、tracked browser trace 未觀測。
  2026-07-14 裁決「先把已投入的 A4 收尾到可宣稱,再開下一片」。
- §2 #7 更新:**修正過時宣稱**——原記 A4 為「deterministic ifcopenshell filters,非 LLM」,
  但 4ec21c5 已接上 Ornith vLLM semantic mode(能力現況一律以 TRUTH §1 `a4` 列為準)。
  並記入本輪裁決:下一片=A8 Synthetic Data Studio;其 ownership(service/Replicator
  runner/資料集儲存/GPU budget/§1.1 例外)**必須先做 Task 0 runtime probe 才有意義**
  ——比照 A3 clash 的 has_occ=False 教訓,未 probe 前不談 ownership、不列入 §1。
- §3 #1 閘 2 wiring:[待授權]→[已授權 2026-07-14]。補上觸發面裁決(中等寬度+豁免
  聲明;太寬會逼每個 PR 亂填 TRUTH、gate 自我腐化)與前置完成事實(branch protection
  已開,閘 2 至此才真正擋得住)。
- §3 新增 #5:前端渲染凍結 + Ai-codeing PNG 降級 + CI grep gate(已授權、未動工)。
  含風險實證下修:A4 實作採 TARGET-shell 規定的 POST /api/search/model,而非 PNG board
  的 /api/a4/semantic/query——實作者照 TARGET 走、未照圖走,TARGET 防線實際有效;本項
  價值在於擋住「下一個沒讀 TARGET 就照圖寫 code 的 agent」。

PROCESS.md §4(長壽紀律)
- D-34:main branch protection 必須維持 enforce_admins=true + require PR。
  **保護的存在不等於保護生效**——2026-07-14 因 enforce_admins=false,11 道 required
  check 對 repo owner 全部失效,7 個 commit 直接 push 進 main,導致 main 帶 typecheck
  紅燈、TRUTH 就地腐化、BACKLOG 據 stale TRUTH 產生錯誤前提。驗證要看 enforce_admins,
  不是看 required check 清單長度。
- D-35:閘不得擋住自己要求的動作。任何「未取得 X 就拒絕 Y」的 gate 必須先放行「產生 X
  的手段」;PreToolUse hook 一律自 stdin 讀 tool_input.command 自我把關,不把正確性押在
  settings 的宣告式條件上(回歸測試:scripts/tests/test-require-gstack-evidence.ps1)。

行數:BACKLOG 83/220、PROCESS 158/320,均遠低於 README §5 預算。


Claude-Session: https://claude.ai/code/session_01G3Mx7VchiGQwk3eUhxm43Z

Co-authored-by: Claude Opus 4.8 <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