Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c04b5a7
plan: MinIO file-server 來源落地實作 plan(5 task:file_library API + proxy +…
monkey1sai Jun 10, 2026
c2a774c
plan: fix Task 5 E2E 假綠 / strict-mode / build:ui 前置三項 reviewer blocker
monkey1sai Jun 10, 2026
1f6ac9b
task#0: feat(governance): file_library 唯讀 tree API(storage 兩層 IFC 結構)
monkey1sai Jun 10, 2026
871ff4c
task#0: fix 讓 file_library path-traversal 測試在 Windows 也真跑(symlink→jun…
monkey1sai Jun 10, 2026
7e5b170
task#0: fix 預設 file_library root 指 repo storage(修 spec §4.1 違反)
monkey1sai Jun 10, 2026
5772c16
task#1: feat(coordinator): governance proxy 加 files/tree 白名單一條
monkey1sai Jun 10, 2026
68a2ea4
task#2: feat(viewer): governanceClient.filesTree() + FilesTreeRespons…
monkey1sai Jun 10, 2026
14e83fb
task#3: feat(viewer): #/minio 接真檔案庫樹 + #/a1 三層選擇器(手動輸入保留)
monkey1sai Jun 10, 2026
2fa563c
task#3: fix 補 MinioData 真樹/error 態與 A1 ifcPath data-binding client-re…
xshiujj-glitch Jun 10, 2026
085a08d
task#4: test(e2e): MinIO file-server source 端到端(#/minio 真樹 + #/a1 選擇器…
monkey1sai Jun 10, 2026
e46c77c
task#4: fix E2E 守門加 dist-ui branch 標記 + 明文標 build:ui 前置(修可重現性風險)
monkey1sai Jun 10, 2026
874c680
task#4: fix E2E 硬 gate 適用範圍:明文記錄 conditional skip 的零保護限制(skip=pass)
monkey1sai Jun 10, 2026
85c936a
fix: 收斂 file_library 保留目錄污染 + 補 empty/fsErr vitest 缺口 + 同步 evidence
monkey1sai Jun 10, 2026
43c0507
docs: openspec change + spec 入庫(minio-fileserver-source 追溯鏈)
monkey1sai Jun 10, 2026
25b3218
fix: PR #204 reviewer 修復輪——openspec delta + runtime root fallback + O…
monkey1sai Jun 10, 2026
e01ac6b
fix: A1 version 清回 placeholder 也清 selector 填入的 ifcPath(Codex round-2 P2)
monkey1sai Jun 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bim-review-coordinator/src/routes/governanceProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ function queryString(originalUrl: string, fallback = ""): string {
}

export function registerGovernanceProxy(app: Express, deps: GovernanceProxyDeps = {}): void {
// A1 file-library browse proxy(唯讀 local file-server tree,透傳 governance-service /api/files/tree)。
// 瀏覽器只打 :8004;樹 JSON 原樣透傳,coordinator 不解讀 / 不保存。
app.get("/api/governance/files/tree", (_request, response) => {
void forward(response, "GET", "/api/files/tree");
});

app.post("/api/governance/rule-runs", (request, response) => {
void forward(response, "POST", "/api/rule-runs", request.body);
});
Expand Down
106 changes: 106 additions & 0 deletions bim-review-coordinator/tests/governance-files-tree.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import fs from "node:fs";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import { type AddressInfo } from "node:net";
import request from "supertest";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createCoordinatorApp, type CoordinatorApp } from "../src/app.js";
import type { CoordinatorConfig } from "../src/config.js";

// GET /api/governance/files/tree:coordinator 逐 endpoint 白名單透傳 governance-service
// GET /api/files/tree(瀏覽器只打 :8004,不直連 governance loopback)。誠實:governance
// 不可達回 502(forward helper 既有行為),不偽造資料。

let active: CoordinatorApp | null = null;
let governanceStub: http.Server | null = null;
let savedGovBase: string | undefined;

beforeEach(() => {
savedGovBase = process.env.GOVERNANCE_API_BASE;
});

afterEach(async () => {
if (active) {
active.io.close();
await new Promise<void>((resolve) => active?.server.close(() => resolve()));
active = null;
}
if (governanceStub) {
await new Promise<void>((resolve) => governanceStub?.close(() => resolve()));
governanceStub = null;
}
if (savedGovBase === undefined) {
delete process.env.GOVERNANCE_API_BASE;
} else {
process.env.GOVERNANCE_API_BASE = savedGovBase;
}
});

function makeApp(overrides: Partial<CoordinatorConfig> = {}): CoordinatorApp {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "bim-review-coordinator-files-tree-test-"));
active = createCoordinatorApp({
sessionStoreDir: path.join(root, "sessions"),
eventLogDir: path.join(root, "events"),
callbackOutboxStorePath: path.join(root, "callback-outbox.json"),
storageRoot: path.join(root, "storage"),
storageHostRoot: path.join(root, "storage"),
corsOrigins: ["http://127.0.0.1:5173"],
conversionPollEnabled: false,
...overrides,
});
return active;
}

async function startGovernanceTreeStub(): Promise<{ baseUrl: string; urls: string[] }> {
const urls: string[] = [];
const tree = {
root: "C:/Repos/active/iot/AI-BIM-governance/storage",
source_kind: "local_fs",
projects: [
{
project_id: "270",
models: [
{ model_id: "機電", versions: [{ name: "ver 竣工.ifc", path: "C:/x/ver 竣工.ifc", size_bytes: 22618, mtime: "2026-06-10T17:17:00+08:00" }] },
],
},
],
};
governanceStub = http.createServer((req, res) => {
if (req.method === "GET" && req.url === "/api/files/tree") {
urls.push(req.url);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(tree));
return;
}
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ detail: "not found" }));
});
await new Promise<void>((resolve) => governanceStub?.listen(0, "127.0.0.1", () => resolve()));
const address = governanceStub.address() as AddressInfo;
return { baseUrl: `http://127.0.0.1:${address.port}`, urls };
}

describe("GET /api/governance/files/tree", () => {
it("透傳 governance-service /api/files/tree 並回原樣樹 JSON", async () => {
const gov = await startGovernanceTreeStub();
process.env.GOVERNANCE_API_BASE = gov.baseUrl;
const app = makeApp();

const res = await request(app.app).get("/api/governance/files/tree");

expect(res.status).toBe(200);
expect(res.body.source_kind).toBe("local_fs");
expect(res.body.projects[0].project_id).toBe("270");
expect(res.body.projects[0].models[0].versions[0].name).toBe("ver 竣工.ifc");
expect(gov.urls).toEqual(["/api/files/tree"]);
});

it("governance-service 不可達 → 502(誠實,不偽造)", async () => {
process.env.GOVERNANCE_API_BASE = "http://127.0.0.1:1";
const app = makeApp();
const res = await request(app.app).get("/api/governance/files/tree");
expect(res.status).toBe(502);
expect(typeof res.body.detail).toBe("string");
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
76 changes: 76 additions & 0 deletions docs/evidence/minio-fileserver-source/summary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"feature": "minio-fileserver-source",
"spec": "docs/superpowers/specs/2026-06-10-minio-fileserver-source-design.md",
"plan": "docs/superpowers/plans/2026-06-10-minio-fileserver-source.md",
"branch": "minio-fileserver-source (worktree)",
"date": "2026-06-10",
"engine": "playwright",
"vertical_slice_ok": true,
"e2e_spec": "web-viewer-sample/e2e/minio-fileserver-source.spec.ts",
"e2e_result": {
"command": "E2E_COORDINATOR_BASE_URL=http://127.0.0.1:8014 npx playwright test e2e/minio-fileserver-source.spec.ts --reporter=list",
"exit_code": 0,
"passed": 2,
"failed": 0,
"skipped": 0,
"tests": [
{ "name": "#/minio 真樹可見 270/889/990 三專案與版本檔", "status": "passed", "duration_ms": 870 },
{ "name": "#/a1 選擇器選 270/機電/ver 竣工.ifc → rule-run → 檢核結果出現", "status": "passed", "duration_ms": 2100 }
]
},
"runtime_topology": {
"note": "E2E page.goto targets the COORDINATOR /ui dist-ui (NOT the playwright :5180 vite webServer). Branch services were started fresh on alternate ports because the stale docker coordinator holds :8004 with an old bundle and no files/tree proxy.",
"coordinator": "http://127.0.0.1:8014 (branch, tsx src/index.ts, CONSOLE_DIST_DIR=freshly-built dist-ui, GOVERNANCE_API_BASE=http://127.0.0.1:49102)",
"governance_service": "http://127.0.0.1:49102 (branch app.py, BIM_FILE_LIBRARY_ROOT=C:/Repos/active/iot/AI-BIM-governance/storage)",
"dist_ui_built": "web-viewer-sample/dist-ui (npm run build:ui, built in 2.33s)",
"stale_8004_note": "Pre-existing docker coordinator on :8004 returns 404 for /api/governance/files/tree and serves an old /ui bundle (index-B98B2yRg.js); it does NOT contain this branch's code. Not used for evidence."
},
"routes_exercised": ["/ui/#/minio", "/ui/#/a1"],
"buttons_clicked": [
"三層選擇器 a1-fs-project (selectOption 270)",
"三層選擇器 a1-fs-model (selectOption 機電)",
"三層選擇器 a1-fs-version (selectOption label=ver 竣工.ifc)",
"執行規則檢核 / POST /api/governance/rule-runs"
],
"default_fixture": {
"selected_via_ui": true,
"selected_path": "C:/Repos/active/iot/AI-BIM-governance/storage/270/機電/ver 竣工.ifc",
"size_bytes": 22618,
"note": "Selected from the file-library tree selector (no manual curl). Manual input box retained for backward compat (still prefilled with storage/fixture-bytes.ifc)."
},
"real_backend_evidence": {
"files_tree_api": "GET /api/governance/files/tree -> 200; source_kind=local_fs; root=C:\\Repos\\active\\iot\\AI-BIM-governance\\storage; project_ids=[270,889,990]; 270 models=[機電,水電,消防]; 270/機電 versions=[ver 000001.ifc, ver 000002.ifc, ver 000003.ifc, ver 竣工.ifc]; ver 竣工.ifc sorted LAST with size 22618 and absolute path.",
"reserved_dir_exclusion_note": "Original P4 E2E capture (against pre-fix code) listed project_ids=[270,889,990,ifc-cache] because storage/ifc-cache/ifcready_*/source.ifc (coordinator IFC download cache, ~60 dirs) matched the two-level {dir}/{dir}/*.ifc rule and polluted the #/minio tree + A1 project dropdown. Adversarial finding [c3] fix adds a reserved-dir exclusion set ({ifc-cache, coordinator}) in file_library/api.py. Re-probed post-fix against the same real storage (governance-service TestClient, BIM_FILE_LIBRARY_ROOT=C:/Repos/active/iot/AI-BIM-governance/storage): project_ids=[270,889,990] (ifc-cache/coordinator no longer appear), 270/889/990 unchanged. Covered by governance-service pytest test_reserved_dirs_excluded_from_projects.",
"mock": false,
"mock_markers_present": false,
"honesty_markers_in_ui": [
"local file-server 來源(比照 bim-control/{projectId}/{modelId} 規約);真 S3/MinIO 待接",
"source_kind=local_fs panel subtitle",
"Bucket layout panel still prov=demo (規約示意)",
"model.usdc still prov=p1 (轉檔產物後端待建,未因本 spec 翻綠)"
]
},
"rule_run_real_execution": {
"in_browser_rule_run_id": "rr_a76e2680c771",
"in_browser_status": "succeeded",
"in_browser_scoreboard": { "total": 14, "passed": 14, "failed": 0, "score": 100 },
"api_verified_rule_run_id": "rr_6c678a264a28",
"api_verified_status": "succeeded",
"api_verified_summary": { "rule_set": "default-governance", "version": "1.0", "total": 14, "passed": 14, "failed": 0, "errored": 0, "score": 100.0, "target_summary": { "DOOR-FIRERATING-REQUIRED": 0, "ELEMENT-NAME-REQUIRED": 14, "WALL-STOREY-ASSIGNED": 0 } },
"note": "Real ifcopenshell CPU rule-run against the genuinely UI-selected IFC (storage/270/機電/ver 竣工.ifc). Async 202->queued->succeeded. Two distinct run ids: one fired by the browser test, one fired by direct proxy POST for runtime-id capture — both succeeded with identical real summary."
},
"states_observed": ["loading (GET /api/governance/files/tree)", "success (tree rendered + scoreboard)", "succeeded (rule_run_status)"],
"states_not_directly_exercised_in_e2e": ["MinioData empty file-library state (projects=[]) — covered by vitest client-render case 'MinioData filesTree() 回空 projects → empty 態顯示「檔案庫為空」' (console.test.tsx), not re-driven in this browser run because real storage has 270/889/990", "MinioData error state — covered by vitest client-render case 'MinioData filesTree() reject → error 態誠實標「未連線後端」'", "A1 file-library selector fsErr graceful-degradation — covered by vitest client-render case 'A1 filesTree() reject → 選擇器標「檔案庫不可用」graceful degrade,手動輸入照常可用'", "rule-run failure/retry (run succeeded; failure path covered by coordinator 502 vitest + governanceClient reject test)"],
"states_coverage_note": "Adversarial finding [c4] fix added two vitest client-render cases (MinioData empty + A1 fsErr graceful-degradation) that the earlier renderToString SSR smoke could not reach (SSR first frame is always the loading branch; empty needs !loading&&!err&&projectCount===0, fsErr needs the catch→setFsErr microtask). The previous over-claim that empty/fsErr were 'covered by vitest console.test smoke' is replaced by these explicit createRoot+act cases. console.test.tsx: 32 tests passed.",
"gpu_3d": "N/A — this feature is CPU-only governance (ifcopenshell). No 3D viewer / stage truth / GPU review session is in scope for #/minio or #/a1 rule-run. The #/minio page references 3D Viewer relationship only as asbuilt metadata; model.usdc remains p1.",
"screenshots": [
"artifacts/e2e/minio-fileserver-source-minio-tree.png",
"artifacts/e2e/minio-fileserver-source-a1-rulerun.png"
],
"traces": [
"artifacts/e2e/minio-fileserver-source-trace/minio-tree-chromium/trace.zip",
"artifacts/e2e/minio-fileserver-source-trace/a1-rulerun-chromium/trace.zip"
],
"traces_note": "Playwright trace.zip live under the MAIN worktree at artifacts/e2e/minio-fileserver-source-trace/{minio-tree,a1-rulerun}-chromium/trace.zip (verified present: 794938 and 840456 bytes; playwright-feature-run.log alongside shows 2 passed). These full traces are local-only (large artifacts not committed per repo 'evidence 只存抽樣' convention); the durable sampled evidence committed under this docs/evidence/ dir is minio-tree.png + a1-rulerun.png + this summary.json. Adversarial finding [c1] flagged the path as artifacts/e2e/_output/...Mi-08e35-*/trace.zip — that _output location does not exist on disk; the canonical output is the minio-fileserver-source-trace/ path recorded here (configured by the spec's page.screenshot/trace outputDir), confirmed by directory listing.",
"full_suite_note": "Running `npx playwright test minio-fileserver-source` (substring) ran the whole e2e suite (18 tests): both minio-fileserver-source tests PASSED; 3 unrelated failures (kit-proxy needs Conversion API :8010, product-console-integration has a pre-existing strict-mode bug in its own spec heading locator, real-ifc-storage-intake needs streaming server) are NOT this feature and NOT regressions introduced here. Isolated `e2e/minio-fileserver-source.spec.ts` run is exit 0, 2 passed."
}
Loading
Loading