Skip to content

docs(openspec): 建立 OpenSpec 分支隔離與 PR 流程 - #9

Closed
monkey1sai wants to merge 6 commits into
mainfrom
codex/openspec/introduce-worker-review-session-lifecycle
Closed

monkey1sai wants to merge 6 commits into
mainfrom
codex/openspec/introduce-worker-review-session-lifecycle

Conversation

@monkey1sai

@monkey1sai monkey1sai commented May 7, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Worker service for artifact upload, conversion jobs, and object serving; review-session request flow; artifact-group readiness; multi-artifact bindings with routing; session-first viewer bootstrap.
  • Enhancements

    • Expanded session lifecycle/events and kit-instance lifecycle; artifact readiness statuses; viewer UI shows binding details and handles blocked/queued states.
  • Documentation

    • Worker API contract, runbook, contracts/specs, and demo/startup guidance updated.
  • Tests

    • Extensive unit/integration tests and smoke scripts added.
  • Chores

    • Updated .gitignore to ignore local/dev artifacts.

@coderabbitai

coderabbitai Bot commented May 7, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a filesystem Worker service and APIs for artifacts/conversions, BIM Control review-session-requests and artifact-groups, coordinator lifecycle and kit-instance bindings, streaming load-by-bindings, session-first viewer/client changes, extensive tests/specs, scripts, docs, and .gitignore updates.

Changes

Worker-led review session lifecycle

Layer / File(s) Summary
Data & Contracts
_worker/app/models.py, _worker/app/settings.py, bim-review-coordinator/src/types.ts, web-viewer-sample/src/types/*, _bim-control/app/main.py (schemas)
Adds/extends models for artifact intake, conversion requests/options, artifact bindings, kit-instance bindings, review-session request shapes, settings resolution, and lifecycle enums.
Worker API & Store
_worker/app/main.py, _worker/app/store.py, _worker/requirements.txt
Creates FastAPI app with CORS and routes (health, artifacts, artifact-groups/readiness, conversions, objects), worker store implementation for source artifacts/conversions/derived outputs, and requirements file.
BIM Control
_bim-control/app/main.py, _bim-control/tests/*
Adds artifact-group endpoints, review-session-request create/get/patch, lifecycle events storage, readiness/blocker computation, seed data, CORS, and tests.
Coordinator orchestration
bim-review-coordinator/src/{app.ts,services/*,socket/*}
Session creation builds artifact_bindings, allocates kit_instance_bindings with routing_policy, returns queued 409 when capacity missing, adds close endpoint and mutability guards, session store updates and helpers.
Streaming runtime
bim-streaming-server/.../stage_loading.py, bim-streaming-server/scripts/tests/*
Stage-loading resolves artifact_bindings by load_order, reports applied_mode/missing_paths/fallbacks, adds loadArtifactGroupRequest handler and contract tests.
Viewer / Clients
web-viewer-sample/src/*, web-viewer-sample/scripts/*, web-viewer-sample/package.json
Session-first bootstrap, lifecycle-aware UI, artifact_bindings support in ArtifactPanel and streamMessages, Coordinator/BIM Control client updates, verification script and npm test:session-first.
Tests / Specs / Docs / Scripts / .gitignore
AGENTS.md, CLAUDE.md, README.md, docs/contracts/*, openspec/**, scripts/*, .gitignore, _worker/{README.md,AGENTS.md}
Docs and OpenSpec specs updated to introduce _worker as artifact+conversion facade, start/stop/health/smoke scripts, verification and contract tests, and .gitignore additions.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant Worker
  participant BIMControl
  participant Coordinator
  participant Streaming
  Client->>Worker: POST /api/artifacts
  Client->>Worker: POST /api/conversions
  Worker-->>Client: conversion job/result (derived URLs)
  Worker->>BIMControl: POST conversion result (metadata/URLs)
  Client->>BIMControl: POST /api/review-session-requests
  BIMControl-->>Client: created + artifact_bindings (or blocked_conversion)
  Client->>Coordinator: POST /api/review-sessions (bindings,routing)
  alt capacity unavailable
    Coordinator-->>Client: 409 queued_for_instance (returns artifact_bindings)
  else capacity available
    Coordinator-->>Client: session + stream-config(active)
    Client->>Streaming: openStageRequest (artifact_bindings)
    Streaming-->>Client: openedStageResult (applied_mode, missing_paths)
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

I sprint to port eight-zero-oh-five,
Turn bytes to binds, then streams arrive.
Control writes notes, the kits align,
The viewer hums in tidy line.
With hops and logs and tests that sing,
A rabbit ships the streaming spring. 🐇✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/openspec/introduce-worker-review-session-lifecycle
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch codex/openspec/introduce-worker-review-session-lifecycle

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

✅ Created PR with unit tests: #10

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

✅ Unit tests committed locally. Commit: b121ec28ffb32bf0b05237f8510cf8e5278a48de

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

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

⚠️ Outside diff range comments (4)
CLAUDE.md (1)

152-169: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Outdated diagram: Missing _worker boundary in overview.

The "核心 repo 的定位總覽" Mermaid diagram at lines 152-169 still shows the old flow where BC (bim-control) directly queries S3 (storage) for file URLs, and KIT loads USD/USDC from S3.

According to the updated architecture in the main AGENTS.md (lines 149-171), this flow should route through _worker (WK) as the artifact facade, with _s3_storage as an optional legacy mirror.

📊 Proposed fix to align diagram with new architecture
 ```mermaid
 flowchart LR
     BC["_bim-control\nFake BIM Data Authority"]
+    WK["_worker\nArtifact + Conversion Facade"]
     S3["_s3_storage\nFake Object Storage"]
+    CS["_conversion-service\nLegacy Conversion API"]
     CO["bim-review-coordinator\nSession / Control Plane"]
     KIT["bim-streaming-server\nOmniverse Kit Runtime"]
     WV["web-viewer-sample\nBrowser Client"]

     WV -->|REST: create / join session| CO
     CO -->|REST: project / version / issue metadata| BC
-    BC -->|file url / artifact url| S3
+    CO -->|REST: review artifact binding| WK
+    WK -->|metadata only| BC
+    WK -->|compatibility adapter| CS
+    WK -->|optional static mirror| S3
     WV -->|WebRTC video + DataChannel JSON| KIT
-    KIT -->|load USD / USDC by URL| S3
+    KIT -->|load USD / USDC by URL| WK
     WV -->|Socket.IO / WebSocket state events| CO
     CO -->|optional collaboration state| KIT
     WV -->|annotation / issue interaction| CO
     CO -->|persist fake review data| BC
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @CLAUDE.md around lines 152 - 169, The Mermaid overview diagram is outdated:
it omits the _worker (WK) artifact facade and still routes artifact access
directly between BC and S3 and KIT and S3; update the diagram to insert WK
("_worker\nArtifact + Conversion Facade") and CS ("_conversion-service\nLegacy
Conversion API"), change the flow so CO -> WK (review artifact binding), WK ->
BC (metadata only), WK -> CS (compatibility adapter), WK -> S3 (optional static
mirror), and KIT -> WK (load USD/USDC by URL) while keeping other edges (WV->CO,
WV->KIT, CO->BC, CO->KIT, CO->BC persisting) intact so the diagram matches
AGENTS.md architecture.


</details>

</blockquote></details>
<details>
<summary>bim-review-coordinator/src/socket/reviewNamespace.ts (1)</summary><blockquote>

`75-90`: _⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_

**`leaveSession` must not require `isSessionMutable` — risk of permanently stale presence data.**

`leaveSession` is a cleanup/departure operation that should succeed even when the session has ended. When a session transitions to non-mutable (completed, cancelled, etc.) while participants are still connected, those participants can no longer call `leaveSession`:

- `store.leave()` is never called → participant remains in the store
- `presenceUpdated` is never broadcast → other clients see a phantom participant
- No `socket.on("disconnect")` handler is visible in this file to compensate

`joinSession`, `highlightRequest`, `selectionUpdate`, and `annotationCreate` blocking on `isSessionMutable` is correct. `leaveSession` is the exception — it should skip the mutability gate.

<details>
<summary>🐛 Proposed fix: introduce a separate <code>validateSessionExists</code> for leave</summary>

```diff
  socket.on("leaveSession", (payload: SessionPayload, ack?: (response: unknown) => void) => {
-   const sessionCheck = validateExistingSession(store, payload);
+   const sessionCheck = validateSessionExists(store, payload);
    if (!sessionCheck.ok) {
      ack?.(sessionCheck);
      return;
    }

Add the new helper below validateExistingSession:

+function validateSessionExists(
+  store: SessionStore,
+  payload: SessionPayload,
+): { ok: true; sessionId: string } | { ok: false; error: string } {
+  const sessionId = payload.session_id;
+  if (!sessionId) return { ok: false, error: "Missing session_id" };
+  if (!isSafeSessionId(sessionId)) return { ok: false, error: "Invalid review session id." };
+  if (!store.get(sessionId)) return { ok: false, error: "Review session not found." };
+  return { ok: true, sessionId };
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bim-review-coordinator/src/socket/reviewNamespace.ts` around lines 75 - 90,
The leaveSession handler currently calls validateExistingSession which blocks
when a session is non-mutable; change it to allow departures by adding a new
helper validateSessionExists (or similar) that only verifies the session exists
and returns {ok, sessionId} without checking isSessionMutable, then use that
helper in the socket.on("leaveSession", ...) flow so socket.leave,
store.leave(sessionId, userId), and
namespace.to(sessionId).emit("presenceUpdated", ...) always run for existing
sessions; ensure you still ack with the returned result (ack?.({ok:true}) or the
error object) and keep references to validateExistingSession, store.leave,
namespace.to(...).emit, and the leaveSession socket handler when making the
change.
docs/contracts/local-dev-runbook.md (1)

40-71: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

先補上 coordinator / viewer 的 nvm 初始化步驟。

這份 runbook 現在直接指示執行 npm install / npm run dev,但 repo 規則要求 bim-review-coordinator 和 web-viewer-sample 在啟動前先載入 nvm。少了這一步,開發者很容易用到錯的 Node 版本,整條本機驗證流程會先卡在啟動階段。

As per coding guidelines, "{bim-review-coordinator,web-viewer-sample}/**/*.{js,ts,json}: Node.js services require nvm initialization before startup: source nvm with export NVM_DIR="$HOME/.nvm" && . "$NVM_DIR/nvm.sh" before running npm or Node commands".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/contracts/local-dev-runbook.md` around lines 40 - 71, Update the runbook
sections for bim-review-coordinator and web-viewer-sample to require nvm
initialization before any npm/Node commands: explicitly instruct developers to
source the NVM environment (initialize NVM_DIR and load nvm.sh) prior to running
npm install or npm run dev in the bim-review-coordinator and web-viewer-sample
steps so the correct Node version is used per the repo policy.
_worker/app/models.py (1)

33-38: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused generate_mapping from ConversionOptions.

ConversionRequest has a top-level generate_mapping: bool = True (line 36) and ConversionOptions also defines generate_mapping: bool = True (line 29). Code analysis shows only the top-level field is used: main.py calls request.model_dump() which passes the nested structure to store.py, but store.py accesses only the top-level field via request.get("generate_mapping", True). The nested field is dead code and should be removed to eliminate confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_worker/app/models.py` around lines 33 - 38, ConversionOptions currently
duplicates the generate_mapping flag that's actually used at the top-level in
ConversionRequest; remove the dead field from the ConversionOptions class
(delete the generate_mapping declaration in ConversionOptions) and ensure no
code references ConversionOptions.generate_mapping elsewhere (search for
ConversionOptions and remove or refactor any use), leaving
ConversionRequest.generate_mapping as the single source of truth used by main.py
(via request.model_dump()) and store.py (which reads
request.get("generate_mapping", True)).
🧹 Nitpick comments (9)
CLAUDE.md (1)

780-780: 💤 Low value

Outdated reference: Should mention _worker as new facade boundary.

Line 780 states "_conversion-service 已保留在本 workspace 作為 _worker 後方的 legacy conversion compatibility path,而不是新的對外檔案/轉檔邊界" but the context in section 9 doesn't clearly establish what IS the new external boundary.

Consider clarifying that _worker is the new external artifact/conversion boundary, while _conversion-service serves as its internal compatibility adapter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` at line 780, The doc text currently leaves ambiguity about the
external boundary; update the wording in section 9 to explicitly state that
`_worker` is the new external facade/artifact and conversion boundary, while
`_conversion-service` is retained only as an internal legacy compatibility
adapter behind `_worker`; replace the existing sentence about
`_conversion-service` being "retained... as `_worker` 後方的 legacy conversion
compatibility path" with a clear statement that `_worker` is the outward-facing
boundary and `_conversion-service` is an internal adapter used only for backward
compatibility, and adjust any surrounding references to consistently treat
`_worker` as the public conversion interface.
docs/contracts/streaming-datachannel-events.md (1)

45-45: ⚡ Quick win

補一個 single_binding_only 的完整 response 範例會更可測

Line 45 提到 partial_load 與 skipped_artifact_ids[],但目前 JSON 範例沒有對應片段。建議新增一段 partial mode 的 openedStageResult 範例,避免實作方各自解讀。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/contracts/streaming-datachannel-events.md` at line 45, The docs mention
applied_mode="single_binding_only", partial_load, and skipped_artifact_ids[] but
the JSON example for openedStageResult is missing a corresponding partial-load
example; add a complete openedStageResult JSON snippet showing applied_mode set
to "single_binding_only", partial_load:true, skipped_artifact_ids listing the
skipped artifact IDs, and missing_paths for bindings without URLs so
implementers have a concrete example to follow (update the example block that
contains openedStageResult and any related sample fields).
bim-review-coordinator/tests/sessions.test.ts (1)

176-199: ⚡ Quick win

把 queued_for_instance 的 artifact_bindings 也鎖進測試。

這個案例是新 lifecycle contract 的一部分,但目前只驗 409 和 status。如果 coordinator 在排隊狀態下把 artifact context 弄丟,這支測試現在抓不到。

🧪 建議補強
     expect(created.status).toBe(409);
     expect(created.body.status).toBe("queued_for_instance");
+    expect(created.body.artifact_bindings).toHaveLength(1);
+    expect(created.body.artifact_bindings[0].artifact_id).toBe("artifact_usdc_test_001");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bim-review-coordinator/tests/sessions.test.ts` around lines 176 - 199, Update
the "reports queued_for_instance when Kit capacity is unavailable" test to also
assert that the response preserves the artifact_bindings context: after posting
to "/api/review-sessions" (the created variable), add assertions that
created.body.artifact_bindings exists and matches the sent artifact_bindings
(e.g., contains the same artifact_group_id, artifact_id, artifact_role, url,
load_order, and ready_status) so the coordinator does not drop artifact context
when returning queued_for_instance.
web-viewer-sample/src/clients/coordinatorClient.ts (1)

4-13: ⚡ Quick win

替 kit_profile 補上明確的契約型別。

CreateReviewSessionInput 是跨服務輸入契約,但 kit_profile 目前只有 Record<string, unknown>。這會讓 viewer、coordinator 跟文件之間更容易漂移,少掉編譯期保護。

As per coding guidelines, "{bim-review-coordinator,web-viewer-sample}/**/*.{ts,tsx,js}: In Node.js/TypeScript services (bim-review-coordinator, web-viewer-sample), export relevant types and interfaces for inter-service contracts; document REST API endpoints and WebSocket events with request/response schemas" and "**/*.{ts,tsx}: Prefer interface over type for defining object shapes in TypeScript".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-viewer-sample/src/clients/coordinatorClient.ts` around lines 4 - 13, The
CreateReviewSessionInput's kit_profile currently uses a loose Record<string,
unknown>; replace it with a named, exported interface (e.g., export interface
KitProfile { ... }) and use that interface in CreateReviewSessionInput
(kit_profile?: KitProfile) so the contract is explicit and compile-time checked;
define required/optional fields for KitProfile based on viewer/coordinator
usage, export the interface from this file, and update any consumers (viewer,
coordinator, docs) to import the new KitProfile interface instead of relying on
Record.
_worker/tests/test_worker_store.py (1)

90-113: ⚡ Quick win

補一個 Windows 反斜線路徑的 safe_filename 測試。

目前只驗了 POSIX 路徑成分;..\\..\\secret\\model.ifc 這種案例才是最容易在這類檔名淨化邏輯裡回歸的變體。

🧪 建議補強
 class TestSafeFilename:
     def test_simple_filename(self):
         assert safe_filename("model.ifc") == "model.ifc"

+    def test_strips_windows_path_components(self):
+        result = safe_filename(r"..\..\secret\model.ifc")
+        assert "\\" not in result
+        assert result == "model.ifc"
+
     def test_strips_path_components(self):
         result = safe_filename("/etc/passwd")
         assert "/" not in result
         assert result == "passwd"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_worker/tests/test_worker_store.py` around lines 90 - 113, The tests for
safe_filename miss Windows-style backslash paths which can bypass sanitization;
add a unit test that calls safe_filename(r"..\..\secret\model.ifc") (and maybe a
mixed-separators variant) and assert that no backslashes remain and the returned
filename equals "model.ifc" (or the expected sanitized fallback), and if
necessary update the safe_filename implementation to normalize/remove
backslashes (e.g., treat "\" like "/" when stripping path components) so Windows
paths are handled the same as POSIX ones.
openspec/changes/introduce-worker-review-session-lifecycle/tasks.md (1)

64-72: ⚡ Quick win

把治理流程寫成強制規則,不要保留模糊空間。

這裡把 CLAUDE.md 寫成和 AGENTS.md 並列,且把提交前檢查放寬成 “or equivalent scope review”。兩點都會弱化 repo 既有規範;建議明確寫成 AGENTS.md 是唯一邊界來源,且提交前必跑 gitnexus_detect_changes()。

📝 建議調整
-- [x] 7.1 Update `AGENTS.md` and `CLAUDE.md` only if the repo boundary source of truth must mention `_worker`; keep generated skill/tooling artifacts ignored.
+- [x] 7.1 Update `AGENTS.md` if the repo boundary source of truth must mention `_worker`; keep `CLAUDE.md` supplementary only and keep generated skill/tooling artifacts ignored.
…
-- [x] 7.7 Before committing implementation changes, run GitNexus change detection or equivalent scope review to confirm only expected symbols and flows changed.
+- [x] 7.7 Before committing implementation changes, run `gitnexus_detect_changes()` to confirm only expected symbols and flows changed.

Based on learnings AGENTS.md is the source of truth for agent behavior and repo boundaries; CLAUDE.md is only supplementary, and never commit without running gitnexus_detect_changes() to verify affected scope matches expectations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/introduce-worker-review-session-lifecycle/tasks.md` around
lines 64 - 72, The checklist weakens repo governance by treating CLAUDE.md as
equal to AGENTS.md and allowing "or equivalent" scope review; update the tasks
so AGENTS.md is explicitly the single source-of-truth for repo boundary/agent
behavior (mention CLAUDE.md only as supplementary) and replace the relaxed
commit check with a mandatory pre-commit verification that runs
gitnexus_detect_changes() (i.e., remove "or equivalent scope review" and require
gitnexus_detect_changes() before committing changes as called out in items 7.1
and 7.7).
bim-streaming-server/source/extensions/ezplus.bim_review_stream.messaging/ezplus/bim_review_stream/messaging/stage_loading.py (1)

244-256: 💤 Low value

Ad-hoc event object creation is fragile.

Line 255 creates an event-like object using type("Event", (), {"payload": request})(). This works but is fragile—if _on_open_stage ever accesses other IEvent attributes, it will fail silently or raise AttributeError.

Consider extracting a simple named tuple or dataclass for internal event forwarding:

Suggested approach
from collections import namedtuple
_InternalEvent = namedtuple("_InternalEvent", ["payload"])

# Then in _on_load_artifact_group:
self._on_open_stage(_InternalEvent(payload=request))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@bim-streaming-server/source/extensions/ezplus.bim_review_stream.messaging/ezplus/bim_review_stream/messaging/stage_loading.py`
around lines 244 - 256, The ad-hoc event object created in
_on_load_artifact_group via type("Event", ..., {"payload": request})() is
fragile; replace it with a small well-defined internal event type (e.g., define
_InternalEvent as a namedtuple or dataclass with a payload field) and
instantiate that when forwarding to _on_open_stage (create _InternalEvent once
at module scope and call self._on_open_stage(_InternalEvent(payload=request)) so
future access to other IEvent attributes fails loudly and is explicit).
bim-review-coordinator/src/services/kitPool.ts (1)

55-65: 💤 Low value

legacyKitInstanceFromBinding status mapping is redundant.

Line 60 has status: binding.status === "ready" ? "ready" : binding.status, which is equivalent to just status: binding.status. The ternary condition returns the same value in both branches when binding.status is "ready".

♻️ Simplify status assignment
 export function legacyKitInstanceFromBinding(binding: KitInstanceBinding | undefined, config: CoordinatorConfig): KitInstance {
   if (!binding) return allocateLocalKitInstance(config);
   return {
     instance_id: binding.kit_instance_id,
     provider: binding.provider,
-    status: binding.status === "ready" ? "ready" : binding.status,
+    status: binding.status,
     stream_server: binding.stream_config.signalingServer,
     signaling_port: binding.stream_config.signalingPort,
     media_server: binding.stream_config.mediaServer,
   };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bim-review-coordinator/src/services/kitPool.ts` around lines 55 - 65, The
status ternary in legacyKitInstanceFromBinding is redundant (status:
binding.status === "ready" ? "ready" : binding.status); change it to directly
assign binding.status (status: binding.status) in the returned KitInstance to
simplify the mapping and remove the unnecessary conditional while keeping the
rest of the fields (instance_id, provider, stream_server, signaling_port,
media_server) unchanged.
_worker/app/store.py (1)

33-37: ⚡ Quick win

Atomic file write pattern is good but lacks error cleanup.

The write_json function uses atomic replacement via temp file, which is good practice. However, if temp_path.replace(path) fails, the temp file may be left behind.

♻️ Add cleanup on failure
 def write_json(path: Path, payload: Mapping[str, Any]) -> None:
     path.parent.mkdir(parents=True, exist_ok=True)
     temp_path = path.with_suffix(path.suffix + ".tmp")
-    temp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
-    temp_path.replace(path)
+    try:
+        temp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
+        temp_path.replace(path)
+    except Exception:
+        if temp_path.exists():
+            temp_path.unlink(missing_ok=True)
+        raise
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_worker/app/store.py` around lines 33 - 37, The write_json function currently
writes to a temp file (temp_path) then calls temp_path.replace(path) but does
not clean up temp_path if replace fails; update write_json to wrap the replace
call in a try/except/finally (or try/except) so that on exception you attempt to
delete temp_path (if it exists) before re-raising the error, ensuring the
directory creation (path.parent.mkdir) and temp write remain unchanged;
reference write_json, temp_path, path.parent.mkdir, and temp_path.replace when
locating the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@_bim-control/app/main.py`:
- Around line 296-298: The zip() between artifact_group_ids and selected_groups
can silently truncate if lengths differ; update the comprehension that builds
missing_groups to use zip(artifact_group_ids, selected_groups, strict=True) so a
ValueError is raised on length mismatch (change the line creating missing_groups
which references artifact_group_ids, selected_groups, and missing_groups
accordingly); keep the rest of the return logic unchanged.

In `@_worker/app/main.py`:
- Around line 145-147: Validate the configured URL scheme before calling
urlopen: inspect the URL from settings.fake_bim_control_url (or the local
variable url) using urllib.parse.urlparse and ensure the scheme is either "http"
or "https"; if the scheme is missing or unallowed, log an error/raise a
ValueError and do not call Request/urlopen. Update the code path that creates
Request(...) and calls urlopen(...) to perform this guard first and fail-fast
with a clear message including the invalid scheme and the original URL.
- Around line 146-151: The response.status >= 400 check is unreachable because
urllib.request.urlopen raises urllib.error.HTTPError for 4xx/5xx responses
before entering the with-block; modify the error handling in the urlopen call to
catch HTTPError separately before the broader URLError so you can return a
distinct message (use HTTPError.code or message) for HTTP errors and keep
URLError for network failures; update the try/except around urlopen (referencing
urlopen, HTTPError, URLError) and remove or adjust the unreachable
response.status branch accordingly.

In `@_worker/README.md`:
- Line 29: The README and contract disagree on the POST /api/conversions payload
field (README uses run_background while docs/contracts/worker-api.md uses
options.auto_complete); pick a single canonical field name (e.g.,
options.auto_complete) and update the _worker code, README, and
docs/contracts/worker-api.md to match, or add explicit compatibility handling in
the POST /api/conversions request parser to accept both run_background and
options.auto_complete (map them into the same internal flag) so clients using
either name work; also update related integrations (_bim-control,
bim-review-coordinator, web-viewer-sample) to use the chosen name and add a
short note about the alias/compatibility period in the README.

In `@_worker/requirements.txt`:
- Around line 1-5: The requirements.txt currently lists unpinned packages
(fastapi, uvicorn[standard], pydantic, pytest, httpx); update this file to pin
each dependency to a specific, tested version (or use bounded ranges like == or
>=,<) to ensure reproducible installs and controlled upgrades—for example,
replace each line for fastapi, uvicorn[standard], pydantic, pytest, and httpx
with explicit version constraints you have validated in CI (or add a separate
constraints.txt and reference it) so installs are deterministic and safe.

In `@bim-review-coordinator/src/app.ts`:
- Around line 263-297: Make the POST /api/review-sessions/:sessionId/close
handler asynchronous and wrap the sequence of operations (calling store.update
to set "closing", eventLog.append, releasing kit bindings with
markKitBindingsDraining/releaseKitBindings, final store.update to "closed", and
the final eventLog.append calls) in a try-catch so errors call next(err) (or
respond 500) instead of leaving the session half-closed; if your store supports
transactions, perform both updates inside a transaction (using the
store.transaction API) so they commit atomically, otherwise capture the original
session state before the first store.update and roll it back
(store.update(session.session_id, originalState)) in the catch after any partial
changes, and ensure you await all async calls to store.update and
eventLog.append so failures are caught.

In `@bim-review-coordinator/src/services/sessionStore.ts`:
- Around line 89-95: The update() method in sessionStore.ts currently merges
updates with "{ ...session, ...update }", which allows callers to overwrite
immutable identity fields (e.g., session_id, created_at) and can cause a
split-brain when save() writes to a new path; fix this by filtering out those
immutable fields from the incoming update before merging or by explicitly
preserving them when building next (e.g., construct next from session first and
then apply only allowed keys from update), ensure you reference
update(sessionId: string, update: Partial<ReviewSession>), get(), and save() so
the saved session retains the original session_id and created_at values.

In `@bim-review-coordinator/tests/unit_kitpool.test.ts`:
- Around line 251-261: The test name is misleading — it says "returns empty
array for empty artifact bindings" but the assertion expects a single binding
with an empty assigned_artifact_ids; update the test name to accurately describe
the behavior (e.g., "returns one binding with empty assigned_artifact_ids for
empty artifact bindings under same_instance policy") and ensure the new name
references the context/policy being tested; locate the test that invokes
allocateKitInstanceBindings with defaultConfig, [] and "same_instance" and
rename the it(...) description accordingly.

In `@scripts/smoke-worker-review-request.ps1`:
- Around line 116-131: The PATCH assertion fails because the created coordinator
session lacks kit_instance_bindings so $streamConfig.lifecycle_status is
"created" and the PATCH sends status="created"; update the session creation
($sessionBody) to include a non-empty kit_instance_bindings (e.g., the same
bindings used for the stream) so the coordinator sets the session status to
"active" and $patchBody.status becomes "active", or alternatively change the
assertion that checks $patched.status to expect "created" if you intend to keep
sessions without bindings; modify the $sessionBody construction to add
kit_instance_bindings or adjust the if check on $patched.status accordingly.

In `@web-viewer-sample/src/clients/bimControlClient.ts`:
- Around line 30-40: The two methods getReviewSessionRequest and
patchReviewSessionRequest currently call the _bim-control
review-session-requests endpoints directly; change them to route through the
bim-review-coordinator API instead (e.g., call the coordinator's
review-session-request endpoints or use an existing coordinator client wrapper),
so the web viewer does not hit _bim-control/_s3_storage directly; update the
request URLs and any helper call sites to use the coordinator path and ensure
the PATCH payload and headers stay the same but are sent to the coordinator
endpoint (or delegate to an exported function like
fetchReviewSessionRequestViaCoordinator) to preserve the REST boundary and
remove direct coupling to _bim-control.

In `@web-viewer-sample/src/clients/streamMessages.ts`:
- Around line 4-10: The buildOpenStageRequest function currently requires and
always sends url, which prevents the new contract allowing requests without url;
change the signature of buildOpenStageRequest to accept url as optional (url?:
string) and update the payload construction in buildOpenStageRequest so the url
field is only included when a non-empty string is provided (e.g., conditionally
spread { url } when url is defined), keeping the existing conditional inclusion
of artifact_bindings; adjust any callers/typing that assumed a required url to
handle the optional parameter.

In `@web-viewer-sample/src/config/env.ts`:
- Line 20: The config adds defaultReviewRequestId which enables code paths that
call bimControlClient.getReviewSessionRequest and other BimControlClient methods
(getArtifacts, getReviewIssues, patchReviewSessionRequest) directly against
bimControlApiBase; routing must instead go through coordinatorApiBase. Fix by
changing the config usage and all call sites in Window.tsx to use the
coordinator API client (use coordinatorApiBase/coordinator client) for loading
review request metadata and artifacts, and update any places that construct or
call BimControlClient to call the coordinator proxy methods instead; ensure
defaultReviewRequestId still comes from env/query but is consumed by
coordinatorClient.getReviewSessionRequest (or equivalent) rather than
bimControlClient.* methods.

In `@web-viewer-sample/src/types/review.ts`:
- Around line 6-18: The KitInstanceBinding interface is missing required spec
fields so viewers can't show binding health or GPU/capacity info; add the MUST
fields to the interface: started_at and last_heartbeat_at (both timestamps or
null as per spec), and the GPU/capacity profile fields from the spec (e.g.,
gpu_type, gpu_count, capacity_profile or whatever exact names the spec uses) and
ensure their types match the spec; update KitInstanceBinding's stream_config or
top-level shape as specified so consumers like viewers can read these fields.

In `@web-viewer-sample/src/Window.tsx`:
- Around line 351-368: The code in Window.tsx calls
this.bimControlClient.getReviewSessionRequest(...) directly (around the block
that sets reviewRequest, requestAssets, and updates state), which violates the
guideline requiring metadata/file URL queries to go through the
bim-review-coordinator; fix by routing this fetch through the coordinator client
or adding a coordinator wrapper method (e.g., call
coordinatorClient.getReviewSessionRequest(reviewEnv.defaultReviewRequestId)
instead of this.bimControlClient...), then use the returned reviewRequest
exactly as before (preserve the subsequent calls to _assetsFromArtifactBindings,
isBlockedLifecycle, lifecycleStatusText and the setState update) or add a short
comment documenting why direct bimControlClient access is an explicit, approved
exception if coordinator cannot be used.

---

Outside diff comments:
In `@_worker/app/models.py`:
- Around line 33-38: ConversionOptions currently duplicates the generate_mapping
flag that's actually used at the top-level in ConversionRequest; remove the dead
field from the ConversionOptions class (delete the generate_mapping declaration
in ConversionOptions) and ensure no code references
ConversionOptions.generate_mapping elsewhere (search for ConversionOptions and
remove or refactor any use), leaving ConversionRequest.generate_mapping as the
single source of truth used by main.py (via request.model_dump()) and store.py
(which reads request.get("generate_mapping", True)).

In `@bim-review-coordinator/src/socket/reviewNamespace.ts`:
- Around line 75-90: The leaveSession handler currently calls
validateExistingSession which blocks when a session is non-mutable; change it to
allow departures by adding a new helper validateSessionExists (or similar) that
only verifies the session exists and returns {ok, sessionId} without checking
isSessionMutable, then use that helper in the socket.on("leaveSession", ...)
flow so socket.leave, store.leave(sessionId, userId), and
namespace.to(sessionId).emit("presenceUpdated", ...) always run for existing
sessions; ensure you still ack with the returned result (ack?.({ok:true}) or the
error object) and keep references to validateExistingSession, store.leave,
namespace.to(...).emit, and the leaveSession socket handler when making the
change.

In `@CLAUDE.md`:
- Around line 152-169: The Mermaid overview diagram is outdated: it omits the
_worker (WK) artifact facade and still routes artifact access directly between
BC and S3 and KIT and S3; update the diagram to insert WK ("_worker\nArtifact +
Conversion Facade") and CS ("_conversion-service\nLegacy Conversion API"),
change the flow so CO -> WK (review artifact binding), WK -> BC (metadata only),
WK -> CS (compatibility adapter), WK -> S3 (optional static mirror), and KIT ->
WK (load USD/USDC by URL) while keeping other edges (WV->CO, WV->KIT, CO->BC,
CO->KIT, CO->BC persisting) intact so the diagram matches AGENTS.md
architecture.

In `@docs/contracts/local-dev-runbook.md`:
- Around line 40-71: Update the runbook sections for bim-review-coordinator and
web-viewer-sample to require nvm initialization before any npm/Node commands:
explicitly instruct developers to source the NVM environment (initialize NVM_DIR
and load nvm.sh) prior to running npm install or npm run dev in the
bim-review-coordinator and web-viewer-sample steps so the correct Node version
is used per the repo policy.

---

Nitpick comments:
In `@_worker/app/store.py`:
- Around line 33-37: The write_json function currently writes to a temp file
(temp_path) then calls temp_path.replace(path) but does not clean up temp_path
if replace fails; update write_json to wrap the replace call in a
try/except/finally (or try/except) so that on exception you attempt to delete
temp_path (if it exists) before re-raising the error, ensuring the directory
creation (path.parent.mkdir) and temp write remain unchanged; reference
write_json, temp_path, path.parent.mkdir, and temp_path.replace when locating
the change.

In `@_worker/tests/test_worker_store.py`:
- Around line 90-113: The tests for safe_filename miss Windows-style backslash
paths which can bypass sanitization; add a unit test that calls
safe_filename(r"..\..\secret\model.ifc") (and maybe a mixed-separators variant)
and assert that no backslashes remain and the returned filename equals
"model.ifc" (or the expected sanitized fallback), and if necessary update the
safe_filename implementation to normalize/remove backslashes (e.g., treat "\"
like "/" when stripping path components) so Windows paths are handled the same
as POSIX ones.

In `@bim-review-coordinator/src/services/kitPool.ts`:
- Around line 55-65: The status ternary in legacyKitInstanceFromBinding is
redundant (status: binding.status === "ready" ? "ready" : binding.status);
change it to directly assign binding.status (status: binding.status) in the
returned KitInstance to simplify the mapping and remove the unnecessary
conditional while keeping the rest of the fields (instance_id, provider,
stream_server, signaling_port, media_server) unchanged.

In `@bim-review-coordinator/tests/sessions.test.ts`:
- Around line 176-199: Update the "reports queued_for_instance when Kit capacity
is unavailable" test to also assert that the response preserves the
artifact_bindings context: after posting to "/api/review-sessions" (the created
variable), add assertions that created.body.artifact_bindings exists and matches
the sent artifact_bindings (e.g., contains the same artifact_group_id,
artifact_id, artifact_role, url, load_order, and ready_status) so the
coordinator does not drop artifact context when returning queued_for_instance.

In
`@bim-streaming-server/source/extensions/ezplus.bim_review_stream.messaging/ezplus/bim_review_stream/messaging/stage_loading.py`:
- Around line 244-256: The ad-hoc event object created in
_on_load_artifact_group via type("Event", ..., {"payload": request})() is
fragile; replace it with a small well-defined internal event type (e.g., define
_InternalEvent as a namedtuple or dataclass with a payload field) and
instantiate that when forwarding to _on_open_stage (create _InternalEvent once
at module scope and call self._on_open_stage(_InternalEvent(payload=request)) so
future access to other IEvent attributes fails loudly and is explicit).

In `@CLAUDE.md`:
- Line 780: The doc text currently leaves ambiguity about the external boundary;
update the wording in section 9 to explicitly state that `_worker` is the new
external facade/artifact and conversion boundary, while `_conversion-service` is
retained only as an internal legacy compatibility adapter behind `_worker`;
replace the existing sentence about `_conversion-service` being "retained... as
`_worker` 後方的 legacy conversion compatibility path" with a clear statement that
`_worker` is the outward-facing boundary and `_conversion-service` is an
internal adapter used only for backward compatibility, and adjust any
surrounding references to consistently treat `_worker` as the public conversion
interface.

In `@docs/contracts/streaming-datachannel-events.md`:
- Line 45: The docs mention applied_mode="single_binding_only", partial_load,
and skipped_artifact_ids[] but the JSON example for openedStageResult is missing
a corresponding partial-load example; add a complete openedStageResult JSON
snippet showing applied_mode set to "single_binding_only", partial_load:true,
skipped_artifact_ids listing the skipped artifact IDs, and missing_paths for
bindings without URLs so implementers have a concrete example to follow (update
the example block that contains openedStageResult and any related sample
fields).

In `@openspec/changes/introduce-worker-review-session-lifecycle/tasks.md`:
- Around line 64-72: The checklist weakens repo governance by treating CLAUDE.md
as equal to AGENTS.md and allowing "or equivalent" scope review; update the
tasks so AGENTS.md is explicitly the single source-of-truth for repo
boundary/agent behavior (mention CLAUDE.md only as supplementary) and replace
the relaxed commit check with a mandatory pre-commit verification that runs
gitnexus_detect_changes() (i.e., remove "or equivalent scope review" and require
gitnexus_detect_changes() before committing changes as called out in items 7.1
and 7.7).

In `@web-viewer-sample/src/clients/coordinatorClient.ts`:
- Around line 4-13: The CreateReviewSessionInput's kit_profile currently uses a
loose Record<string, unknown>; replace it with a named, exported interface
(e.g., export interface KitProfile { ... }) and use that interface in
CreateReviewSessionInput (kit_profile?: KitProfile) so the contract is explicit
and compile-time checked; define required/optional fields for KitProfile based
on viewer/coordinator usage, export the interface from this file, and update any
consumers (viewer, coordinator, docs) to import the new KitProfile interface
instead of relying on Record.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8fc69dee-df45-49c5-a524-efc86898ca01

📥 Commits

Reviewing files that changed from the base of the PR and between 2609751 and b121ec2.

📒 Files selected for processing (56)
  • .gitignore
  • AGENTS.md
  • CLAUDE.md
  • README.md
  • _bim-control/app/main.py
  • _bim-control/tests/test_review_session_requests_api.py
  • _worker/AGENTS.md
  • _worker/README.md
  • _worker/app/__init__.py
  • _worker/app/main.py
  • _worker/app/models.py
  • _worker/app/settings.py
  • _worker/app/store.py
  • _worker/requirements.txt
  • _worker/tests/test_worker_api.py
  • _worker/tests/test_worker_store.py
  • bim-review-coordinator/src/app.ts
  • bim-review-coordinator/src/services/kitPool.ts
  • bim-review-coordinator/src/services/sessionStore.ts
  • bim-review-coordinator/src/socket/reviewNamespace.ts
  • bim-review-coordinator/src/types.ts
  • bim-review-coordinator/tests/sessions.test.ts
  • bim-review-coordinator/tests/unit_kitpool.test.ts
  • bim-review-coordinator/tests/unit_sessionstore.test.ts
  • bim-streaming-server/scripts/tests/test-stage-loading-contract.ps1
  • bim-streaming-server/source/extensions/ezplus.bim_review_stream.messaging/ezplus/bim_review_stream/messaging/stage_loading.py
  • docs/contracts/bim-control-fake-api.md
  • docs/contracts/conversion-api.md
  • docs/contracts/coordinator-socket-events.md
  • docs/contracts/local-dev-runbook.md
  • docs/contracts/review-session-api.md
  • docs/contracts/streaming-datachannel-events.md
  • docs/contracts/worker-api.md
  • openspec/changes/introduce-worker-review-session-lifecycle/.openspec.yaml
  • openspec/changes/introduce-worker-review-session-lifecycle/design.md
  • openspec/changes/introduce-worker-review-session-lifecycle/proposal.md
  • openspec/changes/introduce-worker-review-session-lifecycle/specs/multi-artifact-kit-routing/spec.md
  • openspec/changes/introduce-worker-review-session-lifecycle/specs/review-session-request-lifecycle/spec.md
  • openspec/changes/introduce-worker-review-session-lifecycle/specs/session-first-review-viewer/spec.md
  • openspec/changes/introduce-worker-review-session-lifecycle/specs/worker-artifact-pipeline/spec.md
  • openspec/changes/introduce-worker-review-session-lifecycle/tasks.md
  • openspec/config.yaml
  • scripts/dev-health-check.ps1
  • scripts/smoke-worker-review-request.ps1
  • scripts/start-all.ps1
  • scripts/stop-all.ps1
  • web-viewer-sample/package.json
  • web-viewer-sample/scripts/verify-session-first-contract.mjs
  • web-viewer-sample/src/Window.tsx
  • web-viewer-sample/src/clients/bimControlClient.ts
  • web-viewer-sample/src/clients/coordinatorClient.ts
  • web-viewer-sample/src/clients/streamMessages.ts
  • web-viewer-sample/src/components/ArtifactPanel.tsx
  • web-viewer-sample/src/config/env.ts
  • web-viewer-sample/src/types/artifacts.ts
  • web-viewer-sample/src/types/review.ts

Comment thread _bim-control/app/main.py
Comment on lines +296 to +298
missing_groups = [group_id for group_id, group in zip(artifact_group_ids, selected_groups) if group is None]
ready = bool(selected_groups) and all(_artifact_group_ready(group) for group in selected_groups)
return ("created" if ready and not missing_groups else "blocked_conversion", [group for group in selected_groups if group], missing_groups)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add strict=True to zip() to catch length mismatches.

The zip() call on line 296 pairs artifact_group_ids with selected_groups, but if lengths differ, it silently truncates. Since selected_groups is derived from artifact_group_ids via list comprehension, lengths should match, but adding strict=True makes this invariant explicit and catches bugs if the logic changes.

🛡️ Proposed fix
-    missing_groups = [group_id for group_id, group in zip(artifact_group_ids, selected_groups) if group is None]
+    missing_groups = [group_id for group_id, group in zip(artifact_group_ids, selected_groups, strict=True) if group is None]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
missing_groups = [group_id for group_id, group in zip(artifact_group_ids, selected_groups) if group is None]
ready = bool(selected_groups) and all(_artifact_group_ready(group) for group in selected_groups)
return ("created" if ready and not missing_groups else "blocked_conversion", [group for group in selected_groups if group], missing_groups)
missing_groups = [group_id for group_id, group in zip(artifact_group_ids, selected_groups, strict=True) if group is None]
ready = bool(selected_groups) and all(_artifact_group_ready(group) for group in selected_groups)
return ("created" if ready and not missing_groups else "blocked_conversion", [group for group in selected_groups if group], missing_groups)
🧰 Tools
🪛 Ruff (0.15.12)

[warning] 296-296: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_bim-control/app/main.py` around lines 296 - 298, The zip() between
artifact_group_ids and selected_groups can silently truncate if lengths differ;
update the comprehension that builds missing_groups to use
zip(artifact_group_ids, selected_groups, strict=True) so a ValueError is raised
on length mismatch (change the line creating missing_groups which references
artifact_group_ids, selected_groups, and missing_groups accordingly); keep the
rest of the return logic unchanged.

Comment thread _worker/app/main.py
Comment on lines +145 to +147
request = Request(url, data=body, headers={"Content-Type": "application/json"}, method="POST")
try:
with urlopen(request, timeout=3) as response:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate URL scheme before urlopen (Ruff S310).

settings.fake_bim_control_url is externally configurable; an unexpected file:// or custom scheme could reach urlopen unintentionally.

🛡️ Proposed scheme guard
+from urllib.parse import urlparse
+
 def _post_bim_control_result(settings: Settings, result: dict[str, Any]) -> str | None:
+    _scheme = urlparse(settings.fake_bim_control_url).scheme
+    if _scheme not in {"http", "https"}:
+        return f"_bim-control URL has unexpected scheme: {_scheme}."
     model_version_id = safe_id(str(result["model_version_id"]), "model_version_id")
🧰 Tools
🪛 Ruff (0.15.12)

[error] 145-145: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)


[error] 147-147: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_worker/app/main.py` around lines 145 - 147, Validate the configured URL
scheme before calling urlopen: inspect the URL from
settings.fake_bim_control_url (or the local variable url) using
urllib.parse.urlparse and ensure the scheme is either "http" or "https"; if the
scheme is missing or unallowed, log an error/raise a ValueError and do not call
Request/urlopen. Update the code path that creates Request(...) and calls
urlopen(...) to perform this guard first and fail-fast with a clear message
including the invalid scheme and the original URL.

Comment thread _worker/app/main.py
Comment on lines +146 to +151
try:
with urlopen(request, timeout=3) as response:
if response.status >= 400:
return f"_bim-control returned HTTP {response.status}."
except URLError as exc:
return f"_bim-control callback failed: {exc}."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# Find and examine the actual code in _worker/app/main.py
fd -t f main.py | grep -E "_worker.*main.py"

Repository: monkey1sai/AI-BIM-governance

Length of output: 92


🏁 Script executed:

# Check lines around 146-151 in the _worker/app/main.py file
if [ -f "_worker/app/main.py" ]; then
  echo "=== File found, showing lines 140-155 ===" 
  sed -n '140,155p' "_worker/app/main.py"
fi

Repository: monkey1sai/AI-BIM-governance

Length of output: 897


🏁 Script executed:

# Verify urllib exception hierarchy with Python
python3 << 'EOF'
import urllib.error

# Check if HTTPError exists and is a subclass of URLError
try:
    from urllib.error import HTTPError, URLError
    print("HTTPError import: SUCCESS")
    print(f"HTTPError is subclass of URLError: {issubclass(HTTPError, URLError)}")
    print(f"HTTPError MRO: {HTTPError.__mro__}")
except ImportError as e:
    print(f"Import failed: {e}")

# Check urllib.request.urlopen documentation/behavior
import urllib.request
help_text = urllib.request.urlopen.__doc__
if help_text:
    # Show first 50 lines of help
    lines = help_text.split('\n')[:50]
    for line in lines:
        print(line)
EOF

Repository: monkey1sai/AI-BIM-governance

Length of output: 2752


🏁 Script executed:

python3 << 'EOF'
import urllib.request
import urllib.error
from http.server import HTTPServer, BaseHTTPRequestHandler
import threading
import time

# Create a test server that returns 400
class Handler400(BaseHTTPRequestHandler):
    def do_POST(self):
        self.send_response(400)
        self.send_header('Content-type', 'text/plain')
        self.end_headers()
        self.wfile.write(b'Bad Request')
    
    def log_message(self, format, *args):
        pass  # Suppress log output

# Start test server in background
server = HTTPServer(('127.0.0.1', 9999), Handler400)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
time.sleep(0.2)  # Give server time to start

try:
    # Test 1: What happens when urlopen gets a 400 response?
    print("Test 1: Attempting urlopen with 400 response")
    try:
        response = urllib.request.urlopen('http://127.0.0.1:9999/', timeout=2)
        print(f"  Result: Got response object (status={response.status})")
        print(f"  Body: {response.read()}")
    except urllib.error.HTTPError as e:
        print(f"  Exception raised: HTTPError with code={e.code}")
    except urllib.error.URLError as e:
        print(f"  Exception raised: URLError: {e}")
    except Exception as e:
        print(f"  Exception raised: {type(e).__name__}: {e}")
        
    # Test 2: Check exception handling order
    print("\nTest 2: Exception handling - URLError catches HTTPError?")
    try:
        response = urllib.request.urlopen('http://127.0.0.1:9999/', timeout=2)
    except urllib.error.URLError as e:
        print(f"  URLError except caught: {type(e).__name__}")
        print(f"  Is it HTTPError? {isinstance(e, urllib.error.HTTPError)}")
        if hasattr(e, 'code'):
            print(f"  HTTPError.code value: {e.code}")

finally:
    server.shutdown()
    
EOF

Repository: monkey1sai/AI-BIM-governance

Length of output: 312


🏁 Script executed:

python3 << 'EOF'
import urllib.request
import urllib.error
from unittest.mock import Mock, patch
import io

print("=== urllib exception behavior verification ===\n")

# Test with mock to verify the actual behavior
print("Test: Simulating HTTP 400 response with urlopen")
print("(Checking urllib source/documentation behavior)\n")

# According to urllib documentation, urlopen raises HTTPError for error statuses
# Let's verify by checking the actual exception hierarchy and documented behavior
from urllib.error import HTTPError, URLError

exc = HTTPError("http://example.com", 400, "Bad Request", {}, io.BytesIO(b""))
print(f"HTTPError attributes:")
print(f"  - code: {exc.code}")
print(f"  - msg: {exc.msg}")
print(f"  - isinstance(exc, URLError): {isinstance(exc, URLError)}")

print("\n=== Documented behavior from urllib.request.urlopen ===")
print("urlopen raises HTTPError (a URLError subclass) for HTTP error responses.")
print("This means:")
print("1. urlopen does NOT return a response object for 4xx/5xx")
print("2. Instead, it raises HTTPError before the 'with' block body executes")
print("3. Since HTTPError is a URLError subclass, 'except URLError' catches both")
print("4. The 'if response.status >= 400' check is unreachable because")
print("   the exception prevents the with-block body from executing")

print("\n=== Code path analysis ===")
print("Current code:")
print("  try:")
print("      with urlopen(...) as response:")
print("          if response.status >= 400:  # <- UNREACHABLE")
print("  except URLError as exc:  # <- Catches HTTPError too!")
print("      return generic 'callback failed' message")
print("\nFor HTTP 400: HTTPError is raised, caught by URLError, loses HTTP status info")
print("For network error: URLError is raised, caught, shows generic message")
print("Intended distinction is lost.")

EOF

Repository: monkey1sai/AI-BIM-governance

Length of output: 1199


🏁 Script executed:

# Check imports at the top of _worker/app/main.py
head -30 _worker/app/main.py

Repository: monkey1sai/AI-BIM-governance

Length of output: 1049


response.status >= 400 is unreachable dead code — HTTP errors are swallowed as network errors.

urllib.request.urlopen raises urllib.error.HTTPError (a URLError subclass) for any 4xx/5xx response before the with-block body executes. The if response.status >= 400 check can never be reached; instead every HTTP error is caught by except URLError and emits the generic "callback failed" message, losing the intended distinction between HTTP errors and network failures.

🔧 Proposed fix
-from urllib.error import URLError
+from urllib.error import HTTPError, URLError
 from urllib.request import Request, urlopen
     try:
         with urlopen(request, timeout=3) as response:
-            if response.status >= 400:
-                return f"_bim-control returned HTTP {response.status}."
+            pass  # 2xx/3xx — success
+    except HTTPError as exc:
+        return f"_bim-control returned HTTP {exc.code}."
     except URLError as exc:
         return f"_bim-control callback failed: {exc}."
🧰 Tools
🪛 Ruff (0.15.12)

[error] 147-147: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_worker/app/main.py` around lines 146 - 151, The response.status >= 400 check
is unreachable because urllib.request.urlopen raises urllib.error.HTTPError for
4xx/5xx responses before entering the with-block; modify the error handling in
the urlopen call to catch HTTPError separately before the broader URLError so
you can return a distinct message (use HTTPError.code or message) for HTTP
errors and keep URLError for network failures; update the try/except around
urlopen (referencing urlopen, HTTPError, URLError) and remove or adjust the
unreachable response.status branch accordingly.

Comment thread _worker/README.md

`POST /api/artifacts` accepts either `content_base64`, `content_text`, `source_url`, or `signed_upload_url` plus lineage fields. File bytes are stored under `data/objects/tenants/...`.

`POST /api/conversions` creates a queued job. With the default `run_background=true`, FastAPI schedules an inline local adapter conversion that writes deterministic demo `model.usdc`, index JSON, mapping JSON, and `metadata.json`, then posts metadata to `_bim-control`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

POST /api/conversions 參數名稱與契約文件不一致

Line 29 使用 run_background=true,但 docs/contracts/worker-api.md 的範例是 options.auto_complete。同一 API 請統一欄位名稱(或明確標註別名/相容期),避免 client 送錯 payload。

Based on learnings: "Synchronize API schema or object layout changes with _bim-control, bim-review-coordinator, and web-viewer-sample when modifying _worker."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_worker/README.md` at line 29, The README and contract disagree on the POST
/api/conversions payload field (README uses run_background while
docs/contracts/worker-api.md uses options.auto_complete); pick a single
canonical field name (e.g., options.auto_complete) and update the _worker code,
README, and docs/contracts/worker-api.md to match, or add explicit compatibility
handling in the POST /api/conversions request parser to accept both
run_background and options.auto_complete (map them into the same internal flag)
so clients using either name work; also update related integrations
(_bim-control, bim-review-coordinator, web-viewer-sample) to use the chosen name
and add a short note about the alias/compatibility period in the README.

Comment thread _worker/requirements.txt
Comment on lines +1 to +5
fastapi
uvicorn[standard]
pydantic
pytest
httpx

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check PyPI for latest versions of _worker dependencies

echo "=== Checking latest versions on PyPI ==="
for pkg in fastapi uvicorn pydantic pytest httpx; do
  echo "--- $pkg ---"
  curl -s "https://pypi.org/pypi/$pkg/json" | jq -r '.info.version'
done

echo ""
echo "=== Checking for known vulnerabilities ==="
gh api graphql -f query='
{
  securityVulnerabilities(first: 20, ecosystem: PIP, package: "fastapi") {
    nodes {
      advisory { summary severity publishedAt }
      vulnerableVersionRange
      firstPatchedVersion { identifier }
    }
  }
}' | jq '.data.securityVulnerabilities.nodes[] | {summary: .advisory.summary, severity: .advisory.severity, vulnerable: .vulnerableVersionRange, patched: .firstPatchedVersion.identifier}'

gh api graphql -f query='
{
  securityVulnerabilities(first: 20, ecosystem: PIP, package: "httpx") {
    nodes {
      advisory { summary severity publishedAt }
      vulnerableVersionRange
      firstPatchedVersion { identifier }
    }
  }
}' | jq '.data.securityVulnerabilities.nodes[] | {summary: .advisory.summary, severity: .advisory.severity, vulnerable: .vulnerableVersionRange, patched: .firstPatchedVersion.identifier}'

gh api graphql -f query='
{
  securityVulnerabilities(first: 20, ecosystem: PIP, package: "uvicorn") {
    nodes {
      advisory { summary severity publishedAt }
      vulnerableVersionRange
      firstPatchedVersion { identifier }
    }
  }
}' | jq '.data.securityVulnerabilities.nodes[] | {summary: .advisory.summary, severity: .advisory.severity, vulnerable: .vulnerableVersionRange, patched: .firstPatchedVersion.identifier}'

Repository: monkey1sai/AI-BIM-governance

Length of output: 931


🏁 Script executed:

# Verify the _worker/requirements.txt file exists and check its content
find . -name "requirements.txt" -path "*_worker*" | head -20

Repository: monkey1sai/AI-BIM-governance

Length of output: 99


🏁 Script executed:

# Check if _worker/requirements.txt exists and show its content
if [ -f "_worker/requirements.txt" ]; then
  echo "=== _worker/requirements.txt content ==="
  cat -n "_worker/requirements.txt"
else
  echo "File not found at _worker/requirements.txt"
  # Search for it
  find . -type f -name "*requirements*.txt" | head -20
fi

Repository: monkey1sai/AI-BIM-governance

Length of output: 196


🌐 Web query:

GHSA-h8pj-cxx2-jfg2 httpx vulnerability

💡 Result:

GHSA-h8pj-cxx2-jfg2 is a critical vulnerability (CVSS 9.1) in the Python httpx library, also tracked as CVE-2021-41945. It involves improper input validation in httpx.URL, httpx.Client, and functions using httpx.URL.copy_with, affecting versions before 0.23.0 (specifically all 0.x versions up to 0.22.0 and 1.0.0.beta0) [1][2][3]. The issue allows potential high confidentiality and integrity impacts (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) due to CWE-20 (Improper Input Validation) [2][3]. It was published on 2022-04-28 and fixed in httpx 0.23.0 [1][3]. Last modified in OSV as of 2025-02-13, confirming it's patched and no ongoing issues as of 2026 [2]. References include httpx GitHub discussions #1831, #2184, and release notes for 0.23.0 [3].

Citations:


🌐 Web query:

PYSEC-2024-38 fastapi vulnerability

💡 Result:

PYSEC-2024-38 is a vulnerability advisory for the FastAPI Python web framework, corresponding to CVE-2024-24762 and GHSA-qf9m-vfgh-m389 [1][2][3]. It describes a Regular Expression Denial of Service (ReDoS) issue when processing form data. The root cause is in the underlying python-multipart library, which uses a vulnerable regex to parse the HTTP Content-Type header, including options. An attacker can send a specially crafted Content-Type header (e.g., with many backslashes) that causes the regex to stall indefinitely, consuming CPU and blocking the event loop, preventing the server from handling further requests [1][2][3][4]. This affects FastAPI applications that use form data parsing (e.g., Form parameters), but not JSON endpoints [4][5]. Severity is CVSS 3.1 7.5 (High): AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H [1][2]. Published February 5, 2024 [1][2]. Fixed in FastAPI 0.109.1 via commit 9d34ad0ee8a0dfbbcce06f76c2d5d851085024fc, which updates python-multipart to a patched version (python-multipart >=0.0.7) [1][2][6]. All prior versions of FastAPI are affected if using form data [2]. Official FastAPI advisory: https://github.com/tiangolo/fastapi/security/advisories/GHSA-qf9m-vfgh-m389 [1]. PYPA advisory YAML: https://github.com/pypa/advisory-database/blob/main/vulns/fastapi/PYSEC-2024-38.yaml [2]. NVD entry: https://nvd.nist.gov/vuln/detail/CVE-2024-24762 [3].

Citations:


Pin dependency versions for reproducibility and forward compatibility.

While the requirements file lacks version pins, unpinned dependencies will install the latest available versions (as of May 2026: fastapi 0.136.1, uvicorn 0.46.0, httpx 0.28.1, pydantic 2.13.4, pytest 9.0.3), which are past known security patches. However, pinning versions is recommended to:

  • Ensure reproducible builds across environments and deployments
  • Prevent unexpected breaking changes from minor version updates
  • Control when dependencies are upgraded after security reviews

Consider pinning to specific stable versions rather than allowing unbounded updates. This is a best practice for production code, even if current latest versions are secure.

🧰 Tools
🪛 OSV Scanner (2.3.6)

[HIGH] 1-1: fastapi 0.99.1: undefined

(PYSEC-2024-38)


[CRITICAL] 1-1: httpx 0.9.5: undefined

(PYSEC-2022-183)


[CRITICAL] 1-1: httpx 0.9.5: Improper Input Validation in httpx

(GHSA-h8pj-cxx2-jfg2)


[HIGH] 1-1: uvicorn 0.9.1: undefined

(PYSEC-2020-150)


[HIGH] 1-1: uvicorn 0.9.1: undefined

(PYSEC-2020-151)


[HIGH] 1-1: uvicorn 0.9.1: Log injection in uvicorn

(GHSA-33c7-2mpw-hg34)


[HIGH] 1-1: uvicorn 0.9.1: HTTP response splitting in uvicorn

(GHSA-f97h-2pfx-f59f)


[CRITICAL] 1-1: h11 0.8.1: h11 accepts some malformed Chunked-Encoding bodies

(GHSA-vqfr-h8mv-ghfj)


[HIGH] 1-1: idna 2.9.0: undefined

(PYSEC-2024-60)


[HIGH] 1-1: idna 2.9.0: Internationalized Domain Names in Applications (IDNA) vulnerable to denial of service from specially crafted inputs to idna.encode

(GHSA-jjg7-2v4v-x38h)


[HIGH] 1-1: starlette 0.27.0: Starlette has possible denial-of-service vector when parsing large files in multipart forms

(GHSA-2c2j-9gv5-cj73)


[HIGH] 1-1: starlette 0.27.0: Starlette Denial of service (DoS) via multipart/form-data

(GHSA-f96h-pmfr-66vw)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_worker/requirements.txt` around lines 1 - 5, The requirements.txt currently
lists unpinned packages (fastapi, uvicorn[standard], pydantic, pytest, httpx);
update this file to pin each dependency to a specific, tested version (or use
bounded ranges like == or >=,<) to ensure reproducible installs and controlled
upgrades—for example, replace each line for fastapi, uvicorn[standard],
pydantic, pytest, and httpx with explicit version constraints you have validated
in CI (or add a separate constraints.txt and reference it) so installs are
deterministic and safe.

Comment on lines +30 to +40
async getReviewSessionRequest(reviewRequestId: string): Promise<ReviewSessionRequest> {
return this.request<ReviewSessionRequest>(`/api/review-session-requests/${reviewRequestId}`);
}

async patchReviewSessionRequest(reviewRequestId: string, payload: Record<string, unknown>): Promise<ReviewSessionRequest> {
return this.request<ReviewSessionRequest>(`/api/review-session-requests/${reviewRequestId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

不要讓 viewer 直接呼叫 _bim-control 的 review-session-request API。

這兩個新方法把 web-viewer-sample 直接綁到 _bim-control 的 metadata/PATCH 流程,等於繞過 bim-review-coordinator 的 REST 邊界。這不只增加耦合,還把 review-session-request 的狀態變更責任拉進了 UI client。

As per coding guidelines, "web-viewer-sample/src/**/*.{ts,tsx,js,jsx}: Metadata and file URL queries must be routed through bim-review-coordinator, not directly connected to _bim-control or _s3_storage".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-viewer-sample/src/clients/bimControlClient.ts` around lines 30 - 40, The
two methods getReviewSessionRequest and patchReviewSessionRequest currently call
the _bim-control review-session-requests endpoints directly; change them to
route through the bim-review-coordinator API instead (e.g., call the
coordinator's review-session-request endpoints or use an existing coordinator
client wrapper), so the web viewer does not hit _bim-control/_s3_storage
directly; update the request URLs and any helper call sites to use the
coordinator path and ensure the PATCH payload and headers stay the same but are
sent to the coordinator endpoint (or delegate to an exported function like
fetchReviewSessionRequestViaCoordinator) to preserve the REST boundary and
remove direct coupling to _bim-control.

Comment on lines +4 to +10
export function buildOpenStageRequest(url: string, artifactBindings: ArtifactBinding[] = []): StreamMessage {
return {
event_type: "openStageRequest",
payload: { url },
payload: {
url,
...(artifactBindings.length > 0 ? { artifact_bindings: artifactBindings } : {}),
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

openStageRequest 仍強制 url,與新契約的「可省略 url」模式衝突

Line 4 目前把 url 設為必填,Line 8 也固定送出 url。這會讓 client 無法送出僅靠 artifact_bindings 的請求模式。

💡 建議修正
-export function buildOpenStageRequest(url: string, artifactBindings: ArtifactBinding[] = []): StreamMessage {
+export function buildOpenStageRequest(url?: string, artifactBindings: ArtifactBinding[] = []): StreamMessage {
     return {
         event_type: "openStageRequest",
         payload: {
-            url,
+            ...(url ? { url } : {}),
             ...(artifactBindings.length > 0 ? { artifact_bindings: artifactBindings } : {}),
         },
     };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-viewer-sample/src/clients/streamMessages.ts` around lines 4 - 10, The
buildOpenStageRequest function currently requires and always sends url, which
prevents the new contract allowing requests without url; change the signature of
buildOpenStageRequest to accept url as optional (url?: string) and update the
payload construction in buildOpenStageRequest so the url field is only included
when a non-empty string is provided (e.g., conditionally spread { url } when url
is defined), keeping the existing conditional inclusion of artifact_bindings;
adjust any callers/typing that assumed a required url to handle the optional
parameter.

bimControlApiBase: import.meta.env.VITE_BIM_CONTROL_API_BASE || "http://127.0.0.1:8001",
defaultProjectId: queryParam("projectId") || import.meta.env.VITE_DEFAULT_PROJECT_ID || "project_demo_001",
defaultModelVersionId: queryParam("modelVersionId") || import.meta.env.VITE_DEFAULT_MODEL_VERSION_ID || "version_demo_001",
defaultReviewRequestId: queryParam("reviewRequestId") || queryParam("review_request_id") || import.meta.env.VITE_DEFAULT_REVIEW_REQUEST_ID || "",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether Window.tsx or any web-viewer-sample source uses bimControlApiBase
# or bimControlClient to fetch review-request data, bypassing bim-review-coordinator.
rg -n "bimControlApiBase\|bimControlClient\|review_request_id\|reviewRequestId\|defaultReviewRequestId" \
  web-viewer-sample/src/ -A3 -B1

Repository: monkey1sai/AI-BIM-governance

Length of output: 54


🏁 Script executed:

#!/bin/bash
# First, locate the env.ts file
find web-viewer-sample -name "env.ts" -o -name "env.js" 2>/dev/null

# Check if web-viewer-sample/src exists
ls -la web-viewer-sample/src/ 2>/dev/null | head -20

Repository: monkey1sai/AI-BIM-governance

Length of output: 1271


🏁 Script executed:

#!/bin/bash
# Look for the config directory and env file
fd -t f "env\.(ts|js)" web-viewer-sample/

Repository: monkey1sai/AI-BIM-governance

Length of output: 108


🏁 Script executed:

#!/bin/bash
# Check for Window.tsx in web-viewer-sample
fd -t f "Window\.(ts|tsx|js|jsx)" web-viewer-sample/

Repository: monkey1sai/AI-BIM-governance

Length of output: 148


🏁 Script executed:

#!/bin/bash
# Read the env.ts file
cat -n web-viewer-sample/src/config/env.ts

Repository: monkey1sai/AI-BIM-governance

Length of output: 2031


🏁 Script executed:

#!/bin/bash
# Search for usage of bimControlApiBase and defaultReviewRequestId in Window.tsx
grep -n "bimControlApiBase\|defaultReviewRequestId\|bimControlClient" web-viewer-sample/src/Window.tsx

Repository: monkey1sai/AI-BIM-governance

Length of output: 812


🏁 Script executed:

#!/bin/bash
# Get broader context in Window.tsx for review request handling
grep -n -i "review.request\|reviewRequest" web-viewer-sample/src/Window.tsx | head -20

Repository: monkey1sai/AI-BIM-governance

Length of output: 1870


Architectural violation: Metadata queries must route through bim-review-coordinator, not _bim-control.

The new defaultReviewRequestId config at line 20 directly enables a routing violation. In Window.tsx line 352–353, when defaultReviewRequestId is populated, it is passed to this.bimControlClient.getReviewSessionRequest(), which performs a direct call to _bim-control (port 8001). This violates the guideline: "Metadata and file URL queries must be routed through bim-review-coordinator, not directly connected to _bim-control or _s3_storage."

Additional violations already exist: lines 449–450 and 409 in Window.tsx directly call BimControlClient methods (getArtifacts, getReviewIssues, patchReviewSessionRequest) instead of routing through the coordinator.

All review request metadata loading in web-viewer-sample must be refactored to use coordinatorApiBase instead of bimControlApiBase.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-viewer-sample/src/config/env.ts` at line 20, The config adds
defaultReviewRequestId which enables code paths that call
bimControlClient.getReviewSessionRequest and other BimControlClient methods
(getArtifacts, getReviewIssues, patchReviewSessionRequest) directly against
bimControlApiBase; routing must instead go through coordinatorApiBase. Fix by
changing the config usage and all call sites in Window.tsx to use the
coordinator API client (use coordinatorApiBase/coordinator client) for loading
review request metadata and artifacts, and update any places that construct or
call BimControlClient to call the coordinator proxy methods instead; ensure
defaultReviewRequestId still comes from env/query but is consumed by
coordinatorClient.getReviewSessionRequest (or equivalent) rather than
bimControlClient.* methods.

Comment on lines +6 to +18
export interface KitInstanceBinding {
kit_instance_id: string;
provider: "local_fixed";
tenant_id: string;
assigned_artifact_ids: string[];
status: "allocated" | "starting" | "ready" | "draining" | "released" | "failed";
stream_config: {
signalingServer: string;
signalingPort: number;
mediaServer: string;
};
released_at: string | null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

KitInstanceBinding is missing spec-required fields.

The spec (multi-artifact-kit-routing/spec.md, line 19) mandates started_at, last_heartbeat_at, and GPU/capacity profile fields as MUST. All three are absent from the interface, making them unavailable to any viewer UI that needs to display binding health or GPU status.

✏️ Proposed addition
 export interface KitInstanceBinding {
     kit_instance_id: string;
     provider: "local_fixed";
     tenant_id: string;
     assigned_artifact_ids: string[];
     status: "allocated" | "starting" | "ready" | "draining" | "released" | "failed";
     stream_config: {
         signalingServer: string;
         signalingPort: number;
         mediaServer: string;
     };
+    started_at: string | null;
+    last_heartbeat_at: string | null;
+    gpu_profile?: Record<string, unknown>;
     released_at: string | null;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export interface KitInstanceBinding {
kit_instance_id: string;
provider: "local_fixed";
tenant_id: string;
assigned_artifact_ids: string[];
status: "allocated" | "starting" | "ready" | "draining" | "released" | "failed";
stream_config: {
signalingServer: string;
signalingPort: number;
mediaServer: string;
};
released_at: string | null;
}
export interface KitInstanceBinding {
kit_instance_id: string;
provider: "local_fixed";
tenant_id: string;
assigned_artifact_ids: string[];
status: "allocated" | "starting" | "ready" | "draining" | "released" | "failed";
stream_config: {
signalingServer: string;
signalingPort: number;
mediaServer: string;
};
started_at: string | null;
last_heartbeat_at: string | null;
gpu_profile?: Record<string, unknown>;
released_at: string | null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-viewer-sample/src/types/review.ts` around lines 6 - 18, The
KitInstanceBinding interface is missing required spec fields so viewers can't
show binding health or GPU/capacity info; add the MUST fields to the interface:
started_at and last_heartbeat_at (both timestamps or null as per spec), and the
GPU/capacity profile fields from the spec (e.g., gpu_type, gpu_count,
capacity_profile or whatever exact names the spec uses) and ensure their types
match the spec; update KitInstanceBinding's stream_config or top-level shape as
specified so consumers like viewers can read these fields.

Comment thread web-viewer-sample/src/Window.tsx
Finding 1:queued_for_instance 不再被 viewer 當成一般 bootstrap failure。

我在 coordinatorClient.ts 新增 QueuedForInstanceError,只讓 createReviewSession() 專門辨識 409 + status=queued_for_instance,沒有改 shared request() 的通用行為。

在 Window.tsx 補 _handleQueuedForInstance(),會更新 UI lifecycle、保留 artifact bindings,並在有 reviewRequest 時 patch _bim-control:status=queued_for_instance + queuedForKitInstance lifecycle event。

Finding 2:dedicated_instance 現在會套用 capacity_slots 上限。

在 kitPool.ts 補 resolveCapacitySlots(),若 explicit capacity_slots 小於 dedicated artifact 數量,回傳空 bindings,讓 coordinator 走既有 409 queued_for_instance。

補了 unit_kitpool.test.ts 和 sessions.test.ts 覆蓋 capacity_slots=1 + 兩個 dedicated artifacts 的排隊情境。

Finding 3:review_request_id 改成毫秒 timestamp + random suffix。

在 _bim-control/app/main.py 改為 review_request_<timestamp>_<8hex>,並在 _bim-control/tests/test_review_session_requests_api.py 補 uniqueness / format 測試。
@monkey1sai

Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts in this pull request

@monkey1sai
monkey1sai marked this pull request as ready for review May 7, 2026 11:21
Copilot AI review requested due to automatic review settings May 7, 2026 11:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a session-first review workflow that treats _worker as the external artifact+conversion boundary, adds explicit review session/request lifecycle states, and updates the coordinator/viewer/runtime contracts to support artifact bindings and Kit instance routing. It also formalizes the OpenSpec branch/PR workflow and expands local runbooks, smoke scripts, and contract checks to validate the new flow.

Changes:

  • Add _worker service (API + store + tests) for artifact intake, conversion jobs, versioned object layout, and metadata callback.
  • Extend bim-review-coordinator with lifecycle statuses, artifact/kit bindings, capacity/queue behavior, and a session close endpoint; update viewer bootstrap to use review requests/sessions and lifecycle guards.
  • Update documentation, OpenSpec artifacts, and local scripts (start/stop/health/smoke) to reflect the new boundaries and contracts.

Reviewed changes

Copilot reviewed 55 out of 56 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
web-viewer-sample/src/Window.tsx Session-first bootstrap from review request/session; lifecycle-guarded runtime actions; artifact binding-aware stage open.
web-viewer-sample/src/types/review.ts Add lifecycle status, kit binding, and review session request types used by viewer clients.
web-viewer-sample/src/types/artifacts.ts Add ArtifactBinding contract used across viewer/coordinator/runtime.
web-viewer-sample/src/config/env.ts Add defaultReviewRequestId query/env support for bootstrapping.
web-viewer-sample/src/components/ArtifactPanel.tsx Display artifact bindings alongside legacy artifact list.
web-viewer-sample/src/clients/streamMessages.ts Extend openStageRequest builder to optionally include artifact_bindings.
web-viewer-sample/src/clients/coordinatorClient.ts Add queued-for-capacity error handling and new create/get session APIs.
web-viewer-sample/src/clients/bimControlClient.ts Add review session request GET/PATCH APIs and generic request helper.
web-viewer-sample/scripts/verify-session-first-contract.mjs New contract smoke to assert session-first viewer wiring and message shapes.
web-viewer-sample/package.json Add test:session-first script for the new contract check.
scripts/stop-all.ps1 Include _worker in the stop-all service list.
scripts/start-all.ps1 Start _worker and add health probe/output to dev orchestration.
scripts/smoke-worker-review-request.ps1 New API-only smoke for _worker -> _bim-control -> coordinator flow.
scripts/dev-health-check.ps1 Add optional _worker health check.
README.md Update demo path, service boundaries, and verification commands for worker-first flow.
openspec/config.yaml Document OpenSpec branch isolation + PR/Actions workflow expectations.
openspec/changes/introduce-worker-review-session-lifecycle/tasks.md New OpenSpec change task breakdown for the worker/lifecycle rollout.
openspec/changes/introduce-worker-review-session-lifecycle/specs/worker-artifact-pipeline/spec.md New spec for worker artifact + conversion pipeline contract.
openspec/changes/introduce-worker-review-session-lifecycle/specs/session-first-review-viewer/spec.md New spec for session-first viewer bootstrap and lifecycle behavior.
openspec/changes/introduce-worker-review-session-lifecycle/specs/review-session-request-lifecycle/spec.md New spec for review session request model + lifecycle patching.
openspec/changes/introduce-worker-review-session-lifecycle/specs/multi-artifact-kit-routing/spec.md New spec for artifact bindings + Kit routing policies and honesty rules.
openspec/changes/introduce-worker-review-session-lifecycle/proposal.md New OpenSpec proposal describing goals, scope, and migration plan.
openspec/changes/introduce-worker-review-session-lifecycle/design.md New OpenSpec design doc clarifying boundaries, risks, and rollout phases.
openspec/changes/introduce-worker-review-session-lifecycle/.openspec.yaml New OpenSpec change metadata file.
docs/contracts/worker-api.md New worker API contract documentation.
docs/contracts/streaming-datachannel-events.md Update DataChannel open-stage contract to include artifact bindings + applied_mode fields.
docs/contracts/review-session-api.md Document coordinator lifecycle, bindings, close endpoint, and queue response.
docs/contracts/local-dev-runbook.md Update dev runbook to include _worker and new smoke/contract checks.
docs/contracts/coordinator-socket-events.md Clarify socket validation behavior for non-active sessions.
docs/contracts/conversion-api.md Mark _conversion-service as compatibility path; point to worker contract.
docs/contracts/bim-control-fake-api.md Document review session request endpoints and artifact group APIs.
CLAUDE.md Update workspace boundary definitions to include _worker and OpenSpec workflow rules.
bim-streaming-server/source/extensions/ezplus.bim_review_stream.messaging/ezplus/bim_review_stream/messaging/stage_loading.py Add artifact-binding aware stage request resolution + loadArtifactGroupRequest/result and richer openedStageResult payload.
bim-streaming-server/scripts/tests/test-stage-loading-contract.ps1 New token-based contract smoke for stage-loading DataChannel behavior.
bim-review-coordinator/tests/unit_sessionstore.test.ts New unit tests for expanded SessionStore and mutability rules.
bim-review-coordinator/tests/unit_kitpool.test.ts New unit tests for Kit binding allocation, draining, and release helpers.
bim-review-coordinator/tests/sessions.test.ts Expand integration tests for bindings, queue behavior, close flow, and socket join rejection.
bim-review-coordinator/src/types.ts Add lifecycle statuses, routing policy, artifact bindings, and kit instance bindings types.
bim-review-coordinator/src/socket/reviewNamespace.ts Block socket actions when session is not mutable (closing/closed/failed).
bim-review-coordinator/src/services/sessionStore.ts Persist new session fields; add isSessionMutable, update/setStatus helpers.
bim-review-coordinator/src/services/kitPool.ts Implement Kit binding allocation + draining/release operations.
bim-review-coordinator/src/app.ts Wire new create/stream-config/close behaviors with lifecycle + bindings.
AGENTS.md Update core repo boundaries (add _worker) and OpenSpec GitHub workflow guidance.
.gitignore Ignore additional agent/tooling folders and _worker data directories.
_worker/tests/test_worker_store.py New unit tests for worker store safety/utilities and object layout behavior.
_worker/tests/test_worker_api.py New API tests covering artifact intake, conversions, readiness, CORS, and object serving.
_worker/requirements.txt New worker Python dependencies list.
_worker/README.md New worker service README and endpoint summary.
_worker/app/store.py New worker persistence layer for artifacts, conversions, and object layout.
_worker/app/settings.py New worker settings with env overrides and path resolution.
_worker/app/models.py New Pydantic models for artifact intake and conversion requests.
_worker/app/main.py New FastAPI app wiring endpoints, background conversion, and callback.
_worker/app/init.py Worker package initializer.
_worker/AGENTS.md Repo-local agent boundary rules for _worker.
_bim-control/tests/test_review_session_requests_api.py New tests for review session request + artifact group persistence and patching.

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

Comment on lines +514 to +521
private _assetsFromArtifactBindings(bindings: ArtifactBinding[]): USDAssetType[] {
return bindings
.filter((binding) => binding.artifact_role === "derived" && binding.ready_status === "ready" && binding.url)
.sort((left, right) => left.load_order - right.load_order)
.map((binding) => ({
name: binding.artifact_id || binding.artifact_group_id,
url: binding.url as string,
}));
Comment thread _worker/app/store.py


def safe_id(value: str, label: str) -> str:
if not SAFE_ID_RE.fullmatch(value):
Comment on lines +268 to +278
it("returns empty array for empty artifact bindings regardless of policy", () => {
const bindings = allocateKitInstanceBindings(
defaultConfig,
[],
"same_instance",
"tenant_001",
);

expect(bindings).toHaveLength(1);
expect(bindings[0].assigned_artifact_ids).toEqual([]);
});
Comment on lines 31 to 41
```json
{
"event_type": "openedStageResult",
"payload": {
"url": "http://127.0.0.1:8002/static/projects/project_demo_001/versions/version_demo_001/model.usdc",
"result": "success",
"error": ""
"error": "",
"applied_mode": "single_url",
"missing_paths": [],
"fallback_paths": []
}
Comment thread openspec/config.yaml
Comment on lines 23 to 26
- OpenSpec artifacts guide planning and execution; they do not override AGENTS.md or CLAUDE.md.
- OpenSpec changes must be isolated on `codex/openspec/<change-id>` branches before `/openspec new` or `/openspec apply`; do not develop OpenSpec changes directly on `main`.
- Pull Requests are the review/discussion boundary, GitHub Actions are the automated validation boundary, merge is formal acceptance, and OpenSpec sync/archive happens only after merge.
- Installed agent skills are workflow helpers only; they do not define product requirements and do not override repo boundary rules.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

"artifact_type": "ifc",
"name": "原始 IFC",

P2 Badge Preserve non-IFC source formats in conversion metadata

Worker intake accepts rvt and dwg, but every conversion callback stored here is recorded as IFC (and the artifact group source format is hard-coded the same way). A converted RVT/DWG therefore shows the wrong artifact type/name in _bim-control, which corrupts downstream artifact metadata and review bindings. Include the source format in the conversion result/lineage and use it here instead of hard-coding IFC.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread _bim-control/app/main.py
Comment on lines +280 to +284
def _review_request_status(
data_root: Path,
artifact_group_ids: list[str],
selected_artifact_ids: list[str],
) -> tuple[str, list[dict[str, Any]], list[str]]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Scope review request artifacts to the requested model

When a request supplies artifact_group_ids or selected_artifact_ids, this helper has no requested model/project/tenant to filter against and looks those references up globally. In a workspace with multiple model versions, a ready artifact group from version B can make a request for version A return created, and _artifact_bindings_for_request then carries B's URLs into A's review request. Pass the requested model/project into this check and reject refs that do not belong to it.

Useful? React with 👍 / 👎.

Comment thread _worker/app/main.py
Comment on lines +129 to +131
job = store.complete_conversion_job(conversion_job_id)
result = dict(job["result"])
warning = _post_bim_control_result(settings, result)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not mark conversions ready before metadata publishes

If _bim-control is temporarily unavailable during auto_complete, complete_conversion_job() has already persisted the job result as succeeded before this callback runs, and a callback failure only appends a warning. Callers polling /api/conversions/{id}/result can therefore see a successful conversion while _bim-control never received the artifact group metadata, leaving review-session requests blocked. Keep the result non-ready/failed or retry publishing before exposing success.

Useful? React with 👍 / 👎.

Agent-Logs-Url: https://github.com/monkey1sai/AI-BIM-governance/sessions/0bc8c28d-f07e-448c-b0c4-c65c19342c0a

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

Copilot AI commented May 7, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

已解決衝突並推送合併結果,提交為 32e577b。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
web-viewer-sample/src/clients/coordinatorClient.ts (1)

64-66: ⚡ Quick win

Add URL encoding for path parameters in coordinator client

Both sessionId and modelVersionId should be encoded when constructing API paths. While the backend validates path parameters with a strict pattern that only allows alphanumerics, underscores, and hyphens, client-side URL encoding provides defense-in-depth:

  • Line 65: getReviewSession(sessionId)
  • Line 69: getStreamConfig(sessionId)
  • Line 73: getReviewBootstrap(modelVersionId)

Apply encodeURIComponent() to these parameters:

Example fix
async getReviewSession(sessionId: string): Promise<ReviewSession> {
    return this.request<ReviewSession>(`/api/review-sessions/${encodeURIComponent(sessionId)}`);
}

async getStreamConfig(sessionId: string): Promise<ReviewStreamConfig> {
    return this.request<ReviewStreamConfig>(`/api/review-sessions/${encodeURIComponent(sessionId)}/stream-config`);
}

async getReviewBootstrap(modelVersionId: string): Promise<ReviewBootstrap> {
    return this.request<ReviewBootstrap>(`/api/model-versions/${encodeURIComponent(modelVersionId)}/review-bootstrap`);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-viewer-sample/src/clients/coordinatorClient.ts` around lines 64 - 66,
URL-encode path parameters before interpolating them into API paths: update
getReviewSession to call
this.request(`/api/review-sessions/${encodeURIComponent(sessionId)}`), update
getStreamConfig to call
this.request(`/api/review-sessions/${encodeURIComponent(sessionId)}/stream-config`),
and update getReviewBootstrap to call
this.request(`/api/model-versions/${encodeURIComponent(modelVersionId)}/review-bootstrap`);
ensure you use encodeURIComponent for sessionId and modelVersionId when building
the template strings in those methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@_bim-control/app/main.py`:
- Around line 529-564: Before building the request_record, explicitly validate
resource ownership: ensure safe_model_version_id belongs to payload.project_id
and that every id in payload.artifact_group_ids and
payload.selected_artifact_ids resolves to the same project (and model version
where required) instead of only checking ID format with _safe_id; call or add a
helper (e.g., _verify_resource_ownership or expand _review_request_status) to
fetch and confirm each resource’s project_id/model_version_id and fail early
with a clear error if any mismatch is found, then only proceed to call
_artifact_bindings_for_request and create request_record when ownership is
confirmed so no cross-project/version artifacts are included.
- Around line 380-390: The _append_lifecycle_event function (and the other
read-modify-write sites that call _read_list/_write_list) perform non-atomic
JSON updates and lose data under concurrent requests; wrap the entire
read-modify-write sequence in a per-file exclusive lock (e.g., using
flock/portalocker or an asyncio-compatible file lock) so only one writer can
read, modify, and write the JSON file at a time, then release the lock; apply
the same change to the other locations that call _read_list/_write_list (the
lifecycle/event and review-session-request writers) to ensure serialized updates
and atomic writes (keep the existing _read_list/_write_list use but perform lock
acquisition before calling them and release after the write).
- Around line 288-296: The current branch that handles selected_artifact_ids
treats any artifact with status == "ready" and a url as immediately usable,
allowing raw source artifacts (ifc/rvt/dwg) to be used for session binding;
restrict the selection in the selected_artifacts comprehension (and the
equivalent logic at the other block around lines 353-377) to only include
converted/loadable artifacts (e.g., artifacts whose format/type is usdc/usd or
that have mapping/converted metadata) rather than raw source formats, or
alternatively require artifact_group_ids be provided; update the filter that
builds selected_artifacts (and the readiness check using ready and missing and
the _find_by_id usage) to verify artifact.get("format")/artifact.get("type") ==
"usdc" (or presence of conversion mapping flag) and only then consider status ==
"ready" and url for returning "created".
- Around line 724-730: The ready_status calculation in the group dict
incorrectly marks a group as "ready" based only on usdc_url and
result.get("mapping_url") even when conversion failed; update the ready_status
logic inside the group construction (the ready_status field) to also require
result.get("status") == "succeeded" (or not "failed") so that ready_status
becomes "ready" only when conversion succeeded and both usdc_url and mapping_url
are present; reference result.get("status"), usdc_url,
result.get("mapping_url"), and the _review_request_status() flow to ensure
failed conversions are not treated as ready.

---

Nitpick comments:
In `@web-viewer-sample/src/clients/coordinatorClient.ts`:
- Around line 64-66: URL-encode path parameters before interpolating them into
API paths: update getReviewSession to call
this.request(`/api/review-sessions/${encodeURIComponent(sessionId)}`), update
getStreamConfig to call
this.request(`/api/review-sessions/${encodeURIComponent(sessionId)}/stream-config`),
and update getReviewBootstrap to call
this.request(`/api/model-versions/${encodeURIComponent(modelVersionId)}/review-bootstrap`);
ensure you use encodeURIComponent for sessionId and modelVersionId when building
the template strings in those methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9e077bb7-7e0a-47b5-a28f-0d5d6fbb67a5

📥 Commits

Reviewing files that changed from the base of the PR and between b121ec2 and ad4cb3c.

📒 Files selected for processing (8)
  • _bim-control/app/main.py
  • _bim-control/tests/test_review_session_requests_api.py
  • bim-review-coordinator/src/services/kitPool.ts
  • bim-review-coordinator/tests/sessions.test.ts
  • bim-review-coordinator/tests/unit_kitpool.test.ts
  • web-viewer-sample/scripts/verify-session-first-contract.mjs
  • web-viewer-sample/src/Window.tsx
  • web-viewer-sample/src/clients/coordinatorClient.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • web-viewer-sample/scripts/verify-session-first-contract.mjs
  • bim-review-coordinator/tests/unit_kitpool.test.ts
  • web-viewer-sample/src/Window.tsx
  • _bim-control/tests/test_review_session_requests_api.py

Comment thread _bim-control/app/main.py
Comment on lines +288 to +296
if not artifact_group_ids and selected_artifact_ids:
selected_artifacts = [
artifact
for artifact in artifacts
if artifact.get("artifact_id") in selected_artifact_ids
]
ready = all(artifact.get("status") == "ready" and artifact.get("url") for artifact in selected_artifacts)
missing = [artifact_id for artifact_id in selected_artifact_ids if not _find_by_id(selected_artifacts, "artifact_id", artifact_id)]
return ("created" if ready and not missing else "blocked_conversion", [], missing)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

selected_artifact_ids 不該把原始 source artifact 視為可直接開啟的 session binding。

這條路徑目前把任何 status == "ready" 且有 url 的 artifact 都當成可建立 request,並且會為 ifc / rvt / dwg 產生 binding。這會讓 raw source artifact 繞過 conversion readiness,建立出 coordinator / streaming server 其實無法直接載入的 session。這裡應限制為可載入的 converted artifact(至少 usdc + mapping),或直接要求走 artifact_group_ids。

Based on learnings, bim-streaming-server is the Omniverse Kit runtime for USD/USDC stage runtime, while _worker owns IFC/RVT/DWG conversion.

Also applies to: 353-377

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_bim-control/app/main.py` around lines 288 - 296, The current branch that
handles selected_artifact_ids treats any artifact with status == "ready" and a
url as immediately usable, allowing raw source artifacts (ifc/rvt/dwg) to be
used for session binding; restrict the selection in the selected_artifacts
comprehension (and the equivalent logic at the other block around lines 353-377)
to only include converted/loadable artifacts (e.g., artifacts whose format/type
is usdc/usd or that have mapping/converted metadata) rather than raw source
formats, or alternatively require artifact_group_ids be provided; update the
filter that builds selected_artifacts (and the readiness check using ready and
missing and the _find_by_id usage) to verify
artifact.get("format")/artifact.get("type") == "usdc" (or presence of conversion
mapping flag) and only then consider status == "ready" and url for returning
"created".

Comment thread _bim-control/app/main.py
Comment on lines +380 to +390
def _append_lifecycle_event(data_root: Path, request_id: str, event_type: str, payload: dict[str, Any]) -> dict[str, Any]:
events_path = data_root / "lifecycle_events.json"
event = {
"event_id": f"lifecycle_{int(datetime.now(UTC).timestamp() * 1000)}",
"review_request_id": request_id,
"type": event_type,
"payload": payload,
"created_at": _now(),
}
_write_list(events_path, [*_read_list(events_path), event])
return event

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

這些 JSON 更新在併發下會直接掉資料。

這幾段都是 _read_list() → 修改記憶體內容 → _write_list() 的 read-modify-write,沒有任何 lock。兩個同時的 POST /api/review-session-requests 或 PATCH/lifecycle event 寫入,後寫者會覆蓋前寫者,request 或 event 會被悄悄吃掉。這類共享 JSON 檔的更新至少要做同檔案序列化。

Also applies to: 571-573, 602-605

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_bim-control/app/main.py` around lines 380 - 390, The _append_lifecycle_event
function (and the other read-modify-write sites that call
_read_list/_write_list) perform non-atomic JSON updates and lose data under
concurrent requests; wrap the entire read-modify-write sequence in a per-file
exclusive lock (e.g., using flock/portalocker or an asyncio-compatible file
lock) so only one writer can read, modify, and write the JSON file at a time,
then release the lock; apply the same change to the other locations that call
_read_list/_write_list (the lifecycle/event and review-session-request writers)
to ensure serialized updates and atomic writes (keep the existing
_read_list/_write_list use but perform lock acquisition before calling them and
release after the write).

Comment thread _bim-control/app/main.py
Comment on lines +529 to +564
safe_model_version_id = _safe_id(payload.model_version_id, "model_version_id")
for group_id in payload.artifact_group_ids:
_safe_id(group_id, "artifact_group_id")
for artifact_id in payload.selected_artifact_ids:
_safe_id(artifact_id, "artifact_id")
status, ready_groups, missing_refs = _review_request_status(
resolved_data_root,
payload.artifact_group_ids,
payload.selected_artifact_ids,
)
routing_policy = str(payload.startup_policy.get("routing_policy") or "same_instance")
artifact_bindings = (
_artifact_bindings_for_request(
resolved_data_root,
safe_model_version_id,
ready_groups,
payload.selected_artifact_ids,
routing_policy,
)
if status == "created"
else []
)
request_record = {
"review_request_id": request_id,
"requested_by": payload.requested_by,
"tenant_id": payload.tenant_id,
"project_id": payload.project_id,
"model_version_id": safe_model_version_id,
"artifact_group_ids": payload.artifact_group_ids,
"selected_artifact_ids": payload.selected_artifact_ids,
"startup_policy": payload.startup_policy,
"kit_profile": payload.kit_profile,
"status": status,
"blocker": "conversion_readiness" if status == "blocked_conversion" else None,
"missing_refs": missing_refs,
"artifact_groups": ready_groups,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

建立 request 前先驗證引用資源的歸屬。

這裡只檢查 artifact_group_id / artifact_id 的格式,後面 _review_request_status() 會直接用全域 ID 查資料。這會允許把其他 project_id / model_version_id 的 group 或 artifact 寫進目前的 request,最後產生跨版本的 artifact_bindings 與錯誤 metadata 關聯。至少要先確認 model_version_id 屬於 project_id,且所有引用都屬於同一個 project/version。

As per coding guidelines, _bim-control/**/*.py: All metadata writes must include data descriptions and relationships (e.g., model_version_id, artifact_id, file_url, issue_id, ifc_guid, usd_prim_path).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_bim-control/app/main.py` around lines 529 - 564, Before building the
request_record, explicitly validate resource ownership: ensure
safe_model_version_id belongs to payload.project_id and that every id in
payload.artifact_group_ids and payload.selected_artifact_ids resolves to the
same project (and model version where required) instead of only checking ID
format with _safe_id; call or add a helper (e.g., _verify_resource_ownership or
expand _review_request_status) to fetch and confirm each resource’s
project_id/model_version_id and fail early with a clear error if any mismatch is
found, then only proceed to call _artifact_bindings_for_request and create
request_record when ownership is confirmed so no cross-project/version artifacts
are included.

Comment thread _bim-control/app/main.py
Comment on lines +724 to +730
group = {
"artifact_group_id": artifact_group_id,
"tenant_id": tenant_id,
"project_id": project_id,
"model_version_id": model_version_id,
"status": "ready" if result.get("status") == "succeeded" else str(result.get("status") or "unknown"),
"ready_status": "ready" if usdc_url and result.get("mapping_url") else "blocked_conversion",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

不要在 conversion 失敗時把 artifact group 標成 ready。

ready_status 目前只看 usdc_url 和 mapping_url。如果 worker 回傳 status="failed" 但夾帶舊的 URL,這裡仍會把 group 標成 ready,後續 _review_request_status() 就會放行建立 review request。ready_status 應該和 conversion 成功狀態一起判斷。

💡 建議修正
+    conversion_succeeded = result.get("status") == "succeeded"
     group = {
         "artifact_group_id": artifact_group_id,
         "tenant_id": tenant_id,
         "project_id": project_id,
         "model_version_id": model_version_id,
-        "status": "ready" if result.get("status") == "succeeded" else str(result.get("status") or "unknown"),
-        "ready_status": "ready" if usdc_url and result.get("mapping_url") else "blocked_conversion",
+        "status": "ready" if conversion_succeeded else str(result.get("status") or "unknown"),
+        "ready_status": "ready" if conversion_succeeded and usdc_url and result.get("mapping_url") else "blocked_conversion",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
group = {
"artifact_group_id": artifact_group_id,
"tenant_id": tenant_id,
"project_id": project_id,
"model_version_id": model_version_id,
"status": "ready" if result.get("status") == "succeeded" else str(result.get("status") or "unknown"),
"ready_status": "ready" if usdc_url and result.get("mapping_url") else "blocked_conversion",
conversion_succeeded = result.get("status") == "succeeded"
group = {
"artifact_group_id": artifact_group_id,
"tenant_id": tenant_id,
"project_id": project_id,
"model_version_id": model_version_id,
"status": "ready" if conversion_succeeded else str(result.get("status") or "unknown"),
"ready_status": "ready" if conversion_succeeded and usdc_url and result.get("mapping_url") else "blocked_conversion",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_bim-control/app/main.py` around lines 724 - 730, The ready_status
calculation in the group dict incorrectly marks a group as "ready" based only on
usdc_url and result.get("mapping_url") even when conversion failed; update the
ready_status logic inside the group construction (the ready_status field) to
also require result.get("status") == "succeeded" (or not "failed") so that
ready_status becomes "ready" only when conversion succeeded and both usdc_url
and mapping_url are present; reference result.get("status"), usdc_url,
result.get("mapping_url"), and the _review_request_status() flow to ensure
failed conversions are not treated as ready.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
bim-review-coordinator/tests/sessions.test.ts (2)

100-103: 💤 Low value

Tighten the binding-array assertions in the default-session test.

This it creates a session with no artifact_bindings and no explicit kit_profile, so the response should not just be "an array" — it should be the empty/expected shape. Asserting only Array.isArray(...) will keep passing if the coordinator accidentally leaks stale bindings or returns a populated array from a previous session/test.

♻️ Suggested tightening
     expect(config.body.lifecycle_status).toBe("active");
-    expect(Array.isArray(config.body.artifact_bindings)).toBe(true);
-    expect(Array.isArray(config.body.kit_instance_bindings)).toBe(true);
+    expect(config.body.artifact_bindings).toEqual([]);
+    // Default session auto-allocates a single shared Kit instance binding.
+    expect(Array.isArray(config.body.kit_instance_bindings)).toBe(true);
+    expect(config.body.kit_instance_bindings.length).toBeGreaterThanOrEqual(0);

If the default coordinator behavior auto-allocates one shared Kit binding (as suggested by kit_instance_bindings coverage further down), prefer toHaveLength(1) instead of >= 0 so the assertion actually pins behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bim-review-coordinator/tests/sessions.test.ts` around lines 100 - 103,
Tighten the assertions in the default-session test so you verify the exact
expected shapes instead of just "is array": replace the loose Array.isArray
checks on config.body.artifact_bindings and config.body.kit_instance_bindings
with explicit length/shape assertions — e.g. assert artifact_bindings is an
empty array (expect(config.body.artifact_bindings).toHaveLength(0) or
toEqual([])) and assert kit_instance_bindings matches the expected default count
(if coordinator auto-allocates one, use
expect(config.body.kit_instance_bindings).toHaveLength(1); otherwise assert
exact empty array), updating the assertions that reference
config.body.artifact_bindings and config.body.kit_instance_bindings in the
sessions.test.ts "default-session" test.

536-594: 💤 Low value

Section header advertises markKitBindingsDraining coverage that the tests don't actually exercise.

The header on lines 536–538 promises tests for allocateKitInstanceBindings, markKitBindingsDraining, releaseKitBindings, but the only test referencing draining (lines 566–594, "released bindings stay released when markKitBindingsDraining is called after close") never drives the session into a draining state — it goes straight created → close, which transitions bindings via the normal release path. So markKitBindingsDraining is effectively untested here, and the test name is misleading for anyone debugging that code path later.

Either rename the test to reflect what it actually verifies (e.g., "close releases all kit bindings and stamps released_at"), or add a separate test that explicitly puts a binding into draining and then asserts release is a no-op / stays released. Otherwise the section comment is a hazard for future readers grepping for draining coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bim-review-coordinator/tests/sessions.test.ts` around lines 536 - 594, The
section header claims coverage for allocateKitInstanceBindings,
markKitBindingsDraining, releaseKitBindings but the existing test "released
bindings stay released when markKitBindingsDraining is called after close" never
calls markKitBindingsDraining — fix by either renaming that test to something
like "close releases all kit bindings and stamps released_at" to reflect the
actual behavior, or add a new test that uses the API/path that triggers
markKitBindingsDraining (exercise the markKitBindingsDraining code path on a
session or binding, then call close/release and assert the binding remains
released and released_at is set), referencing the test file
tests/sessions.test.ts and the functions/behaviors markKitBindingsDraining,
allocateKitInstanceBindings, and releaseKitBindings so the draining code path is
explicitly exercised.
_worker/tests/test_worker_api.py (1)

348-523: ⚡ Quick win

這段 API 測試和前半段重覆度很高,建議合併。

/health、artifact-group 404/readiness、generate_mapping=False、object traversal/missing file、RVT intake 等情境前面都已測過;保留一組或改成 parametrize 會比較乾淨,否則 suite 時間和失敗訊號都會被放大。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_worker/tests/test_worker_api.py` around lines 348 - 523, Tests in this file
are highly duplicated across multiple test functions (e.g.,
test_health_returns_ok, test_get_artifact_group_returns_404_for_missing,
test_get_artifact_group_readiness_returns_404_for_missing,
test_conversion_without_mapping_has_null_mapping_url,
test_object_endpoint_rejects_path_traversal,
test_object_endpoint_returns_404_for_missing_file,
test_artifact_intake_accepts_rvt_format) — consolidate them by removing exact
duplicates and/or converting similar cases to parametrized tests using
pytest.mark.parametrize; keep one canonical test per behavior (health endpoint,
artifact-group 404/readiness, conversion mapping False, object traversal/missing
file, RVT intake) and reference the existing function names above when locating
and merging test logic to ensure coverage is preserved and suite runtime is
reduced.
_worker/tests/test_worker_store.py (1)

517-863: ⚡ Quick win

下半段測試與 helper 幾乎重覆前半段,建議收斂成一套。

safe_id、safe_filename、model validation、Settings、WorkerStore flow 在上半段都已覆蓋;這裡又重新定義一次 make_store,並重跑相近斷言,只會讓後續漂移、執行時間與 failure noise 一起增加。保留一種寫法,或改成 @pytest.mark.parametrize 併案即可。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_worker/tests/test_worker_store.py` around lines 517 - 863, The tests and
helpers in the lower half duplicate the earlier coverage
(safe_id/safe_filename/model validation/Settings/WorkerStore) and redefine
helpers (make_store, make_intake_request); consolidate by removing duplicated
tests and reusing the existing helpers or convert the repeated assertions into
parametrized tests using the existing make_store and make_intake_request
functions; specifically, delete the redundant helper definitions (make_store,
make_intake_request) in this block or replace duplicated test cases like
test_store_create_source_artifact_* and test_store_complete_conversion_* with
pytest.mark.parametrize variants that call the original helper functions and
assert the unique expectations (refer to symbols: make_store,
make_intake_request, WorkerStore.create_source_artifact,
WorkerStore.create_conversion_job, WorkerStore.complete_conversion_job,
get_artifact_group).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@_worker/tests/test_worker_store.py`:
- Around line 582-583: Replace the broad pytest.raises(Exception) in the three
Pydantic schema tests (including
test_artifact_intake_request_requires_at_least_one_source_field) with
pytest.raises(pydantic.ValidationError); ensure you import ValidationError
(e.g., from pydantic import ValidationError or use pydantic.ValidationError) so
each test asserts the specific Pydantic validation error rather than catching
all Exceptions.

---

Nitpick comments:
In `@_worker/tests/test_worker_api.py`:
- Around line 348-523: Tests in this file are highly duplicated across multiple
test functions (e.g., test_health_returns_ok,
test_get_artifact_group_returns_404_for_missing,
test_get_artifact_group_readiness_returns_404_for_missing,
test_conversion_without_mapping_has_null_mapping_url,
test_object_endpoint_rejects_path_traversal,
test_object_endpoint_returns_404_for_missing_file,
test_artifact_intake_accepts_rvt_format) — consolidate them by removing exact
duplicates and/or converting similar cases to parametrized tests using
pytest.mark.parametrize; keep one canonical test per behavior (health endpoint,
artifact-group 404/readiness, conversion mapping False, object traversal/missing
file, RVT intake) and reference the existing function names above when locating
and merging test logic to ensure coverage is preserved and suite runtime is
reduced.

In `@_worker/tests/test_worker_store.py`:
- Around line 517-863: The tests and helpers in the lower half duplicate the
earlier coverage (safe_id/safe_filename/model validation/Settings/WorkerStore)
and redefine helpers (make_store, make_intake_request); consolidate by removing
duplicated tests and reusing the existing helpers or convert the repeated
assertions into parametrized tests using the existing make_store and
make_intake_request functions; specifically, delete the redundant helper
definitions (make_store, make_intake_request) in this block or replace
duplicated test cases like test_store_create_source_artifact_* and
test_store_complete_conversion_* with pytest.mark.parametrize variants that call
the original helper functions and assert the unique expectations (refer to
symbols: make_store, make_intake_request, WorkerStore.create_source_artifact,
WorkerStore.create_conversion_job, WorkerStore.complete_conversion_job,
get_artifact_group).

In `@bim-review-coordinator/tests/sessions.test.ts`:
- Around line 100-103: Tighten the assertions in the default-session test so you
verify the exact expected shapes instead of just "is array": replace the loose
Array.isArray checks on config.body.artifact_bindings and
config.body.kit_instance_bindings with explicit length/shape assertions — e.g.
assert artifact_bindings is an empty array
(expect(config.body.artifact_bindings).toHaveLength(0) or toEqual([])) and
assert kit_instance_bindings matches the expected default count (if coordinator
auto-allocates one, use
expect(config.body.kit_instance_bindings).toHaveLength(1); otherwise assert
exact empty array), updating the assertions that reference
config.body.artifact_bindings and config.body.kit_instance_bindings in the
sessions.test.ts "default-session" test.
- Around line 536-594: The section header claims coverage for
allocateKitInstanceBindings, markKitBindingsDraining, releaseKitBindings but the
existing test "released bindings stay released when markKitBindingsDraining is
called after close" never calls markKitBindingsDraining — fix by either renaming
that test to something like "close releases all kit bindings and stamps
released_at" to reflect the actual behavior, or add a new test that uses the
API/path that triggers markKitBindingsDraining (exercise the
markKitBindingsDraining code path on a session or binding, then call
close/release and assert the binding remains released and released_at is set),
referencing the test file tests/sessions.test.ts and the functions/behaviors
markKitBindingsDraining, allocateKitInstanceBindings, and releaseKitBindings so
the draining code path is explicitly exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bfeec5f9-13ce-49eb-9fd2-925299574d38

📥 Commits

Reviewing files that changed from the base of the PR and between ad4cb3c and 32e577b.

📒 Files selected for processing (4)
  • _bim-control/tests/test_review_session_requests_api.py
  • _worker/tests/test_worker_api.py
  • _worker/tests/test_worker_store.py
  • bim-review-coordinator/tests/sessions.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • _bim-control/tests/test_review_session_requests_api.py

Comment on lines +582 to +583
def test_artifact_intake_request_requires_at_least_one_source_field():
with pytest.raises(Exception):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the test file
fd "test_worker_store.py" -type f

Repository: monkey1sai/AI-BIM-governance

Length of output: 243


🏁 Script executed:

# Get the structure and look at the specific lines mentioned
wc -l _worker/tests/test_worker_store.py

Repository: monkey1sai/AI-BIM-governance

Length of output: 111


🏁 Script executed:

# Read the test file around the specified lines
cat -n _worker/tests/test_worker_store.py | sed -n '575,590p'

Repository: monkey1sai/AI-BIM-governance

Length of output: 740


🏁 Script executed:

# Check lines 641-642
cat -n _worker/tests/test_worker_store.py | sed -n '635,650p'

Repository: monkey1sai/AI-BIM-governance

Length of output: 761


🏁 Script executed:

# Check lines 665-666
cat -n _worker/tests/test_worker_store.py | sed -n '660,670p'

Repository: monkey1sai/AI-BIM-governance

Length of output: 525


🏁 Script executed:

# Check the imports at the top of the file
head -30 _worker/tests/test_worker_store.py | cat -n

Repository: monkey1sai/AI-BIM-governance

Length of output: 1152


🏁 Script executed:

# Find the models.py file
fd "models.py" _worker -type f

Repository: monkey1sai/AI-BIM-governance

Length of output: 243


🏁 Script executed:

# Read the models file to see the model definitions
cat _worker/app/models.py | head -100

Repository: monkey1sai/AI-BIM-governance

Length of output: 1395


🏁 Script executed:

# Get the full structure of models.py
wc -l _worker/app/models.py

Repository: monkey1sai/AI-BIM-governance

Length of output: 97


Use ValidationError instead of Exception for Pydantic model validation tests.

These tests validate Pydantic model schema behavior. Catching Exception masks other unexpected errors and weakens test signal. Replace with pydantic.ValidationError on all three occurrences.

Changes required
 import pytest
+from pydantic import ValidationError
 
 from app.models import ArtifactIntakeRequest, ConversionRequest, ConversionOptions

Then update the three test cases:

 def test_artifact_intake_request_requires_at_least_one_source_field():
-    with pytest.raises(Exception):
+    with pytest.raises(ValidationError):
 def test_artifact_intake_request_rejects_invalid_source_format():
-    with pytest.raises(Exception):
+    with pytest.raises(ValidationError):
 def test_conversion_request_requires_min_length_source_artifact_id():
-    with pytest.raises(Exception):
+    with pytest.raises(ValidationError):
🧰 Tools
🪛 Ruff (0.15.12)

[warning] 583-583: Do not assert blind exception: Exception

(B017)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_worker/tests/test_worker_store.py` around lines 582 - 583, Replace the broad
pytest.raises(Exception) in the three Pydantic schema tests (including
test_artifact_intake_request_requires_at_least_one_source_field) with
pytest.raises(pydantic.ValidationError); ensure you import ValidationError
(e.g., from pydantic import ValidationError or use pydantic.ValidationError) so
each test asserts the specific Pydantic validation error rather than catching
all Exceptions.

@monkey1sai

Copy link
Copy Markdown
Owner Author

Superseded by PR #11

PR #11 (feat(worker)!: 統一 demo 步驟 ①/② 至 _worker 並退役 legacy storage/conversion) is built on top of this branch and contains all of this PR's content plus the dev IFC source selection flow and legacy service retirement. PR #11 has been merged into main (commit 9d36e15).

Closing this PR to avoid duplicate conflict resolution. The introduce-worker-review-session-lifecycle OpenSpec change has been archived under openspec/changes/archive/2026-05-07-introduce-worker-review-session-lifecycle/ as part of PR #11's merge.

Refs:

@monkey1sai monkey1sai closed this May 7, 2026
monkey1sai added a commit that referenced this pull request May 7, 2026
…ability specs

PR #11 已 merge 至 main(commit 9d36e15),依 CLAUDE.md §0.1 將 OpenSpec change
從 active 移至 archive 並同步 ADDED requirements 至 live specs。

- 移動 openspec/changes/add-dev-ifc-source-selection-flow/ →
  openspec/changes/archive/2026-05-07-add-dev-ifc-source-selection-flow/
- 同步 4 個新 capability live spec:
  * worker-dev-ifc-source-selection
  * worker-demo-upload-convert-ui
  * legacy-storage-conversion-retirement
  * streaming-multi-layer-payload-loading
- 4 specs 均通過 openspec validate

PR #9 (codex/openspec/introduce-worker-review-session-lifecycle) 已被 PR #11
superseded 並 close;其 spec 已於 PR #11 merge 時 archive 為
2026-05-07-introduce-worker-review-session-lifecycle。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request May 7, 2026
…r-from-pr9

feat(introduce-worker)!: backfill PR #9 universal fixes 依 OpenSpec 順序進入 main
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 11, 2026
* docs(openspec): 建立 workflow v3 與 SaaS 路線圖分工 OpenSpec change

新增 documentation-source-of-truth capability,明確:
- workflow v3 (docs/PROJECT_DEVELOPMENT_WORKFLOW.md) = 開發流程入口
  (七層架構、Phase 完成度、驗證 4 層、品質管線 7 步、PR Checklist、sequence diagram)
- SaaS 路線圖 (docs/plans/AI-BIM-governance-saas-roadmap-2026-05.md)
  = OpenSpec 候選 #1-#9 + #1A/#2A、NVIDIA 採用決策 §13、§11.4 Multi-Kit
  Instance 並行官方定義、§9 硬體配置、§11 MCP 查詢結果權威
- 兩份文件互補不替代,互相 cross-reference
- 任何後續分工調整必須走 OpenSpec change,不直接在 main 上 commit

具體變動:
- 新增 openspec/changes/align-workflow-v3-with-saas-roadmap/
  含 proposal.md、tasks.md、specs/documentation-source-of-truth/spec.md
- docs/plans/AI-BIM-governance-saas-roadmap-2026-05.md §1 加 cross-reference
  指向 workflow v3
- README.md 新增「核心文件入口」段,列出 AGENTS.md / workflow v3 / 路線圖 /
  openspec specs 四份權威的角色與閱讀順序

對應 PR #8 在 cursor/fix/date---feature/fix/issue/project-development-workflow-877a
分支上的 workflow v3 對齊 commit 3e2eedc;兩個 PR 並行 review,無強依賴。

openspec validate align-workflow-v3-with-saas-roadmap: ✓ valid
git diff --check: ✓ no whitespace issues

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

* docs(openspec): 修正 merge order + 補 §1.2 / §1.4 staleness

依 code review 補正:

1. **Merge order 改為強依賴**(critical):
   - proposal.md `## Impact`:「兩個 PR 無強依賴」→「PR #8 必須先 merge 進 main,
     本 PR 才能 merge」;否則 main 上會立刻產生 dead link 並違反本 PR 自身新增的
     documentation-source-of-truth 「cross-reference 持續成立」requirement
   - tasks.md 新增 §5 Merge Order Coordination 區段

2. **修正 main roadmap §1.2 staleness**:
   - 段首「9 個 capability」→「10 個 capability」
   - 表格末加 row `runtime-verification-task-status`(v1 Phase 3 / v2 Layer 6)
   - main 上 openspec/specs/ 實際有 10 個目錄,PR #20 之後 §1.2 一直 stale

3. **修正 main roadmap §1.4 staleness**:
   - 加 row `2026-05-08-fix-runtime-verification-task-status` archived change
   - main 上 openspec/changes/archive/ 實際有 5 個目錄,§1.4 只列 4 個

4. **tasks.md 重組**:
   - §3 修正 roadmap §1.2 / §1.4 staleness
   - §4 Validate(原 §3)
   - §5 Merge Order Coordination(新增)
   - §6 Archive(原 §4)

對應 PR #23 review (#23 (comment))
的 Critical 與 Warning issues。

openspec validate align-workflow-v3-with-saas-roadmap: ✓ valid
git diff --check: ✓ no whitespace issues

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@monkey1sai
monkey1sai deleted the codex/openspec/introduce-worker-review-session-lifecycle branch May 12, 2026 02:08
monkey1sai added a commit that referenced this pull request May 26, 2026
…budget

PR #122 已 merged 至 main(commit f217660)。執行 OpenSpec archive:
- openspec/changes/slim-agents-md-auto-load/
  → openspec/changes/archive/2026-05-26-slim-agents-md-auto-load/
- 新 capability spec synced:
  openspec/specs/agent-doc-context-budget/spec.md

Roadmap sync 評估:本 change 屬 agent-tooling / doc-governance,不對應
SaaS roadmap §1.6 的 OpenSpec 候選 #1-#9,不影響 Phase 狀態 / 硬體配置 /
NVIDIA 採用決策,故不需更新 docs/plans/AI-BIM-governance-saas-roadmap-2026-05.md。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request May 26, 2026
PR #123 已 merged 至 main(commit f69e849)。執行 OpenSpec archive:
- openspec/changes/trim-docs-and-dedupe-ide-skills/
  → openspec/changes/archive/2026-05-26-trim-docs-and-dedupe-ide-skills/
- Spec delta synced:
  - openspec/specs/documentation-source-of-truth/spec.md (~1 modified + 1 added)
  - openspec/specs/agent-doc-context-budget/spec.md (+ 2 requirements)

Roadmap sync 評估:本 change 屬 agent-tooling / doc-governance(HTML on-demand、
evidence archive sibling、IDE mirror stub),不對應 SaaS roadmap §1.6 OpenSpec
候選 #1-#9,不影響 Phase 狀態 / 硬體配置 / NVIDIA 採用決策,故不需更新
docs/plans/AI-BIM-governance-saas-roadmap-2026-05.md。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request Jun 1, 2026
opus 對抗式 review 4 major:
- [major] #7 shutdown 死鎖: io.close 嵌在 server.close callback 內,Socket.IO keep-alive 連線讓 server.close callback 永不觸發 → exit(0) 到不了。抽 createGracefulShutdown(src/shutdown.ts)修正順序 io→server + 可注入 deps;補 shutdown.test.ts 3 unit
- [major] #9 strict test 沒 pin 502 原因: 加 expect(reason=http_status),擋 fallback-mode regression 假綠
- [major] #9 non-strict fallback 零覆蓋: 補對稱測試(non-strict + http non-2xx → 202 + dispatch),鎖 strict 接線不破壞 demo loop
- #7 signal handler 接線由 shutdown.test 直接覆蓋(取代只測 dispose 本體)

驗證: npm run verify 264 passed(260 + 4 新,tsc 0 error)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request Jun 1, 2026
… 繁中)

CodeRabbit + Codex review:
- [CodeRabbit major] shutdown.ts dispose 未 guard: try/catch,drain 失敗仍繼續 io/server close + exit(0),不 hang 到 SIGKILL
- [Codex P2] compose IFC_DOWNLOAD_STRICT hard-code false 覆蓋 .env: 改 ${IFC_DOWNLOAD_STRICT:-false} 讓 operator env 可覆蓋(否則 #9 production 指引失效)
- [CodeRabbit major] design/spec 章節改繁中(OpenSpec rules,parser headers 保留)
- defer follow-up(document 在 design): (Codex P2)non-http ref strict 未生效(超出 #9 http scope)、(Codex P2)in-flight intake drain race(既有 dispose 時機,#7 未惡化)

驗證: npm run verify 264 passed; openspec validate --strict 通過。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request Jun 1, 2026
…rol 死碼 (CH-2: #9 #7 #19) (#142)

* docs(openspec): 提案 harden-coordinator-ifc-intake

CH-2 收 coordinator 3 風險(#9 IFC strict 接線 / #7 graceful dispose signal / #19 退役 _bim-control 死碼)。proposal/design/tasks + spec delta(local-coordinator-ifc-ready-intake-boundary ADD 1、project-risks-mitigation ADD 1;#19 tasks-only);openspec validate --strict 通過。

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

* fix(coordinator): harden IFC intake + graceful dispose + 移除 _bim-control 死碼 (#9 #7 #19)

CH-2 apply:
- #9 IFC strict 接線: config 加 ifcDownloadStrict(parseBooleanEnv IFC_DOWNLOAD_STRICT default false);app downloadIfcToSharedVolume 加 fallbackOnFetchError=!strict;compose 加 IFC_DOWNLOAD_STRICT(production 設 true→non-2xx 回 502 不靜默 placeholder)
- #7 graceful dispose: index.ts 接 SIGTERM/SIGINT → shutdown(dispose drain→server.close→io.close→exit 0),補實作 RISK-IN-MEMORY-QUEUE-PERSISTENCE graceful shutdown
- #19 退役死碼: 刪 BimControlClient + safeArtifacts + config bimControlApiBase + compose BIM_CONTROL_API_BASE 死 env;保留 Artifact type 與簽章;清 11 個 test makeApp

驗證: npm run verify 260 passed(baseline 257+3 新,tsc 0 error); root pytest 65 passed(零回歸)。

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

* fix(coordinator): CH-2 review fixes (#7 shutdown 順序死鎖 + #9 測試保護網)

opus 對抗式 review 4 major:
- [major] #7 shutdown 死鎖: io.close 嵌在 server.close callback 內,Socket.IO keep-alive 連線讓 server.close callback 永不觸發 → exit(0) 到不了。抽 createGracefulShutdown(src/shutdown.ts)修正順序 io→server + 可注入 deps;補 shutdown.test.ts 3 unit
- [major] #9 strict test 沒 pin 502 原因: 加 expect(reason=http_status),擋 fallback-mode regression 假綠
- [major] #9 non-strict fallback 零覆蓋: 補對稱測試(non-strict + http non-2xx → 202 + dispatch),鎖 strict 接線不破壞 demo loop
- #7 signal handler 接線由 shutdown.test 直接覆蓋(取代只測 dispose 本體)

驗證: npm run verify 264 passed(260 + 4 新,tsc 0 error)。

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

* fix(coordinator): CH-2 review round 2 (shutdown guard / compose env / 繁中)

CodeRabbit + Codex review:
- [CodeRabbit major] shutdown.ts dispose 未 guard: try/catch,drain 失敗仍繼續 io/server close + exit(0),不 hang 到 SIGKILL
- [Codex P2] compose IFC_DOWNLOAD_STRICT hard-code false 覆蓋 .env: 改 ${IFC_DOWNLOAD_STRICT:-false} 讓 operator env 可覆蓋(否則 #9 production 指引失效)
- [CodeRabbit major] design/spec 章節改繁中(OpenSpec rules,parser headers 保留)
- defer follow-up(document 在 design): (Codex P2)non-http ref strict 未生效(超出 #9 http scope)、(Codex P2)in-flight intake drain race(既有 dispose 時機,#7 未惡化)

驗證: npm run verify 264 passed; 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 Jul 10, 2026
* docs(superpowers): plans×code 修復輪 spec+plan(grill 裁決 R1–R10)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G9C6aJGUJR37PgWSBPBNVf

* docs(plans): IX-3D-01 承認 a1-inline、A.1.1 #conv 列改 alias(R1 裁決落地)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G9C6aJGUJR37PgWSBPBNVf

* docs(plans): exception ledger 補登 3 筆+§5.A/§1.9/加性慣例/#conv 除役同步(R1/R3/R6)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G9C6aJGUJR37PgWSBPBNVf

* docs(plans): README 鐵律#7/#9 依現實與 R2/R8 裁決改寫、A3 clash 敘述更新、#8 埠表補顆粒度

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G9C6aJGUJR37PgWSBPBNVf

* docs(plans): 對齊矩陣 #conv/歷史頁/手動觸發列與紀律 D-11/§2.3/§11/§13E1 依 R2 裁決同步

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G9C6aJGUJR37PgWSBPBNVf

* chore: re-run CI after PR body evidence update

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G9C6aJGUJR37PgWSBPBNVf

* chore: re-run CI after body evidence table restore

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G9C6aJGUJR37PgWSBPBNVf

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
monkey1sai added a commit that referenced this pull request Aug 26, 2026
…untime truth via shared poller(unified-console-runtime-truth slice 1) (#700)

* docs(spec-to-done): unified-console-runtime-truth slice 1 範圍文件(§1 真值綁定+§5 測試對齊)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE

* plan: unified-console-runtime-truth slice 1 實作計畫(§1 真值綁定+§5 測試對齊,十個 task)

- 共用 poller store(單一 in-flight/退避 ≤60s/hidden 暫停)+ ConsoleDataProvider
- #home/#pipeline/#runtime/頂列 GPU chip/側欄 badge 綁十個既有端點,data-state 四值誠實渲染
- 假資料 export 移 test-only(D1=P),docks/WorkspacePage 四個欠帳以 ratchet 釘住
- 5.1–5.4 既有測試與 semantic cases 翻轉為誠實狀態斷言;Playwright E2E 打真後端 :8004
- 收官 gate、tasks.md 只加「本機綠,待 181」子彈;blocker 八點交 coordinator 裁決

Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* plan: fix Task 3 拆為 3a/3b/3c,sweep 改逐字 patch+逐檔檢查點

reviewer(task-decomposition,major):原 Task 3 把三種可獨立成立的 scope 揉進同一個
12-step/713 行/單一 commit,且 Step 10 對四個既有測試檔只給文字位置描述、無逐檔測試
檢查點。依同一份 plan 對 Task 4/5/6 建立的粒度基準拆解:

- Task 3a:既有 unified-mount 測試前置防護 sweep(aliasRedirect/dockLiveLink/a1DockLive
  三檔),每個 patch 給 old/new 逐字片段,每改完一檔立刻單檔 vitest 檢查點;不改斷言,
  sweep 前後全量同綠;獨立 commit `task#3a:`。
- Task 3b:真值投影純函式 runtimeTruth.ts+狀態文案 key+mock builders,不動 production
  元件;獨立 commit `task#3b:`。
- Task 3c:UnifiedShell 整檔重寫(tasks 1.7)+5.1 翻轉;plan 內附證據說明 5.1 為何必須與
  殼層同 commit(現況 EdgeConsole.sharedstatus.test.tsx:67 `expect(spy).not.toHaveBeenCalled()`
  會在殼層改輪詢當下變紅,拆開會讓該 commit 帶紅燈);獨立 commit `task#3c:`。

同時修正原 Step 10 兩個會壞事的指示:incomingHandoff.test.tsx 經 `rg -n "EdgeConsole"`
核實 0 命中(不掛殼層,poller 不會啟動),照原指示插 spy 會覆蓋該檔自身的 runtimeStatus
mock 而使四案退化,故改為「核實後不改」並保留反例警告;aliasRedirect 的 helper 必須插在
該檔既有 vi.spyOn 之前,否則 503 會蓋掉它刻意設定的空值 stub。

另同步:Global Constraints commit 前綴、檔案結構表、Task 4 對 Task 3c 的回指、Task 10
的 commit 清單改為十二個。需求未刪,tasks §1/§5 範圍不變。

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

* plan: 合併為 5 個 task(額度 32 次內);CoordinatorHttpError 區塊改與 slice 2 逐字一致;coordinator 裁決 #1–#8

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE

* task#0: coordinatorClient 加性擴充(CoordinatorHttpError、kitHealth/governanceIssues/governanceRuleRuns)

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

* task#0: 共用 poller store(單一 in-flight/退避 ≤60s/hidden 暫停)與 ConsoleDataProvider

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

* task#0: fix 補報 1A Step 1 的 HIGH 停點未履行並列 Blocker #9 待追認

plan 1A Step 1 規定「任一為 HIGH/CRITICAL → 停止並回報 coordinator」(本檔 line 147),
repo CLAUDE.md GitNexus 政策同樣要求 HIGH/CRITICAL 必須先警示、不得忽略。實跑
`gitnexus impact jsonGet -d upstream -r AI-BIM-governance` 為 risk=HIGH/impactedCount=16,
但當輪 implementer 未停止、未回報 coordinator,於 ignored 的 artifacts/slice1-impact.txt
自記續行理由後打勾,並完成 Step 2–6(157a4aa,CoordinatorHttpError 落地)。

本 commit 只補流程缺口,不改任何 production code:
- 1A Step 1 下方加「實跑偏離」註記:載明停點未履行、`[x]` 僅代表 impact 已實跑並抄錄、
  不代表停點已依規履行,並附 fixer 覆核證據。
- 「Blocker/裁決點」新增第 9 項(未裁):交 coordinator 事後追認或要求回退,
  並要求 PR body blast radius 段逐字揭露 jsonGet=HIGH(16) 與未裁狀態。

fixer 覆核(2026-08-25 本 session 實跑):impact 仍 HIGH/16,16 筆 caller 的 filePath
全為 web-viewer-sample/src/console/coordinatorClient.ts 同檔既有方法,皆以 await jsonGet<T>()
消費、未檢查 error.constructor/Object.getPrototypeOf,message 逐字不變;
coordinatorClient.test.ts(42)+conversions-history(2)+runtimeTruth(4)+coordinatorStatusStore(8)
共 56 passed,npx tsc --noEmit exit 0。技術面未觀察到退化,不等於流程停點已合規。

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

* task#0: fix 撤回 1A Step 1 勾選並補齊 HIGH 停點的回報與可 commit 證據

必修 gap:1A Step 1「任一為 HIGH/CRITICAL → 停止並回報 coordinator」(本檔 line 147)
未履行。前一輪 605327a 已補記事實並列 Blocker #9,但 (a) Step 1 仍掛 [x],
與「停點未履行」的事實矛盾;(b) 六筆 impact 數值只存在 ignored 的
artifacts/slice1-impact.txt(.gitignore:248 artifacts/*.txt),不隨 PR 留存,
coordinator 無可 commit 的正本可據以裁決。

本 commit 仍不改任何 production code,只補流程與紀錄:
- 1A Step 1:[x] → [ ],標 HELD,載明「impact 已實跑並抄錄」為真但停點未履行,
  待 coordinator 追認後方可勾回。
- 1A Step 1 註記新增「第二輪 fixer 覆核+停點回報」:六筆 impact 逐筆數值入檔,
  並區分停點的兩半 —— 回報半邊已補行、停止半邊無法回溯補行。
- Blocker #9 補本輪處置;本項維持「未裁」。

本 session 獨立實跑(非轉抄):
- npx gitnexus@1.6.9 impact -d upstream -r AI-BIM-governance 六筆 ——
  jsonGet = HIGH/16;coordinatorClient = LOW/0;UnifiedShell = LOW/2;
  HomePage = LOW/2;PipelinePage = LOW/2;OpsPage = LOW/2。
- cd web-viewer-sample; npx vitest run(coordinatorClient.test.ts 42+
  conversions-history 2+runtimeTruth 4+coordinatorStatusStore 8)= 56 passed。
- cd web-viewer-sample; npx tsc --noEmit = exit 0。

detect-changes 標 skipped:本 commit 純 docs/plan,無 code 改動;
已改跑 git diff --name-only --cached 自查 scope(僅該 plan 檔)+ git diff --cached --check 無輸出。

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

* plan: coordinator 追認 jsonGet HIGH 續行(Blocker #9 關閉);後續 task 的 HIGH 改為記錄後續行、CRITICAL 才停

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE

* task#1: 既有 unified-mount 測試補共用 poller 十端點 spy 與 store reset(不改斷言)

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

* task#1: 真值投影純函式 runtimeTruth(cell/pickers/healthOf/lastUpdatedText)+狀態文案 key+mock builders

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

* task#1: UnifiedShell 頂列 chips/GPU chip/側欄 badge 綁真值(移除字面 82%),注入共用 poller;5.1 同步翻轉

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

* task#2: #home 四 KPI+六 svc-dot 綁 coordinator 真值(asbuilt/data-state),移除 fixture 固定值

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

* task#2: fix 補勾 3A(Step 1–8)plan checkbox(前次 commit 漏帶)

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

* task#2: #pipeline 五段+治理/報表列綁真值;outbox 只用 redacted 摘要;RVT 退役標示;觸發轉檔 disabled 附原因

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

* task#2: fix 拆純函式模組修 eslint-baseline 迴歸;Home outbox KPI 導向改回 design §4 的 #minio

IMPORTANT #1(eslint-baseline required gate 紅燈)
- 修前 `check-eslint-baseline.mjs` 回 trusted=18 baseline=18 current=20 regressions=2(policy=shrink-only,exit 1)。
- 兩筆 react-refresh/only-export-components 迴歸的檔案在 origin/main 皆不存在(同屬本分支新檔),故由本 PR 整體負責:
  - ServiceHealthList.tsx:`ServiceRow`/`deriveServiceRows` 移入新檔 `serviceRows.ts`,.tsx 只留元件。
  - ConsoleDataProvider.tsx:context 與 `useConsoleData` 移入新檔 `consoleData.ts`,.tsx 只留 Provider 元件。
- 比照既有 `runtimeTruth.ts`/`coordinatorStatusStore.ts` 純模組慣例;三個 importer(HomePage/PipelinePage/UnifiedShell)改指向 `./consoleData`。
- 修後 regressions=0(exit 0)。未改 `scripts/eslint-baseline.json`:`--trusted-baseline` 比對會把新增 finding 判為迴歸,加白名單必然 fail-closed。

IMPORTANT #2(Home outbox KPI 導向偏離 canonical design)
- design.md §4 明定 #home 四 KPI 導向 `#conv`/`#sessions`/`#issues`/`#minio`;實作為 `#pipeline`,既無註記也無測試覆蓋。
- 依 spec 自述衝突規則「與 OpenSpec change 衝突時一律以 change 為準」,改 code 對齊 `#minio`,不擅自改動 OpenSpec design.md。
- 新增 `kpi-outbox` 導向斷言(與既有 `kpi-sess` 並列)鎖住行為;TDD 先見紅 `expected '#pipeline' to be '#minio'` 再實作。

驗證(本 session 於 worktree 實跑)
- npx tsc --noEmit → exit 0
- npx vitest run → 87 files / 1166 tests passed(較修前 +1=新增的 kpi-outbox 導向測試)
- npm run lint:baseline → trusted=18 baseline=18 current=18 regressions=0,exit 0
- npm run build:ui → built,exit 0
- gitnexus detect-changes --scope staged → 8 files/2 symbols(HomePage、PipelinePage)、risk low、affected processes 0
- git diff --cached --check → 無輸出

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE

* task#3: #runtime 真值 OpsPage(Kit instance/GPU 未取得/服務健康/事件誠實停用),固定 GPU 數值歸零

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

* task#3: 假資料 export 移至 test-only(D1=P),provider seeds 縮減,符號層守門測試;5.2 解凍

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

* task#4: design-system semantic cases 改為誠實狀態斷言(home/pipeline/ops 三屏+workspace badge),case id 不變

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

* task#4: Playwright E2E——/ui 預設入口真值 vertical slice(真後端 :8004;KPI↔API 對照、handoff anchor、offline→retry、loading、單一 in-flight)

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

* task#4: tasks.md 註記本機綠待 181;plan 勾選收官

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

* fix(unified): P3 final review——initialIssues 留在 production 供 Issues/BCF dock 種入(a3 semantic failure case 迴歸修復,ratchet 釘住);serviceRows 防禦讀取 runtimeStatus 子欄位;tasks.md 1.1/1.7 措辭誠實化

P3 final review(wf_ab83125c-ad1)f1:workspace.a3.default failure case(issues-open-red-chip/issues-firerating-issue-title)在 af88ecd 把 initialIssues 移到 test-only 後空掉,
不是 pre-existing——改回 production 種入(docks.tsx 屬 §2 範圍不動),SLICE2_DEBT 加 UnifiedShell.tsx:[initialIssues],prototypeFixtures 改 re-export。
f4:serviceRows.ts 對 configured_endpoints/service 用 ?. 防禦(缺欄位標 —/unknown,不崩潰)。f2/f3:tasks.md 1.1 揭露 jsonGet HIGH(追認)、1.7 的 82% 說法限定 production 檔。
驗證:npx tsc --noEmit 0;npm run lint:baseline regressions=0;npx vitest run 89 files/1175 tests 綠。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE

* test(design): rebaseline home/pipeline/runtime.ops 為產品面誠實 offline golden(owner 明示 2026-08-25,D1=P;canonical_product_surface 比照 PR #429)

雙旗標 capture 腳本只重拍設計原型(authoring origin,未變),無法表達產品在 /api 503 stub 下的 offline/empty 狀態;
改以 design-system-visual.spec.ts 於 d9b40de 的 actual 截圖為 golden,三屏 baseline_provenance.authority=canonical_product_surface,
approval 記 owner 2026-08-25 明示;workspace.a4.default 與其餘 9 屏 golden 未動;manifest captured_at_utc 為同日 origin 重拍時間戳。
verify-design-system-reference.ps1 -VerifyOrigin:passed — 13 screens, 26 golden files。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE

* docs(evidence): slice 1 P4 browser evidence(Playwright 7 passed on isolated HEAD stack :8006;summary+home/pipeline/runtime/offline 抽樣截圖;not-observed 項如實列於 summary)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE

* docs(evidence): slice 1 P4 證據更新至 6cd0074(s1-r2;Playwright 7 passed on isolated HEAD stack :8006 with API-seeded review session;summary 如實列 not-observed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE

* fix(unified): P5 round-1 修補——#home 啟動器容器 data-prov fixture→demo(canonical 七值)、E2E 檔頭誠實敘述 require-real 不可用、tasks.md 1.4/1.8/5.4/5.5/5.6 與 ratchet 標題同步(6 個 export、a3 迴歸為本 slice 造成並已修、rebaseline 落地實況與 PR 切分揭露);golden/manifest 移出產品 PR

change-scope 政策(reference_authority_mixed_fail_closed)禁止產品與 golden 同 PR:三屏 golden 與 manifest 還原為 origin/main 內容,
升格改走獨立 reference PR(先降級為 reference_missing → 本 PR → 以合併後產品截圖重新核准)。
P5 round 1(wf_fad3e444-2b6):f6/g2/e1 refuted;f1/g1/e4/c1/c2/c3 fix_now 已修;f7/e5/e7/e8/c4–c9 follow_up/known_gap 進 PR body。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE

* docs(tasks): 5.5/5.6 記錄 owner 核准的三段 golden 路線(#697 關、#698 降級已合併、重新核准待做)

- 5.5 落地實況:reference-first 換 golden(#697)在 CI 以產品 offline golden 比對 main
  fixture UI 而紅(manifest/baseline 屬 full_dispatch_globs);改走三段:(1) #698 降級三屏為
  reference_missing(10 屏/20 golden,-VerifyOrigin 於 main passed — 10 screens)→(2) 本產品 PR
  (scope mixed、10 屏比對;#home/#pipeline/#runtime 此刻無 pixel golden)→(3) 重新核准 PR
  (合併後產品面升格 canonical_product_surface,待做)。標明「13 screens/13/13」數字是 #698 之前
  在 4e73c10 的本機驗證,不是 main 現況。
- 5.6 本機列同樣標明時間點(4e73c10,#698 之前)與 #698 之後的 10 屏比對結果改看 PR body。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE

* docs(evidence): slice 1 P4 attempt 2 at 88608cb(s1-r2;#698 之後、fix_now 修補之後重取)

Playwright e2e/unified-console-runtime-truth.spec.ts 7 passed ×2(run A 空 stack→handoff-none;
run B 以真 API 種入 review_session 走 handoff-link),隔離 HEAD stack:host-native coordinator :8006
+HEAD dist-ui(bundle index-BPjskEUF.js),非 canonical :8004(docker 仍 pre-slice bundle)亦非 181。
summary.json supersedes 6cd0074 版;home/pipeline 截圖為 HEAD 版(offline/runtime 位元相同)。
notObserved 15 項如實列出(181/:8004 部署、conversion 列、governance ids、GPU session、
back-off 上限、document.hidden、MinIO watch…);executor 另指出 gaps e1–e9(含 e5 殼層靜態字串
data-prov 標示、e9 loading 與 offline 不可區分)交 P5 round 2 裁決。UI task 仍待 181 驗收。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE

* docs(slice1): P5 round-2 措辭修正——HomePage 檔頭註解與 tasks 5.4 過期句/未 tracked 證據引用

- HomePage.tsx:6 註解仍寫「維持 data-prov=\"fixture\"」,與 :98(P5 c1 修為 data-prov=\"demo\")矛盾;
  改為如實描述(容器 demo、badge 文字尚未改綁真值)。註解僅開發者可見,不改變 bundle/渲染輸出
  (P5 round 2 verifier 判定;本次 commit 在 P4 attempt 2 證據之後,state 驗證器將視為 evidence_stale,
  已於 PR body 揭露)。
- tasks.md 5.4:h1——「見 artifacts/slice1-gates.txt」引用的是 .gitignore 排除、永不入 range 的本機輸出,
  改為明示不可查證、承重驗證為 CI required check design-semantic-visual;h2——「pixel 三屏預期紅(待 5.5
  owner rebaseline)」補上時間點(57d29d3、#698 之前)與現況(三屏 reference_missing,見 5.5)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE

* test(unified): use a non-listener placeholder for conversion_authority.base_url in the coordinator mock

scripts/lib/production-boundary-contract.ps1 rejects any added
http(s)://…:49xxx literal in browser source (the __testdata__ directory is not
one of its test-path exclusions), and the new __testdata__/coordinatorMocks.ts
carried the real host-native value http://127.0.0.1:49101. The mock now uses
http://conversion-authority.test; serviceRows only echoes the string into the
"無探測端點 · 未取得" detail, and no unified test asserts the literal
(vitest src/console/unified: 10 files / 52 tests pass).

Test-only; rendered production output and the P4 attempt-2 evidence are
unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE

* test(e2e): rebaseline hifi-token-authority unified-home.png to the runtime-truth #home

e2e/hifi-token-authority.spec.ts pins a tracked pixel baseline for /ui#home
(artifacts/e2e/hifi-token-authority/unified-home.png, maxDiffPixelRatio 0.01).
Slice 1 rebinds #home to coordinator runtime truth, so the fixture-era
baseline now differs by 2.73% (CI functional-runtime-conv on 769b682 /
d973a79: KPI cells render live/offline truth — 轉檔中 1, sessions 0, 未結
ISSUE "—/未連線" because the harness runs no governance-service, outbox 0;
service-health rows show the harness stub endpoints).

New baseline = the CI actual from run 32917797981 (windows-2025, artifact
functional-runtime-conv-769b6824…-attempt-1, _output/hifi-token-authority/
unified-home-actual.png), reviewed by the coordinator as the expected honest
state. The spec's token-authority assertions (AB token effective, theme keys
not written) are unchanged; only the content baseline moves. Path is
artifacts/e2e/** (non-product for the design-system gate; not part of
docs/plans reference authority).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134ZvBvDDUpHSjzjvDZXEBE

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants