Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ artifacts/spec-to-done-design/
# AI coding raw telemetry and generated package aggregates are local/hosted artifacts, never source.
artifacts/telemetry/ai-coding/
artifacts/metrics/ai-coding/
# Observed architecture reports are regenerated from source on demand. The approved
# ratchet baseline (architecture/observed-baseline.json) is the tracked authority.
artifacts/architecture/

# ----------------------------
# Session workflow scratch (.workflow/ultracode 執行痕跡,可重生,不入庫)
Expand Down
52 changes: 43 additions & 9 deletions architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ architecture/
├── architecture-contract.json
├── architecture-contract.schema.json
├── architecture-delta.schema.json
├── observed-graph.config.json
├── observed-graph.config.schema.json
├── observed-baseline.json
├── observed-baseline.schema.json
├── deltas/
│ └── <change-id>.json
└── README.md
Expand All @@ -37,6 +41,8 @@ architecture/
- `architecture-contract.schema.json`:desired architecture 的結構 schema。
- `architecture-delta.schema.json`:每次變更的 machine-readable delta schema。
- `deltas/<change-id>.json`:新增/刪除 dependency edge、public contract、ownership、state machine 與 exception 的聲明。
- `observed-graph.config.json`:observed 靜態掃描設定(目錄→service 對映、掃描排除、可視為 inbound target 的 port、誤報抑制樣式)。
- `observed-baseline.json`:**已核准的 observed baseline**(grandfathered edge 與 cycle)。ratchet 只放行這裡有的東西,其餘 fail closed。

## 3. 第一版硬規則

Expand All @@ -46,7 +52,7 @@ architecture/
| `ARCH-HTTP-001` | Browser HTTP API 只進 `bim-review-coordinator:8004` | semantic validator |
| `ARCH-SVC-001` | 每個 capability 只有一個 owner,owner 與 `must_not` 不得衝突 | semantic validator |
| `ARCH-CALL-001` | 新 service edge 必須同時存在於 desired contract 與 change delta | semantic validator |
| `ARCH-GRAPH-001` | 不得新增 dependency cycle | Phase 2 observed-graph ratchet |
| `ARCH-GRAPH-001` | 不得新增 dependency cycle | observed-graph ratchet(已 active) |
| `ARCH-READY-001` | `ready` 必須同時有 Kit-side 與 browser-side evidence,包括 first frame 與 stage match | semantic validator + existing runtime evidence |
| `ARCH-UI-001` | user-facing capability 必須前端可操作並有 browser E2E evidence | delegated to existing frontend operability gates |
| `ARCH-DELTA-001` | Lane G / S 架構變更必須提交 architecture delta | delta schema + semantic validator |
Expand Down Expand Up @@ -92,8 +98,15 @@ Targeted validation:
```powershell
python scripts/dev/validate_architecture_contract.py --repo-root . --strict
python -m pytest tests/test_architecture_contract.py -q -p no:cacheprovider

# observed ratchet(Phase 2)
python scripts/dev/export_observed_architecture.py --repo-root . --strict
python scripts/dev/export_observed_architecture.py --repo-root . --report-only --output artifacts/architecture/observed-dependencies.json
python -m pytest tests/test_observed_architecture.py -q -p no:cacheprovider
```

`--report-only` 產出的 report 是 **可重生的本機產物**(`artifacts/architecture/` 已 gitignore),同一份 source tree 在 Windows 與 Linux 會得到 byte-identical 輸出。入庫的權威是 `observed-baseline.json`。

Canonical dispatch:

```powershell
Expand All @@ -110,13 +123,29 @@ openspec validate --all --strict

## 6. Ratchet 原則

第一版不要求一次清掉所有歷史結構問題。後續 observed graph gate 採:
第一版不要求一次清掉所有歷史結構問題。observed graph gate 採:

```text
existing violations <= recorded baseline
new violations == 0
```

具體規則(`scripts/lib/observed_architecture.py`):

| 情況 | 結果 |
|---|---|
| observed edge 已在 `observed-baseline.json` | pass(grandfathered) |
| **新** edge 不在 contract `may_call` | `observed.edge.not_allowed`(error) |
| **新** edge 在 contract 但沒有任何 delta 宣告 | `observed.edge.undeclared`(error) |
| **新** edge 同時被 contract 與 delta 宣告 | pass |
| 新 cycle signature 或 cycle 數超出 `cycle_budgets` | `observed.cycle.new` / `observed.cycle.count_increase`(error) |
| baseline 有但已不再 observed | `*.baseline_stale`(**warning**,提示收緊 baseline) |
| config/baseline/contract/schema 不是 JSON object、scan root 不存在、非 browser client 卻沒有 `inbound_edge_ports`、baseline 把不被 contract 允許的 edge 標成 `declared`、baseline status 非法或 `(from,to)` 重複、來源檔讀不到或解析失敗 | error(fail closed;「沒比對到」不得等於 pass) |

Baseline 身分只比對 `(from, to)` 與 cycle 成員集合,**不含 file:line**,所以行號漂移不會弄破 ratchet;evidence 只供解釋。cycle 同時比對 **signature 與數量**,因此「刪一個環又加一個環、總數不變」仍會 fail。

warning 本身不是 error,但 canonical 測試 `tests/test_observed_architecture.py::test_canonical_repository_observed_ratchet_passes` 對真 repo 斷言 `warning_count == 0`,所以 baseline 一旦 stale,**CI 仍會紅**,用意是逼你把 baseline 收緊而不是放著爛。CLI 的 `--strict` 行為與此一致。

每次 `$improve-codebase-architecture` 或 architecture review 發現 recurring issue 時,處理順序是:

```text
Expand All @@ -130,11 +159,16 @@ finding

## 7. 尚未宣稱完成的能力

以下仍是後續 phase,不應被第一版文件或 PR 誤報為已完成:
以下仍是後續 phase,不應被目前文件或 PR 誤報為已完成:

- TypeScript `dependency-cruiser` rules(Phase 3)。
- Python Import Linter contracts(Phase 3)。
- `review-session`、`endpoint-lease`、`stage-binding` executable state machines(Phase 4)。
- architecture quality grade 與定期 architecture garbage collection(Phase 5)。

### Phase 2 的已知偏離與界線(誠實揭露)

- TypeScript `dependency-cruiser` rules。
- Python Import Linter contracts。
- GitNexus observed graph 匯出與 desired-vs-observed diff。
- `review-session`、`endpoint-lease`、`stage-binding` executable state machines。
- PR 自動偵測「實際新增 edge 但 delta 未聲明」。
- architecture quality grade 與定期 architecture garbage collection。
- **不用 GitNexus 當 gate 輸入。** change tasks 原文寫「Export … from GitNexus」。GitNexus CLI 在本 repo 已多次觀察到 transport 失敗與 stale index(見 `docs/plans/NOW.md` S4-B closeout),無法支撐 fail-closed 的 CI gate。因此 observed graph 改由**純標準函式庫的靜態掃描**產生,GitNexus 降為 advisory(`observed-graph.config.json` 的 `advisory_only_sources` 已記錄此決定)。任務語意(deterministic observed report)維持,實作來源不同。
- **靜態掃描的偵測面有限。** 目前 service-level edge 只從兩種低誤報訊號取得:帶 scheme 的 URL literal(`http/https/ws/wss://host:port`)與 compose 的 `depends_on` / env URL。**執行期才決定的位址(例如 viewer 由 coordinator 動態取得 streaming 端點)不會出現在 observed graph**,所以 `web-viewer-sample → bim-streaming-server` 雖然被 contract 允許卻不在 baseline。這是「偵測不到」,不是「不存在」;ratchet 只保證**新增**的可靜態偵測 edge 會被擋,不宣稱已窮舉所有實際呼叫。
- **module-level graph 只在 service 內部比對**,不做跨 service module graph,也不做 repo-wide import 掃描。
- **`apps/kit-manager-web` 尚未在 contract 中宣告為 service node。** 它確實會呼叫 coordinator `:8004`,已以 `undeclared-node` debt 記入 baseline,須由後續 delta 正式宣告,不得用 re-baseline 帶過。
2 changes: 1 addition & 1 deletion architecture/architecture-contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@
"statement": "No new module or service dependency cycles may be introduced.",
"severity": "error",
"enforcement": {
"status": "planned",
"status": "active",
"mode": "observed-graph-ratchet",
"rule": "no-new-dependency-cycles"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
"schema_version": "ai-bim-architecture-delta/v1",
"change_id": "introduce-executable-architecture-contracts",
"lane": "G",
"summary": "Introduce the desired architecture contract, semantic validator, architecture delta contract, and canonical verification dispatch without changing product runtime behavior.",
"summary": "Introduce the desired architecture contract, semantic validator, architecture delta contract, canonical verification dispatch, and the observed-architecture ratchet, without changing product runtime behavior.",
"created_on": "2026-07-30",
"affected_services": [],
"affected_surfaces": [
"architecture-governance",
"root-contract-verification",
"openspec-change-control"
"openspec-change-control",
"observed-architecture-ratchet"
],
"added_dependency_edges": [],
"removed_dependency_edges": [],
Expand All @@ -18,6 +19,11 @@
"contract": "repository architecture governance",
"change_type": "additive",
"description": "Adds machine-readable desired architecture and change-delta contracts; no product API or event contract changes."
},
{
"contract": "observed architecture ratchet",
"change_type": "additive",
"description": "Adds a deterministic static observed dependency scan and an approved baseline. New statically detectable service edges must be declared by both the contract and a delta, and cycle identity and count must not grow. No product API or event contract changes."
}
],
"data_ownership_changes": [],
Expand Down
101 changes: 101 additions & 0 deletions architecture/observed-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
{
"$schema": "./observed-baseline.schema.json",
"schema_version": "ai-bim-observed-baseline/v1",
"approved_on": "2026-07-30",
"approved_by": "introduce-executable-architecture-contracts Phase 2",
"method": "Generated by scripts/dev/export_observed_architecture.py from the committed source tree at the time of approval. Entries recorded here are grandfathered: the ratchet permits them and fails closed on anything new. Removing an entry is always allowed and is the intended direction of travel.",

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Correct the baseline-removal claim.

An observed edge is permitted only when it remains in the baseline or is declared by both the architecture contract and a delta. Removing a baseline entry while the source edge remains undeclared will fail the ratchet, so removal is not “always allowed.”

Suggested wording
-  "method": "Generated by scripts/dev/export_observed_architecture.py from the committed source tree at the time of approval. Entries recorded here are grandfathered: the ratchet permits them and fails closed on anything new. Removing an entry is always allowed and is the intended direction of travel.",
+  "method": "Generated by scripts/dev/export_observed_architecture.py from the committed source tree at the time of approval. Entries recorded here are grandfathered: the ratchet permits them and fails closed on anything new. Removing an entry is allowed when the edge is removed or declared by both the contract and a delta.",
📝 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
"method": "Generated by scripts/dev/export_observed_architecture.py from the committed source tree at the time of approval. Entries recorded here are grandfathered: the ratchet permits them and fails closed on anything new. Removing an entry is always allowed and is the intended direction of travel.",
"method": "Generated by scripts/dev/export_observed_architecture.py from the committed source tree at the time of approval. Entries recorded here are grandfathered: the ratchet permits them and fails closed on anything new. Removing an entry is allowed when the edge is removed or declared by both the contract and a delta.",
🤖 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 `@architecture/observed-baseline.json` at line 6, Update the descriptive
metadata near the baseline-generation statement to remove the claim that
deleting an entry is always allowed, and accurately state that an observed edge
is permitted only when retained in the baseline or declared by both the
architecture contract and a delta. Preserve the existing grandfathering and
fail-closed behavior.

"service_edges": [
{
"from": "bim-review-coordinator",
"to": "bim-streaming-server",
"status": "declared",
"note": "Permitted by architecture-contract.json may_call (internal-conversion-request, internal-runtime-status, audited-stage-control-intent)."
},
{
"from": "bim-review-coordinator",
"to": "governance-service",
"status": "declared",
"note": "Permitted by architecture-contract.json may_call (coordinator-governance-proxy)."
},
{
"from": "bim-review-coordinator",
"to": "kit-manager-api",
"status": "declared",
"note": "Permitted by architecture-contract.json may_call (audited-runtime-lifecycle-intent)."
},
{
"from": "kit-manager-api",
"to": "bim-streaming-server",
"status": "declared",
"note": "Permitted by architecture-contract.json may_call (kit-fleet-operations, runtime-telemetry)."
},
{
"from": "kit-manager-web",
"to": "bim-review-coordinator",
"status": "undeclared-node",
"debt": {
"owner": "introduce-executable-architecture-contracts",
"reason": "apps/kit-manager-web is a second browser client that reaches the coordinator public API, but architecture-contract.json declares only web-viewer-sample as a browser node. The edge itself respects ARCH-HTTP-001 because it targets coordinator:8004; the gap is the missing service declaration, not a boundary violation. Declaring the node changes desired architecture and therefore needs its own delta rather than being smuggled in with the ratchet.",
"target_phase": "phase-3"
}
},
{
"from": "kit-manager-web",
"to": "kit-manager-api",
"status": "undeclared-node",
"debt": {
"owner": "introduce-executable-architecture-contracts",
"reason": "compose.runtime-manager.yml starts kit-manager-web after kit-manager-api. This is a startup ordering dependency for an operator console that is not yet a declared contract node. Recorded as debt alongside the kit-manager-web declaration gap; a browser client reaching an internal service directly would violate ARCH-HTTP-001 and must be resolved, not merely re-baselined, when the node is declared.",
"target_phase": "phase-3"
}
},
{
"from": "web-viewer-sample",
"to": "bim-review-coordinator",
"status": "declared",
"note": "Permitted by architecture-contract.json may_call (public-rest, socket-io) and by browser_access_policy http_api_entrypoints."
}
],
"cycles": [
{
"scope": "module-graph:bim-streaming-server",
"members": [
"ezplus.bim_review_stream.messaging",
"ezplus.bim_review_stream.messaging.extension",
"ezplus.bim_review_stream.messaging.kit_struct_log"
],
"debt": {
"owner": "bim-streaming-server",
"reason": "The Kit extension package __init__ re-exports extension and kit_struct_log, which import back from the package. This is the conventional Omniverse extension entry-point shape; breaking it requires reworking the extension surface.",
"target_phase": "phase-3"
}
},
{
"scope": "module-graph:governance-service",
"members": ["diff_engine", "diff_engine.engine"],
"debt": {
"owner": "governance-service",
"reason": "diff_engine/__init__.py re-exports from engine.py while engine.py imports package-level helpers. Untangling requires moving shared helpers into a leaf module.",
"target_phase": "phase-3"
}
},
{
"scope": "module-graph:web-viewer-sample",
"members": ["App.tsx", "StreamOnlyWindow.tsx", "Window.tsx"],
"debt": {
"owner": "web-viewer-sample",
"reason": "App, Window, and StreamOnlyWindow mutually reference each other for routing and stream-only fallback. The a4-console-convergence and migrate-console-to-hifi-design changes own this surface; splitting it here would conflict with them.",
"target_phase": "phase-3"
}
}
],
"cycle_budgets": [
{ "scope": "service-graph", "maximum": 0, "note": "No service-level dependency cycle is permitted." },
{ "scope": "module-graph:bim-review-coordinator", "maximum": 0 },
{ "scope": "module-graph:bim-streaming-server", "maximum": 1 },
{ "scope": "module-graph:governance-service", "maximum": 1 },
{ "scope": "module-graph:kit-manager-api", "maximum": 0 },
{ "scope": "module-graph:kit-manager-web", "maximum": 0 },
{ "scope": "module-graph:web-viewer-sample", "maximum": 1 }
]
}
81 changes: 81 additions & 0 deletions architecture/observed-baseline.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://ai-bim-governance.local/schemas/observed-baseline.schema.json",
"title": "AI-BIM approved observed architecture baseline",
"description": "Grandfathered dependency edges and cycles. The ratchet allows what is recorded here and fails closed on anything new.",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"approved_on",
"approved_by",
"method",
"service_edges",
"cycles",
"cycle_budgets"
],
"definitions": {
"debt": {
"type": "object",
"additionalProperties": false,
"required": ["owner", "reason", "target_phase"],
"properties": {
"owner": { "type": "string", "minLength": 1 },
"reason": { "type": "string", "minLength": 1 },
"target_phase": { "type": "string", "minLength": 1 }
}
}
},
"properties": {
"$schema": { "type": "string", "minLength": 1 },
"schema_version": { "const": "ai-bim-observed-baseline/v1" },
"approved_on": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
"approved_by": { "type": "string", "minLength": 1 },
"method": { "type": "string", "minLength": 1 },
"service_edges": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["from", "to", "status"],
"properties": {
"from": { "type": "string", "minLength": 1 },
"to": { "type": "string", "minLength": 1 },
"status": { "enum": ["declared", "undeclared-edge", "undeclared-node"] },
"note": { "type": "string", "minLength": 1 },
"debt": { "$ref": "#/definitions/debt" }
}
}
},
"cycles": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["scope", "members", "debt"],
"properties": {
"scope": { "type": "string", "minLength": 1 },
"members": {
"type": "array",
"minItems": 2,
"items": { "type": "string", "minLength": 1 }
},
"debt": { "$ref": "#/definitions/debt" }
}
}
},
"cycle_budgets": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["scope", "maximum"],
"properties": {
"scope": { "type": "string", "minLength": 1 },
"maximum": { "type": "integer", "minimum": 0 },
"note": { "type": "string", "minLength": 1 }
}
}
}
}
}
Loading
Loading