diff --git a/.claude/rules/dependency-boundary.md b/.claude/rules/dependency-boundary.md index a81e122e..039fd87e 100644 --- a/.claude/rules/dependency-boundary.md +++ b/.claude/rules/dependency-boundary.md @@ -64,7 +64,7 @@ shared by several layers". | Gate | Location | When it goes red | |------|------|--------| | **Build-time lock** `POLHEM9001` | `src/Directory.Build.targets` | The moment you write it, `dotnet build` fails | -| **Transitive closure test** | `tests/Polhem.Definition.UnitTests/DefinitionDependencyGateTests.cs`; for the front-end layer `tests/Polhem.UI.Avalonia.UnitTests/FrontEndDependencyGateTests.cs` | When tests run | +| **Transitive closure test** | The `*DependencyGateTests` classes under `tests/`, for example `tests/Polhem.Definition.UnitTests/DefinitionDependencyGateTests.cs` (the header of `src/Directory.Build.targets` says how they pair with the lock) | When tests run | **Both are needed**, because the build-time lock cannot see "a **package** brought in indirectly through some `ProjectReference`" (that is exactly how DynamicExpresso got in), while the closure test only tells you when tests diff --git a/check-dependency-map.sh b/check-dependency-map.sh index 3cb03205..29a039a6 100755 --- a/check-dependency-map.sh +++ b/check-dependency-map.sh @@ -12,11 +12,19 @@ # those references cross, so the overview is derived from the same facts. # - The projects each overview box lists are exactly the projects of that area. A box names them without `Polhem.`, and # a name starting with a dot extends the last full name before it (`Web.Blazor, .Mud` is `Web.Blazor.Mud`). +# - The external package table, between the `dependency-map:packages` comments, lists every project once, and each +# project's packages are exactly the `PackageReference` and `FrameworkReference` items of its csproj that reach a +# consumer: an item marked `PrivateAssets` all is left out. Only names are compared. A row may name several projects +# separated by ` / `, and a cell that contains `(none)` means no packages, whatever else it says; a version after a +# name (`2.x`) and a `FrameworkReference: ` prefix are ignored, so package and framework references are compared +# alike. A package named twice in one row is reported. # -# Left out: `Polhem.Analyzers`, which only orders the build and never reaches a consumer, and references that leave -# src/, such as the `Polhem.JsonRpc.*` packages. The external package table in the document is not checked. +# Left out: `Polhem.Analyzers`, which only orders the build and never reaches a consumer, and project references that +# leave src/. # -# Known limit: a csproj reference is matched as text, so a reference added by a shared props or targets file is not seen. +# Known limits: a csproj reference is matched as text, so a reference added by a shared props or targets file is not +# seen, an item's `Condition` is not evaluated, and an item inside an XML comment still counts (the last two report a +# difference rather than hide one). set -euo pipefail cd "$(dirname "$0")" @@ -38,6 +46,27 @@ git ls-files 'src/*/*.csproj' | while read -r file; do done done | sort > "$work/actual" +# The packages each project passes on to its consumers, one " " line each. +git ls-files 'src/*/*.csproj' | while read -r file; do + name=$(basename "$file" .csproj) + [[ "$name" == "Polhem.Analyzers" ]] && continue + awk -v project="$name" ' + BEGIN { RS = "<" } + /^(Package|Framework)Reference[ \t\r\n]/ { + pending = "" + if (!match($0, /Include[ \t\r\n]*=[ \t\r\n]*["\047][^"\047]+["\047]/)) next + item = substr($0, RSTART, RLENGTH) + sub(/^Include[ \t\r\n]*=[ \t\r\n]*["\047]/, "", item); sub(/["\047]$/, "", item) + if (tolower($0) ~ /privateassets[ \t\r\n]*=[ \t\r\n]*["\047]all["\047]/) next + if ($0 ~ /\/>/) print project, item + else pending = item + next + } + pending != "" && tolower($0) ~ /^privateassets>[ \t\r\n]*all/ { pending = ""; next } + pending != "" && /^\/(Package|Framework)Reference>/ { print project, pending; pending = "" } + ' "$file" +done | sort -u > "$work/packages" + failed=0 report() { @@ -126,10 +155,51 @@ for doc in docs/*/architecture/dependency-map.md; do comm -13 "$work/members" "$work/listed" > "$work/diff" if [[ -s "$work/diff" ]]; then report "$doc" "projects an overview box lists that are not in its area" "$work/diff"; fi - rm -f "$work/edges" "$work/members" "$work/overview" "$work/listed" "$work/errors" + awk -v rows="$work/rows" -v table="$work/table" ' + // { inside = 1; next } + // { inside = 0; next } + !inside || !/^\| *Polhem\./ { next } + { + n = split($0, cell, "|") + projects = cell[2]; packages = cell[3] + gsub(/`/, "", packages); gsub(/FrameworkReference: */, "", packages) + gsub(/、/, ",", packages) + p = split(projects, names, "/") + for (i = 1; i <= p; i++) { + project = names[i]; gsub(/^ +| +$/, "", project) + print project > rows + if (packages ~ /\(none\)/) continue + k = split(packages, items, ",") + for (j = 1; j <= k; j++) { + item = items[j]; gsub(/^ +| +$/, "", item) + sub(/ +[0-9][0-9.x]*$/, "", item) + if (item != "") print project, item > table + } + } + } + ' "$doc" + touch "$work/rows" "$work/table" + uniq -d "$work/table" > "$work/diff" + if [[ -s "$work/diff" ]]; then report "$doc" "packages named twice in one row of the external package table" "$work/diff"; fi + sort -u -o "$work/table" "$work/table" + sort -o "$work/rows" "$work/rows" + + comm -23 "$work/projects" <(sort -u "$work/rows") > "$work/diff" + if [[ -s "$work/diff" ]]; then report "$doc" "projects missing from the external package table" "$work/diff"; fi + uniq -d "$work/rows" > "$work/diff" + if [[ -s "$work/diff" ]]; then report "$doc" "projects in more than one row of the external package table" "$work/diff"; fi + comm -13 "$work/projects" <(sort -u "$work/rows") > "$work/diff" + if [[ -s "$work/diff" ]]; then report "$doc" "projects in the external package table that are not in src/" "$work/diff"; fi + comm -23 "$work/packages" "$work/table" > "$work/diff" + if [[ -s "$work/diff" ]]; then report "$doc" "packages in the csproj files but not in the external package table" "$work/diff"; fi + comm -13 "$work/packages" "$work/table" > "$work/diff" + if [[ -s "$work/diff" ]]; then report "$doc" "packages in the external package table but not in the csproj files" "$work/diff"; fi + + rm -f "$work/edges" "$work/members" "$work/overview" "$work/listed" "$work/errors" "$work/rows" "$work/table" done if [[ "$failed" -ne 0 ]]; then - echo "Update the diagrams in docs/en/architecture/dependency-map.md and its translations to match the csproj files." + echo "Update the diagrams and the external package table in docs/en/architecture/dependency-map.md and its" + echo "translations to match the csproj files." exit 1 fi diff --git a/docs/en/architecture/dependency-map.md b/docs/en/architecture/dependency-map.md index 93b5a850..320e52bf 100644 --- a/docs/en/architecture/dependency-map.md +++ b/docs/en/architecture/dependency-map.md @@ -267,6 +267,8 @@ graph BT ## External Package Dependencies + + | Project | External Packages | |---------|-------------------| | Polhem.Core | *(none)* | @@ -280,12 +282,16 @@ graph BT | Polhem.Repository | Microsoft.Extensions.DependencyInjection.Abstractions 10.x | | Polhem.Hosting | Microsoft.Extensions.DependencyInjection.Abstractions 10.x, Microsoft.Extensions.Hosting.Abstractions 10.x | | Polhem.Web.Blazor | Microsoft.AspNetCore.Components.Web 10.x | +| Polhem.Web.Blazor.Mud | MudBlazor 9.x | +| Polhem.Web.Blazor.Fluent | Microsoft.FluentUI.AspNetCore.Components 5.x | | Polhem.Web.Blazor.Server | `FrameworkReference: Microsoft.AspNetCore.App` | | Polhem.UI.Avalonia | Avalonia 12.x, Avalonia.Controls.DataGrid 12.x | | Polhem.Api.Client | Polhem.JsonRpc.Payload.Client | | Polhem.UI.ApiBridge | Microsoft.Extensions.DependencyInjection.Abstractions 10.x | | Polhem.UI.Avalonia.Shell | Avalonia 12.x, Avalonia.Controls.DataGrid 12.x, Semi.Avalonia 12.x, Semi.Avalonia.DataGrid 12.x, CommunityToolkit.Mvvm 8.x, Microsoft.Extensions.DependencyInjection 10.x | -| Polhem.Api.Contracts / Polhem.Repository.Abstractions / Polhem.UI.Core | *(none)* | +| Polhem.Api.Contracts / Polhem.Repository.Abstractions / Polhem.UI.Core / Polhem.Web.Blazor.Wasm | *(none)* | + + > `Polhem.Api.Core`'s MessagePack reference is the only transport-format package in the framework, and > keeping it the only one is the point of [ADR-036](../../../maintainers/adr/adr-036-wire-serialization-externalized.md). @@ -293,7 +299,8 @@ graph BT > [polhem-dev/polhem-jsonrpc](https://github.com/polhem-dev/polhem-jsonrpc); the version the framework uses is set in > `PolhemJsonRpc.props` at the repository root. An HTTP host adds `Polhem.JsonRpc.AspNetCore` itself. > Build-time-only references (`PrivateAssets="all"`: SourceLink, the public API analyzers, the -> repository's own analyzers) are omitted — they reach no consumer. +> repository's own analyzers) are omitted — they reach no consumer. `check-dependency-map.sh` compares the package +> names in this table with the csproj files; versions are not compared. ## Target Framework Summary @@ -303,18 +310,18 @@ host rather than a choice. ## Tooling Packages (separately distributed) -Not part of the `src/` library graph above — these ship as `dotnet tool` global tools on NuGet: +Not part of the `src/` library graph above — these ship as `dotnet tool` packages on NuGet: | Package | Command | Description | |---------|---------|-------------| -| **Polhem.Cli** (`tools/Polhem.Cli/`) | `dotnet polhem` | Framework CLI; `dotnet polhem --help` lists its commands. References `Polhem.Definition`, whose public `Defaults` API it calls for materialise / list operations on embedded framework defaults. Versioned with the framework (both read `Version.props`). | -| **Polhem.DefineEditor** (`tools/DefineEditor/`) | `polhem-define` | Avalonia desktop tool for visually editing the define types; also built as a macOS `.app`. Carries no database driver: it runs Polhem.DbManager for database work. | +| **Polhem.Cli** (`tools/Polhem.Cli/`) | `dotnet polhem` | Framework CLI; `dotnet polhem --help` lists its commands. References `Polhem.Definition`, whose public `Defaults` API it calls for materialise / list operations on embedded framework defaults, and `tools/Polhem.Definition.Spreadsheet` (MiniExcel) for the form workbooks. Versioned with the framework (both read `Version.props`). | +| **Polhem.DefineEditor** (`tools/DefineEditor/`) | `dotnet polhem-define` (local tool) | Avalonia desktop tool for visually editing the define types; also built as a macOS `.app`. Carries no database driver: it runs Polhem.DbManager for database work. | | **Polhem.DbManager** (`tools/DbManager/`) | `polhem-db` | Runs the `db` commands of `Polhem.Hosting` against a definition folder, with the drivers of every supported database. | ## Architectural Notes - **Polhem.Core** is the lowest-level foundation package with no internal dependencies. -- **Polhem.Expressions** holds `DynamicExpressoEvaluator`, the DynamicExpresso-backed implementation of the expression engine. The *abstraction* — `IExpressionEvaluator`, `ExpressionPolicy`, `ExpressionEvaluationException` — lives in `Polhem.Core.Expressions`, so `Polhem.Definition` (the `FormExpressionCalculator`) and `Polhem.Business` (the rule processor) consume the engine without taking a dependency on DynamicExpresso; only the composition roots that pick an implementation (`Polhem.Hosting` for DI registration, `Polhem.UI.Avalonia` and `Polhem.Web.Blazor.Server` for client-side live preview) reference this package; the live preview engine they share, `FormLiveComputationEngine` in `Polhem.UI.Core`, takes the evaluator from them. That split keeps the definition layer free of third-party packages while a field computed on the client still matches what the server writes on save. See [adr-028](../../../maintainers/adr/adr-028-expression-rule-engine.md) and [adr-038](../../../maintainers/adr/adr-038-definition-dependency-boundary.md). +- **Polhem.Expressions** holds `DynamicExpressoEvaluator`, the DynamicExpresso-backed implementation of the expression engine. The *abstraction* — `IExpressionEvaluator`, `ExpressionPolicy`, `ExpressionEvaluationException` — lives in `Polhem.Core.Expressions`, so `Polhem.Definition` (the `FormExpressionCalculator`) and `Polhem.Business` (the rule processor) consume the engine without taking a dependency on DynamicExpresso; only the composition roots that pick an implementation (`Polhem.Hosting` for DI registration, `Polhem.UI.Avalonia` and `Polhem.Web.Blazor` for client-side live preview) reference this package; the live preview engine they share, `FormLiveComputationEngine` in `Polhem.UI.Core`, takes the evaluator from them. That split keeps the definition layer free of third-party packages while a field computed on the client still matches what the server writes on save. See [adr-028](../../../maintainers/adr/adr-028-expression-rule-engine.md) and [adr-038](../../../maintainers/adr/adr-038-definition-dependency-boundary.md). - **Polhem.Definition** is the most depended-on project. Its direct dependents are Contracts, Db, RepoAbs, Caching, Business, Api.Core and UI.Avalonia. - **Polhem.Api.Contracts** is a shared contract/abstraction layer, not an application-level API project. Despite the "API" name, both `Polhem.Business` and `Polhem.Api.Core` depend on it (`Business → Contracts`, `Core → Contracts`), so it sits *below* them — the diagram puts it in **Foundation** rather than with the API projects. - **Polhem.Hosting** is the composition root: it consolidates the backend services (`Polhem.Api.Server`, `Polhem.Business`, `Polhem.Db`, `Polhem.Repository`, `Polhem.ObjectCaching`) behind a single `AddPolhemFramework` extension on `IServiceCollection`, with no ASP.NET Core dependency. Non-web hosts (WinForms, Console, Worker Service) reference it directly. It has its own **Composition root** area rather than sharing one with `Polhem.Api.Server`: reaching across every layer is what a composition root does, so the "API layer must not reference the Repository layer" constraint does not apply to it. What *does* apply is that it holds no data access of its own — statements live in `Polhem.Db` / `Polhem.Repository`, and Hosting keeps only the hosted-service shells and DI wiring. @@ -328,4 +335,4 @@ Not part of the `src/` library graph above — these ship as `dotnet tool` globa - **`Polhem.UI.*` family criterion**: every front end references `Polhem.UI.Core`, so referencing it no longer decides the family. What decides it is whether the package builds on the single-user model, where one process serves one signed-in user and the services come from the process-wide `UIServices.Provider` (and, with a backend, from `Polhem.UI.ApiBridge`). - Single-user → `Polhem.UI.*` (current: `Polhem.UI.Core`, `Polhem.UI.ApiBridge`, `Polhem.UI.Avalonia`, `Polhem.UI.Avalonia.Shell`; future: `Polhem.UI.WinForms`, `Polhem.UI.Wpf`, etc.) - Serves several users from one process, with its own state management → independent family prefix (e.g. `Polhem.Web.Blazor.*`). The Blazor head used to stay away from `Polhem.UI.Core` because that package held the process-wide `ClientInfo`; that state now lives in `Polhem.UI.ApiBridge`, so the Blazor head references `Polhem.UI.Core` for the shared value rules but must not use `UIServices` or reference `Polhem.UI.ApiBridge`. `UIServicesUsageGateTests` (`tests/Polhem.Web.Blazor.Server.UnitTests`) fails when it does. -- The **Web frontend layer** is three kinds of Razor Class Library (RCL): the shared `Polhem.Web.Blazor`, a UI package (`Polhem.Web.Blazor.Mud`, `Polhem.Web.Blazor.Fluent`) and a host package (`Polhem.Web.Blazor.Server`, `Polhem.Web.Blazor.Wasm`); an application chooses one UI package and one host package, and the two do not reference each other ([adr-059](../../../maintainers/adr/adr-059-blazor-shared-layer.md)). The shared layer depends on `Polhem.Api.Client`, on `Polhem.UI.Core` for the value rules it shares with the native heads, and on `Polhem.Expressions` for the live preview of computed fields; the Blazor Server host chooses the transport in `AddPolhemBlazor`, while the WebAssembly host is always remote: `UseRemoteProvider` (each circuit's `PolhemApiClient` calls over HTTP) or `UseLocalProvider` (in process, for trusted users only, which also needs `AddPolhemFramework` on the same service collection). +- The **Web frontend layer** is three kinds of package: the shared `Polhem.Web.Blazor`, a UI package (`Polhem.Web.Blazor.Mud`, `Polhem.Web.Blazor.Fluent`) and a host package (`Polhem.Web.Blazor.Server`, `Polhem.Web.Blazor.Wasm`); an application chooses one UI package and one host package, and the two do not reference each other ([adr-059](../../../maintainers/adr/adr-059-blazor-shared-layer.md)). All but the WebAssembly host are Razor Class Libraries; the WebAssembly host is a plain class library that a Blazor WebAssembly project references. The shared layer depends on `Polhem.Api.Client`, on `Polhem.UI.Core` for the value rules it shares with the native heads, and on `Polhem.Expressions` for the live preview of computed fields. The Blazor Server host chooses the transport in `AddPolhemBlazor`: `UseRemoteProvider` (each circuit's `PolhemApiClient` calls over HTTP) or `UseLocalProvider` (in process, for trusted users only, which also needs `AddPolhemFramework` on the same service collection). The WebAssembly host is always remote. diff --git a/docs/zh-TW/architecture/dependency-map.md b/docs/zh-TW/architecture/dependency-map.md index 316205c6..f90f04d6 100644 --- a/docs/zh-TW/architecture/dependency-map.md +++ b/docs/zh-TW/architecture/dependency-map.md @@ -1,4 +1,4 @@ - + # 專案相依性全景圖 [English](../../en/architecture/dependency-map.md) · [← 文件索引](../README.md) @@ -259,6 +259,8 @@ graph BT ## 外部相依套件 + + | 專案 | 外部套件 | |------|----------| | Polhem.Core | *(none)* | @@ -272,19 +274,23 @@ graph BT | Polhem.Repository | Microsoft.Extensions.DependencyInjection.Abstractions 10.x | | Polhem.Hosting | Microsoft.Extensions.DependencyInjection.Abstractions 10.x、Microsoft.Extensions.Hosting.Abstractions 10.x | | Polhem.Web.Blazor | Microsoft.AspNetCore.Components.Web 10.x | +| Polhem.Web.Blazor.Mud | MudBlazor 9.x | +| Polhem.Web.Blazor.Fluent | Microsoft.FluentUI.AspNetCore.Components 5.x | | Polhem.Web.Blazor.Server | `FrameworkReference: Microsoft.AspNetCore.App` | | Polhem.UI.Avalonia | Avalonia 12.x、Avalonia.Controls.DataGrid 12.x | | Polhem.Api.Client | Polhem.JsonRpc.Payload.Client | | Polhem.UI.ApiBridge | Microsoft.Extensions.DependencyInjection.Abstractions 10.x | | Polhem.UI.Avalonia.Shell | Avalonia 12.x, Avalonia.Controls.DataGrid 12.x, Semi.Avalonia 12.x, Semi.Avalonia.DataGrid 12.x, CommunityToolkit.Mvvm 8.x, Microsoft.Extensions.DependencyInjection 10.x | -| Polhem.Api.Contracts / Polhem.Repository.Abstractions / Polhem.UI.Core | *(none)* | +| Polhem.Api.Contracts / Polhem.Repository.Abstractions / Polhem.UI.Core / Polhem.Web.Blazor.Wasm | *(none)* | + + > `Polhem.Api.Core` 的 MessagePack 是框架內**唯一**的傳輸格式套件,而讓它維持唯一正是 > [ADR-036](../../../maintainers/adr/adr-036-wire-serialization-externalized.md) 的用意。`Polhem.JsonRpc.*` 套件維護在 > [polhem-dev/polhem-jsonrpc](https://github.com/polhem-dev/polhem-jsonrpc),框架使用的版本設定在 repo 根目錄的 > `PolhemJsonRpc.props`;HTTP 宿主自行加入 `Polhem.JsonRpc.AspNetCore`。僅建置期的參考 > (`PrivateAssets="all"`:SourceLink、公開 API 分析器、本 repo 自己的 analyzer) -> 不列入 —— 它們不會流到任何消費者。 +> 不列入 —— 它們不會流到任何消費者。`check-dependency-map.sh` 會比對本表的套件名稱與 csproj 檔,版本不比對。 ## 目標框架摘要 @@ -293,18 +299,18 @@ graph BT ## 工具套件(獨立發行) -不屬於上方 `src/` 套件相依圖——以 `dotnet tool` 全域工具形式 ship 在 NuGet: +不屬於上方 `src/` 套件相依圖——以 `dotnet tool` 套件形式 ship 在 NuGet: | 套件 | 命令 | 說明 | |------|------|------| -| **Polhem.Cli**(`tools/Polhem.Cli/`) | `dotnet polhem` | 框架 CLI;`dotnet polhem --help` 列出其命令。引用 `Polhem.Definition`,呼叫其公開 `Defaults` API 處理 embedded 框架預設的 materialize / list。版本與框架一致(兩者都讀 `Version.props`)。 | -| **Polhem.DefineEditor**(`tools/DefineEditor/`) | `polhem-define` | Avalonia 桌面工具,可視覺化編輯各種定義類型;也可建置成 macOS `.app`。不帶資料庫驅動:資料庫相關工作交給 Polhem.DbManager。 | +| **Polhem.Cli**(`tools/Polhem.Cli/`) | `dotnet polhem` | 框架 CLI;`dotnet polhem --help` 列出其命令。引用 `Polhem.Definition`,呼叫其公開 `Defaults` API 處理 embedded 框架預設的 materialize / list;並引用 `tools/Polhem.Definition.Spreadsheet`(MiniExcel)處理表單活頁簿。版本與框架一致(兩者都讀 `Version.props`)。 | +| **Polhem.DefineEditor**(`tools/DefineEditor/`) | `dotnet polhem-define`(本機工具) | Avalonia 桌面工具,可視覺化編輯各種定義類型;也可建置成 macOS `.app`。不帶資料庫驅動:資料庫相關工作交給 Polhem.DbManager。 | | **Polhem.DbManager**(`tools/DbManager/`) | `polhem-db` | 以所有支援資料庫的驅動,對定義資料夾執行 `Polhem.Hosting` 的 `db` 指令。 | ## 架構要點 - **Polhem.Core** 為最底層基礎套件,無任何內部相依性。 -- **Polhem.Expressions** 只承載 `DynamicExpressoEvaluator`——運算式引擎以 DynamicExpresso 為底的實作。**抽象**(`IExpressionEvaluator`、`ExpressionPolicy`、`ExpressionEvaluationException`)位於 `Polhem.Core.Expressions`,因此 `Polhem.Definition`(`FormExpressionCalculator`)與 `Polhem.Business`(規則處理器)消費引擎時不會相依 DynamicExpresso;只有決定用哪個實作的組裝層(`Polhem.Hosting` 的 DI 註冊、`Polhem.UI.Avalonia` 與 `Polhem.Web.Blazor.Server` 的前端即時預覽)才引用本套件;兩端共用的即時預覽引擎 `Polhem.UI.Core` 的 `FormLiveComputationEngine` 由它們傳入運算引擎。這個分界讓定義層不帶第三方套件,同時維持前端算值與後端存檔一致。見 [adr-028](../../../maintainers/adr/adr-028-expression-rule-engine.md) 與 [adr-038](../../../maintainers/adr/adr-038-definition-dependency-boundary.md)。 +- **Polhem.Expressions** 只承載 `DynamicExpressoEvaluator`——運算式引擎以 DynamicExpresso 為底的實作。**抽象**(`IExpressionEvaluator`、`ExpressionPolicy`、`ExpressionEvaluationException`)位於 `Polhem.Core.Expressions`,因此 `Polhem.Definition`(`FormExpressionCalculator`)與 `Polhem.Business`(規則處理器)消費引擎時不會相依 DynamicExpresso;只有決定用哪個實作的組裝層(`Polhem.Hosting` 的 DI 註冊、`Polhem.UI.Avalonia` 與 `Polhem.Web.Blazor` 的前端即時預覽)才引用本套件;兩端共用的即時預覽引擎 `Polhem.UI.Core` 的 `FormLiveComputationEngine` 由它們傳入運算引擎。這個分界讓定義層不帶第三方套件,同時維持前端算值與後端存檔一致。見 [adr-028](../../../maintainers/adr/adr-028-expression-rule-engine.md) 與 [adr-038](../../../maintainers/adr/adr-038-definition-dependency-boundary.md)。 - **Polhem.Definition** 為被依賴次數最多的專案,直接相依者為 Contracts、Db、RepoAbs、Caching、Business、Api.Core 與 UI.Avalonia。 - **Polhem.Api.Contracts** 是共用契約/抽象層,並非應用層級的 API 專案。雖名為「API」,但 `Polhem.Business` 與 `Polhem.Api.Core` 都相依於它(`Business → Contracts`、`Core → Contracts`),故其位置在兩者**之下** —— 圖上歸入**基礎**,而不和 API 專案放在一起。 - **Polhem.Hosting** 為 composition root:將後端服務(`Polhem.Api.Server`、`Polhem.Business`、`Polhem.Db`、`Polhem.Repository`、`Polhem.ObjectCaching`)整合於一個 `IServiceCollection.AddPolhemFramework` 擴充入口,不依賴 ASP.NET Core。非 web 宿主(WinForms、Console、Worker Service)直接引用此套件。圖上自成**組合根**一區,不和 `Polhem.Api.Server` 同區:橫跨各層本就是組合根的職責,故「API 層不得直接引用 Repository 層」的限制不適用於它。真正適用的是**它本身不含資料存取** —— SQL 語句歸 `Polhem.Db` / `Polhem.Repository`,Hosting 只留 hosted service 外殼與 DI 接線。 @@ -318,4 +324,4 @@ graph BT - **`Polhem.UI.*` family 判別準則**:所有前端都引用 `Polhem.UI.Core`,所以是否引用它已不能決定 family。決定的是套件是否建立在單一使用者模型上:一個 process 服務一位已登入使用者,服務來自整個 process 共用的 `UIServices.Provider`(有後端時再加上 `Polhem.UI.ApiBridge`)。 - 單一使用者 → 歸 `Polhem.UI.*`(目前:`Polhem.UI.Core`、`Polhem.UI.ApiBridge`、`Polhem.UI.Avalonia`、`Polhem.UI.Avalonia.Shell`;未來:`Polhem.UI.WinForms`、`Polhem.UI.Wpf` 等同理) - 一個 process 服務多位使用者、自有狀態管理 → 走獨立 family prefix(如 `Polhem.Web.Blazor.*`)。Blazor 端過去不引用 `Polhem.UI.Core`,是因為該套件持有整個 process 共用的 `ClientInfo`;這份狀態已移到 `Polhem.UI.ApiBridge`,所以 Blazor 端現在為了共用的值規則引用 `Polhem.UI.Core`,但不得使用 `UIServices`,也不得引用 `Polhem.UI.ApiBridge`。違反時 `UIServicesUsageGateTests`(`tests/Polhem.Web.Blazor.Server.UnitTests`)會失敗。 -- **Web 前端層**由三種 RCL(Razor Class Library)組成:共用的 `Polhem.Web.Blazor`、UI 套件(`Polhem.Web.Blazor.Mud`、`Polhem.Web.Blazor.Fluent`)與宿主套件(`Polhem.Web.Blazor.Server`、`Polhem.Web.Blazor.Wasm`);應用程式各選一個 UI 套件和宿主套件,兩者互不參考([adr-059](../../../maintainers/adr/adr-059-blazor-shared-layer.md))。共用層相依 `Polhem.Api.Client`,為了與 native 各端共用的值規則相依 `Polhem.UI.Core`,並為了計算欄位的即時預覽相依 `Polhem.Expressions`,由宿主在 `AddPolhemBlazor` 選擇傳輸方式:`UseRemoteProvider`(每個 circuit 的 `PolhemApiClient` 經 HTTP 呼叫),或 `UseLocalProvider`(in-process 呼叫,僅限受信任的使用者,且須在同一個 service collection 上呼叫 `AddPolhemFramework`)。 +- **Web 前端層**由三種套件組成:共用的 `Polhem.Web.Blazor`、UI 套件(`Polhem.Web.Blazor.Mud`、`Polhem.Web.Blazor.Fluent`)與宿主套件(`Polhem.Web.Blazor.Server`、`Polhem.Web.Blazor.Wasm`);應用程式各選一個 UI 套件和宿主套件,兩者互不參考([adr-059](../../../maintainers/adr/adr-059-blazor-shared-layer.md))。除了 WebAssembly 宿主之外都是 Razor Class Library;WebAssembly 宿主是一般的類別庫,由 Blazor WebAssembly 專案參考。共用層相依 `Polhem.Api.Client`,為了與 native 各端共用的值規則相依 `Polhem.UI.Core`,並為了計算欄位的即時預覽相依 `Polhem.Expressions`。Blazor Server 宿主在 `AddPolhemBlazor` 選擇傳輸方式:`UseRemoteProvider`(每個 circuit 的 `PolhemApiClient` 經 HTTP 呼叫),或 `UseLocalProvider`(in-process 呼叫,僅限受信任的使用者,且須在同一個 service collection 上呼叫 `AddPolhemFramework`)。WebAssembly 宿主一律遠端呼叫。 diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 587139f6..1b626fc6 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -20,14 +20,15 @@ The front-end layer is locked the same way (ADR-054): Polhem.UI.Core and Polhem.UI.Avalonia reach the backend only through the contracts in Polhem.Api.Contracts, so a reference to Polhem.Api.Client or - Polhem.Api.Core, which would carry MessagePack and the JSON-RPC server packages into every front end, + Polhem.Api.Core, which would carry MessagePack and the JSON-RPC packages into every front end, fails the build here. - The same boundary is asserted from the other end by - tests/Polhem.Definition.UnitTests/DefinitionDependencyGateTests.cs and, for the front-end layer, - tests/Polhem.UI.Avalonia.UnitTests/FrontEndDependencyGateTests.cs and, for Polhem.Api.Core and the API client - that inherits it, tests/Polhem.Api.Client.UnitTests/ApiClientDependencyGateTests.cs, which walk the full - transitive closure. The two are complements, not duplicates: this target catches the direct + Polhem.Api.Core and the Blazor shared layer and WebAssembly host are governed too; the condition on + PolhemEnforceDependencyBoundary below is the list. + + The same boundary is asserted from the other end by closure tests, classes named *DependencyGateTests + under tests/ (the definition layer's is DefinitionDependencyGateTests, the Avalonia front end's + FrontEndDependencyGateTests), which walk the full transitive closure. The two are complements, not duplicates: this target catches the direct edge at the moment it is written, the test catches a package that arrives indirectly through a project reference — which is exactly how DynamicExpresso.Core got in. --> @@ -140,7 +141,7 @@ + Text="$(MSBuildProjectName) is locked to an explicit dependency allowlist (ADR-038, ADR-054). Unexpected consumer-facing reference(s): @(_PolhemBoundaryViolation, ', '). Anything added here is inherited by everything that references this assembly. Prefer keeping the abstraction here and the implementation in a separate assembly that only a composition root references. If the dependency really belongs here, add it to PolhemAllowedDependency in src/Directory.Build.targets, add it to the allowlist of the closure test that covers this project (a *DependencyGateTests class under tests/), and record why in the ADR." />