Industry Extensions 的隔离单位是程序集,不是运行时模块实例。三个 csproj 都只引用 Domain(Auction 再引用 Finance),没有 Application/Contracts/Infrastructure 分层,也没有服务注册入口。
1. 物理项目
| 项目 | Description 声明 | 真实内容 |
|---|---|---|
| Finance | 显式选择的金融计算器 | 5 个计算器 + 1 个分档引擎 + Error catalog |
| HR | 显式选择的 HR 计算器 | LeaveCalculator + LeavePolicySchedule + Error catalog |
| Auction | 显式选择的拍卖计算器 | AuctionCommissionCalculator + Error catalog |
所有项目 TargetFramework 为 net10.0,开启 Nullable/ImplicitUsings,没有 PackageId、Version、SourceLink 或 NuGet pack 元数据。所谓“可选包”当前主要指 ProjectReference 选择,不是已经独立发布和签名的商业 NuGet 产品。
2. 依赖方向
旧边界“通用平台不得反向依赖行业包”已经存在一个有意例外:LegalCalculators 直接依赖 Finance。原因是复用通用 TaxBracket/分档引擎,但物理位置使法律计算器依赖“Finance 行业包”。应把真正通用的 bracket primitive 移到中立 calculators 包,或记录例外。
3. 治理 marker 寄宿
IndustryExtensionsModule 不在三个计算程序集,而在已被 Host 加载的 BitzOrcas.Platform.Application。注释明确说三个子程序集不在 API 组合根引用图,因此借寄宿 marker 让 Governance Generator 生成贡献。
这会产生“控制面登记 IndustryExtensions,但数据面没有计算程序集”的状态。Module Registry、Operations 和 Profile catalog 若只看 contribution,可能把能力报告为可用。Readiness 必须同时检查 assembly/reference、所需 wrapper 和规则版本。
4. 没有运行时启用机制
源码没有 AddIndustryExtensions、IModuleServiceConfigurator、Feature Key、Permission Catalog、Health Check 或 Adapter readiness。添加 ProjectReference 后,任何代码都可直接调用 static 方法;没有统一开关。
因此“Profile 显式选择扩展”目前是构建期约定,而不是可审计运行时政策。若模板/Profile 要支持选择,必须让项目生成结果、许可证、Feature 和组合根证据形成闭环。
5. 消费模式
// Wrapper 固定本次业务用途允许的、已审核政策版本。var policy = await policyStore.GetPublishedAsync( jurisdiction, scenario: "finance.income-tax", occurredOn, cancellationToken);
// 静态计算器只处理数值;上层保留身份、租户和证据边界。var raw = IncomeTaxCalculator.CalcIndividualIncomeTax(facts.TaxableIncome);if (raw.IsFailure) return raw.Error;
// 结果账本保存输入 hash、政策版本、明细和舍入策略,才能重放。return await ledger.RecordAsync( facts.RedactedSnapshot, policy.Version, raw.Value, cancellationToken);示例中的 policyStore/ledger 是目标集成层,不在当前三个程序集。不要把租户、数据库或认证塞回纯计算函数;由消费模块建立 Application wrapper。
6. Wrapper 的职责
- 校验调用者有权处理薪资、贷款、合同或拍卖事实;
- 解析 Tenant、Office、Jurisdiction、Taxpayer/Employee/Contract 类型;
- 按发生日期选择已发布政策;
- 校验金额单位、货币、日期和输入证据;
- 调用纯计算器并生成 breakdown;
- 保存不可变结果账本和审计;
- 对敏感字段执行最小化、加密、保留与删除政策;
- 把 Error code 经 I18n 映射为展示文案;
- 对外返回“参考/正式”用途与法务免责声明。
7. 无持久化不等于无隐私风险
类库本身不保存数据,也不记录日志,这是良好隔离。但调用输入可能包含收入、扣除、贷款本金、损失、孕产和工龄等高敏感信息。上层若把参数直接写日志、Trace tag 或异常描述,仍会泄漏。
可观测性记录场景、政策版本、Error code、数值范围 bucket 和时延,不记录原始工资/贷款/健康数据。结果 ledger 应根据用途分别执行税务、劳动或合同档案保留规则。
8. Error 边界
Finance/Hr 的 error catalog 允许多个首段前缀,Auction 固定 Auction。Error description 是中文完整句;code 才是稳定合同。
var result = MortgageCalculator.CalcEqualInstallmentMonthly( principal, annualRate, years);
// 上层只把 code 写入 Problem Details;原始敏感输入不进入日志。if (result.IsFailure){ diagnostics.Count(result.Error.Code, calculator: "mortgage"); return Errors.Localize(result.Error.Code, languageContext.CurrentLanguage);}
// 成功后再由调用模块决定是否持久化和审计。return result.Value;部分辅助方法直接返回 decimal/int,无法表达校验失败;对外 wrapper 应先验证输入,避免绕过 Result 语义。
9. AOT 声明
源码 XML 多次称零反射、Native AOT 安全。实现确实只用基础数值、Span、List、DateTime 与 ValueObject,没有动态反射/序列化。但“方法可被 AOT 编译”不等于项目已在 Native AOT Host 上构建运行。
GA 证据应包含 AOT publish、trim warnings、调用 smoke 和 public API compatibility。尤其 tuple/ValueObject、Error catalog 和未来规则反序列化需要持续验证。
10. 版本与分发
若三个项目作为商业可选包交付,需要 SemVer、兼容性政策、签名、SBOM、许可证 entitlement、SourceLink、符号包、漏洞扫描和可撤回版本。规则变化与二进制版本不应一一绑定;更适合把引擎版本和 Policy bundle 版本分离。
消费者升级时应能并行保留旧政策,重放历史结果。只替换 DLL 中的 static array 会让历史计算不可重现,也无法判断哪个客户仍使用旧法源。
11. 运行时能力探针
目标 readiness 至少回答:程序集是否加载、wrapper 是否注册、许可证/Feature 是否允许、政策仓是否有当前 Published 版本、法源是否过期、黄金用例是否通过、ledger/outbox 是否可用。
仅有 Governance contribution 不应让 Ready 变绿。
12. 测试边界
纯类库以单元/属性/黄金测试为主;wrapper 另测 HTTP 授权、租户隔离、Policy resolution、审计和幂等;分发层测 NuGet/AOT/模板/Profile。不要为了“集成测试”给纯计算器引入数据库。
当前 tests 树没有任何 calculator 方法引用,这是最直接的发布阻断项。
13. 检查命令
# 项目引用和 Host 缺席。rg -n "ProjectReference|Description" src/Platform/IndustryExtensions -g '*.csproj'rg -n "IndustryExtensions" src/Hosts -g '*.cs' -g '*.csproj'
# DI、权限、Feature、端点与测试当前预期无命中。rg -n "AddIndustryExtensions|PermissionCatalog|Feature|GenerateEndpoint|\[Fact\]" \ src/Platform/IndustryExtensions tests -g '*.cs'