fix(coordinator): storageRoot host-native fallback 修同步下載 502 - #94
Conversation
`bim-review-coordinator/src/config.ts` `loadConfig` 對 `storageRoot` 的 default 寫死 `/workspace/storage`(docker container mount point)。host-native(Windows / Linux)跑時若沒設 `STORAGE_ROOT` env,node 把字串當 host 絕對路徑,Windows 上 `fs.mkdir` 拋 `ENOTDIR: not a directory, mkdir 'C:\workspace'`,導致 `POST /api/external/ifc-ready` 的 `ifcDownloader.downloadIfcToSharedVolume` 全數 502 `write_failed`,fast-ifc-link-demo-loop happy path 走不通。 旁邊 `storageHostRoot` 已有 `path.join(cwd, "storage")` fallback,只有 `storageRoot` 漏接。本 commit 把 default 對齊,host-native 直接落在 `<cwd>/storage`(等同 `bim-review-coordinator/storage/`)。 - `src/config.ts:206` storageRoot default 從 `/workspace/storage` 改成 `path.join(cwd, "storage")`;欄位註解同步更新 - `src/services/ifcDownloader.ts:15` JSDoc 註解說明 docker compose / host-native 兩種來源 - `tests/config.test.ts` 加 `loadConfig storageRoot fallback` describe block, 2 個 test(default 走 cwd / 顯式 STORAGE_ROOT 保留) 不改 spec / capability:`fast-ifc-link-demo-loop` (PR #92, archived 2026-05-21) implementation drift fix。`local-coordinator-ifc-ready-intake-boundary` SHALL 行為(同步下載 + 502 on failure)語意不變,只修 host-native default 不可用。 docker compose 行為驗證:`compose.runtime-manager.yml:53,94` 顯式設 `STORAGE_ROOT: /workspace/storage`,env override 不受 default 改動影響。 驗證(L1): - `cd bim-review-coordinator && npm run verify`:11 files / 168 tests passed (+2 new storageRoot fallback tests) L4 真實 runtime 重跑 Postman happy path 跳過:會中斷既有 host-native coordinator service(:8004),建議 merge 後手動重啟並重跑。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR makes storage root configuration path-relative instead of container-absolute by defaulting to ChangesStorage root configuration and fallback behavior
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR hotfixes the bim-review-coordinator host-native default for storageRoot so synchronous IFC downloads write into a valid local directory when STORAGE_ROOT is not provided, preventing POST /api/external/ifc-ready from failing with 502 due to invalid path creation on Windows/Linux host-native runs.
Changes:
- Change
loadConfig()storageRootdefault from hard-coded/workspace/storagetopath.join(process.cwd(), 'storage')(aligning with the existingstorageHostRootfallback). - Clarify
IfcDownloadOptions.storageRootJSDoc to document docker-compose vs host-native behavior. - Add unit tests verifying
storageRootfallback behavior and that explicitSTORAGE_ROOTis respected.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
bim-review-coordinator/src/config.ts |
Adjusts storageRoot default to <cwd>/storage to work on host-native environments while still allowing docker-compose override via env. |
bim-review-coordinator/src/services/ifcDownloader.ts |
Updates JSDoc to reflect the intended source of storageRoot under docker-compose vs host-native usage. |
bim-review-coordinator/tests/config.test.ts |
Adds coverage for the new storageRoot fallback and env override behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…OOT,IFC 落對 shared volume `compose.runtime-manager.yml` `coordinator` service environment 漏設 `STORAGE_ROOT` 與 `STORAGE_HOST_ROOT`(對比 `streaming-server` line 94 / `kit-manager-api` line 53 都顯式設 `STORAGE_ROOT: /workspace/storage`)。PR #94 補了 host-native node 的 default fallback,但在 docker container 內 coordinator cwd 是 `/workspace/bim-review-coordinator`, fallback 解到 container-local path `/workspace/bim-review-coordinator/storage/`,**避開** docker shared volume mount point `/workspace/storage`,IFC bytes 寫進 container local fs, host 與 host-native streaming-server 都拿不到。 ## Diagnostic(2026-05-22,with PR #94 deployed,container rebuild 後) - ① POST /api/external/ifc-ready → 202 dispatched,`download_status: downloaded` ✓ - 但 `local_path` 寫成 `/workspace/bim-review-coordinator/storage/ifc-cache/<job>/source.ifc` - container 內 `/workspace/storage/ifc-cache/` 為空(mount destination 沒被寫到) - streaming-server(host-native :49101)從 host `<repo>\storage\ifc-cache\` 拿不到檔案 - conversion_status 卡 `queued` 不前進 ## Fix - coordinator service env 加 `STORAGE_ROOT: /workspace/storage`(對齊 mount destination) - coordinator service env 加 `STORAGE_HOST_ROOT: ${RUNTIME_STORAGE_ROOT:-./storage}`, 與 mount source 同 expansion;預設 `./storage`(相對 docker compose cwd) - inline 註解標明:user 為 host-native streaming-server 場景應在 `.env` 設 `RUNTIME_STORAGE_ROOT=<host absolute path>`(例: `C:\Repos\active\iot\AI-BIM-governance\storage`),否則 streaming-server 收到的 `host_local_path` 是 relative 字串,host fs 解析依賴 streaming-server 啟動 cwd。 ## Note(超出本 fix 範圍) `bim-streaming-server/source/.../conversion_authority.py` `_ifc_artifact` 目前**未接** dispatch payload 內的 `host_local_path` / `local_path` 欄位,僅讀 `ifc_artifact.url` (HTTP URL fetch)。本 fix 只負責 coordinator 寫對位置 + dispatch payload schema 正確, streaming-server 端的 `host_local_path` 消費邏輯應由另一個 change 補。 ## Verification - `docker compose -p hotfix-validate -f compose.runtime-manager.yml -f compose.host-kit.yml --env-file .env.web-plane.host-kit.example config`: STORAGE_ROOT / STORAGE_HOST_ROOT 正確 expansion(`./storage` fallback,因 .env 未設 RUNTIME_STORAGE_ROOT) - `cd bim-review-coordinator && npm run verify`:11 files / 168 tests passed (不動 TS code,無 regression) 不改 `.env.web-plane.host-kit.example`(權限限制無法讀寫),用 compose inline 註解 導引 user 設置。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… shared volume (streaming-server-prefer-local-ifc-path) 完整 OpenSpec change(scaffold + apply 同 commit;模仿 fast-ifc-link-demo-loop PR #92 pattern)。 ## Why fast-ifc-link-demo-loop(PR #92,archived 2026-05-21)宣告 coordinator → streaming-server dispatch payload 加 `local_path` / `host_local_path`,並寫進 spec delta:**streaming-server SHALL prefer host_local_path when present...**。但 streaming-server 端**完全沒實作**這個 consumer 行為(`conversion_authority._ifc_artifact` 只認 url,`ifc2usdc_powershell_adapter._url_to_local_path` HTTP scheme 直接 fail)。 PR #94/#95 修好 coordinator 端(host-native default + compose env);2026-05-22 重跑 Postman 證實 IFC 已正確落到 shared volume,但 `conversion_status` 卡 queued、`viewer_url: null`。 本 change 補完 streaming-server consumer drift,讓 fast-mvp happy path 真實跑通。 ## What changed ### Streaming-server source(2 files / +54 lines) - `conversion_authority._ifc_artifact`:return dict propagate `local_path` / `host_local_path` (lineage / debug visibility) - `ifc2usdc_powershell_adapter.Ifc2UsdcPowershellConverterAdapter`: - `__init__` 加 `storage_root: Path | None`,resolve env `STORAGE_ROOT` 或 cwd - `_resolve_local_ifc` 新解析順序:`host_local_path` → `local_path` → 既有 url 解析 - 新 helper `_try_local_path`:storage_root sandbox(path traversal hard-fail)+ 不存在 soft fallback ### Streaming-server tests(2 files / +167 lines) - `test_host_native_conversion_service.py`:加 5 個 adapter `_resolve_local_ifc` test - host_local_path 在 storage_root 內 + 存在 → 直接用,跳過 url - local_path 為 fallback(host_local_path 不在時) - 兩者 path 在 storage_root 內但檔案不存在 → soft fallback url(edge-local://) - host_local_path 在 storage_root 外 → raise `invalid_ifc_input` - regression:既有 edge-local:// url-only payload 仍能解析 - `test_conversion_authority_api.py`:加 2 個 `_ifc_artifact` propagation test - local_path / host_local_path 存在時 propagate 進 return dict - 不存在時 default None ### OpenSpec change(5 files / +372 lines) `openspec/changes/streaming-server-prefer-local-ifc-path/`: - `proposal.md`:why / what / non-goals / capability impact - `design.md`:resolution order / sandboxing / backward compat / failure semantics - `tasks.md`:12 phase tasks(0 setup → 11 archive) - `acceptance.md`:L1-L5 acceptance - `specs/conversion-webhook-lifecycle/spec.md`:`## ADDED Requirements` `Streaming-server consumes shared-volume local IFC path before url fetch` + 3 Scenarios ## Blast radius GitNexus pre-impact:`_ifc_artifact` / `_resolve_local_ifc` / `_url_to_local_path` 全 **LOW** (direct d=1 callers 在同 module/class,affected processes = `create_conversion` + `convert`, 都是預期內) ## Verification(L1-L3) | Level | Result | |---|---| | L1 `cd bim-streaming-server && pytest tests -q` | **31 passed**(原 23 + 新 8 cases) | | L1 `cd bim-review-coordinator && npm run verify` | 11 files / **168 tests passed**(regression OK,coordinator side 不動) | | L1 root `pytest tests -p no:cacheprovider` | **9 passed**(contracts / fakes regression OK) | | L2 `openspec validate streaming-server-prefer-local-ifc-path --strict` | **valid** | | L2 `openspec validate --specs --strict` | 26 passed / 0 failed | | L3 GitNexus impact (pre-change) | LOW for `_ifc_artifact` / `_resolve_local_ifc` / `_url_to_local_path` | | L4 真實 runtime end-to-end | **跳過** — 需重啟既有 host-native streaming-server(:49101),建議 merge 後重啟並重跑 Postman 驗證 conversion_status → ready + viewer_url 出現 | ## Predecessor / Follow-up ✓ Predecessor:`fast-ifc-link-demo-loop`(PR #92, archived PR #93) + hotfix PR #94(coordinator host-native fallback)+ PR #95(compose env) ✗ 仍 open follow-up(本 change 不做): - streaming-server docker deployment 的 dual-fs 議題(scope = host-native fast MVP only) - `main spec body` 缺少 fast-ifc-link-demo-loop 的 `Coordinator dispatch payload carries local path references` requirement entry(archive PR #93 只加了 implementation status note;archive 本 change 時可順手補) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ecs + roadmap (#97) PR #96(implementation,squash `3c7dbd5`)merged 後的 OpenSpec sync/archive cleanup。 - `git mv openspec/changes/streaming-server-prefer-local-ifc-path/` → `openspec/changes/archive/2026-05-22-streaming-server-prefer-local-ifc-path/` - `openspec/specs/conversion-webhook-lifecycle/spec.md` 加 2 個 ADD requirement: 1. `Coordinator dispatch payload carries local path references`(backfill `fast-ifc-link-demo-loop` archive PR #93 缺漏的 requirement body,3 Scenarios) 2. `Streaming-server consumes shared-volume local IFC path before url fetch`(本 change 新 ADD,3 Scenarios) + 對應的 implementation status note(2026-05-21 與 2026-05-22 兩段) - `docs/plans/AI-BIM-governance-saas-roadmap-2026-05.md` 加 archive 摘要(2026-05-22 fast-mvp loop final landing,含 hotfix bundle PR #94 / #95 / #96) `openspec validate --specs --strict` = **26 passed / 0 failed** ## fast-mvp loop 收尾完成 兩段 OpenSpec change archive(`remove-conflict-review-from-fast-mvp` + `fast-ifc-link-demo-loop`)+ 三段 hotfix(PR #94 coordinator default、PR #95 compose env、PR #96 streaming-server consumer),把外部 IFC Worker → coordinator → streaming-server → viewer 連結這條 happy path 從 spec 到 code 完整對齊。 L4 真實 happy path 需 user 設 `.env` `RUNTIME_STORAGE_ROOT=<host absolute path>`、recreate coordinator container、重啟 host-native streaming-server 帶 `STORAGE_ROOT=<host absolute path>`,本 archive 不包含此環境設定步驟(留給 user runbook)。 Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…_url(coordinator-auto-poll-streaming-conversion) (#98) 完整 OpenSpec change(scaffold + apply 同 commit)。 ## Why fast-mvp loop 三段 archive 收尾後驗證,`POST /api/external/ifc-ready` → coordinator dispatch → streaming-server 真實轉檔 succeeded(40s)— **但 coordinator 端 `conversion_status` 永遠停在 `queued`**:既有 spec `Coordinator ingests host-native conversion result into callback outbox` 已寫「SHALL ingest through polling, an internal result loop, or an equivalent internal callback」,但 polling / internal result loop 都未實作。`fetchConversionResult` 只在 manual `POST /api/internal/conversions/<id>/ingest` 端點被呼叫;沒有自動 caller,viewer_url 永不出現。 ## What changed(10 files / +968 / -50) ### Coordinator src(3 files) - `services/streamingConversionClient.ts`:加 module-level `isTerminalConversionResult` helper(terminal 判定抽出來避免雙處硬編)+ class method `pollConversionResult(jobId, options): PollerHandle`:setTimeout chain(避免 setInterval overlap)、cancel()、max attempts → poll_timeout 走 fake failed result 餵 onTerminal - `config.ts`:加 `conversionPollEnabled` / `conversionPollIntervalSeconds`(default 5)/ `conversionPollMaxAttempts`(default 60 = 5 min)+ `parseBooleanEnv` helper - `app.ts`: - `CoordinatorApp` interface 加 `dispose: () => void` - 加 module-scope `pollerRegistry: Map<conversion_job_id, PollerHandle>` - refactor 既有 manual ingest handler 內邏輯抽成共用 helper `ingestStreamingConversionResult(jobId, { result?, source })`(manual + auto-poll 共用) - 加 `schedulePollerForConversion(jobId)`:closure wrapper for `streamingConversionClient.pollConversionResult` + onTerminal=ingest helper + registry cleanup - dispatch path:`markDispatched` 之後若 `config.conversionPollEnabled && !pollerRegistry.has(id)` → `schedulePollerForConversion(id)` - manual ingest handler 開頭 cancel + delete registry(避免雙 ingest) - return 加 `dispose`:遍歷 registry cancel 所有 timer ### Coordinator tests(2 files) - `unit_kitpool.test.ts`:fixture 加 3 個新 config field(default `false` 避免 unit test 啟 timer) - `auto-poll-conversion.test.ts`(新):5 case - dispatch → poller queued/queued/ready 序列 → 自動 ingest 出 viewer_url - dispatch → poller queued/failed → 自動 ingest 為 failed,viewer_url 不出現 - 重複 idempotent dispatch 不雙起 poller(stub /result 呼叫次數有 budget) - manual ingest 觸發 cancel auto poller(stub /result 只被打 1 次) - `conversionPollEnabled: false` fixture 不啟 poller ### OpenSpec change(5 files) - proposal / design / tasks / acceptance / `conversion-webhook-lifecycle` MODIFIED requirement + 3 新 Scenario ## GitNexus blast radius = LOW | symbol | risk | d=1 | |---|---|---| | `fetchConversionResult` | LOW | `createCoordinatorApp`(同 module) | | `createCoordinatorApp` | LOW | 無 upstream(main entry) | | `loadConfig` | LOW | (PR #94 同樣 path,LOW) | ## Verification | Level | Result | |---|---| | L1 coordinator `npm run verify` | 12 files / **173 tests passed**(168 既有 + 5 新 auto-poll) | | L1 streaming-server `pytest tests -q` | **31 passed**(不動,regression OK) | | L1 root `pytest tests` | **9 passed** | | L2 `openspec validate coordinator-auto-poll-streaming-conversion --strict` | **valid** | | L2 `openspec validate --specs --strict` | **26 passed / 0 failed** | | L3 GitNexus pre-impact | LOW for `fetchConversionResult` / `createCoordinatorApp` | | **L4 真實 runtime** | **跳過** — 留 merge 後 docker compose recreate coordinator(讀新 code)+ 跑 Postman ① + 等(不手動 POST ingest)→ ② Poll 預期 5-90 秒內 `viewer_url` 自動出現 | ## Predecessor / Follow-up ✓ Predecessor:`streaming-server-prefer-local-ifc-path`(PR #96 / archive PR #97)+ PR #94 / PR #95 hotfix bundle 本 change 是 fast-mvp loop 自動化的最後一片拼圖,merge 後外部 caller 不再需要任何手動 trigger 就能拿到 viewer_url。 不解 / 排除: - 不持久化 poller state(coordinator restart in-memory timers lost;手動 endpoint 仍可救) - 不解 cloud callback outbox retry / dead-letter(另一條既有 capability,本 change 不動) - 不引入 streaming-server push callback(本 change 走 coordinator pull) - 不引入第三方 scheduler library Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…步 specs + roadmap (#99) PR #98(implementation,squash `2103bf7`)merged 後的 OpenSpec sync/archive cleanup。 - `git mv openspec/changes/coordinator-auto-poll-streaming-conversion/` → `openspec/changes/archive/2026-05-22-coordinator-auto-poll-streaming-conversion/` - `openspec/specs/conversion-webhook-lifecycle/spec.md` MODIFIED `Coordinator ingests host-native conversion result into callback outbox` requirement:加 SHALL auto-poll 子條款(env config 三項可調)+ 3 新 Scenario(dispatch auto-schedules / manual de-dup / poll timeout failed-equivalent)+ implementation status note - `docs/plans/AI-BIM-governance-saas-roadmap-2026-05.md` 加 archive 摘要(2026-05-22 fast-mvp loop zero-touch automation 完成) `openspec validate --specs --strict` = **26 passed / 0 failed** ## fast-mvp loop zero-touch 收尾完成 4 段 OpenSpec change archive(`remove-conflict-review-from-fast-mvp` + `fast-ifc-link-demo-loop` + `streaming-server-prefer-local-ifc-path` + `coordinator-auto-poll-streaming-conversion`)+ 4 段 hotfix bundle(PR #94 / #95 / #96 / #98)整體把外部 IFC Worker → coordinator → streaming-server → viewer 連結這條 happy path 從 spec 到 code、從 manual 到 zero-touch 完整對齊並驗證。 L4 真實 runtime 第一次 zero-touch end-to-end 跑通(2026-05-22):dispatch → 40 秒內 viewer_url 自動出現,無 manual ingest trigger。 Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Hotfix:
bim-review-coordinator/src/config.tsloadConfig對storageRoot的 default 寫死/workspace/storage(docker container mount point)。Host-native(Windows / Linux)跑時沒設STORAGE_ROOTenv,node 把字串當 host 絕對路徑,Windows 上fs.mkdir拋ENOTDIR: not a directory, mkdir 'C:\workspace',導致POST /api/external/ifc-ready的ifcDownloader.downloadIfcToSharedVolume全數 502write_failed,fast-ifc-link-demo-loop (PR #92, archived 2026-05-21) happy path 走不通。旁邊
storageHostRoot已有path.join(cwd, "storage")fallback,只有storageRoot漏接。本 PR 把 default 對齊,host-native 直接落在<cwd>/storage(等同bim-review-coordinator/storage/)。重現
2026-05-22 用 Postman collection
docs/postman/fast-ifc-link-demo.postman_collection.json預設值對 host-native coordinator(:8004)送POST /api/external/ifc-ready,coordinator 回:改動(3 files / +25 / -3)
bim-review-coordinator/src/config.tsstorageRootdefault/workspace/storage→path.join(cwd, \"storage\");欄位註解同步更新bim-review-coordinator/src/services/ifcDownloader.tsbim-review-coordinator/tests/config.test.tsloadConfig storageRoot fallbackdescribe,2 個 test(default → cwd / 顯式 env 保留)Spec / Capability impact = NONE
fast-ifc-link-demo-loop(PR #92) implementation drift fix。local-coordinator-ifc-ready-intake-boundarySHALL 行為(同步下載 + 502 on failure)語意完全不變,只修 host-native default 不可用 → 因此不開新 OpenSpec change、不動openspec/specs/。Docker compose 行為驗證
compose.runtime-manager.yml:53,94明確設STORAGE_ROOT: /workspace/storage(env override 拿到 default 之前),改 default 不影響 container 路徑。GitNexus impact
mcp__gitnexus__impact({target:\"loadConfig\", direction:\"upstream\"})= LOW(direct = 0,只在 startup 被 import,不改 signature / 欄位語意,只動 default value)。Verification
npm run verifyTest plan
🤖 Generated with Claude Code
Summary by CodeRabbit
Configuration Improvements
Documentation