fix(invariants): join package checks at startup
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-19-package-invariant-runtime-contracts.md: d3b327694b0d6779409582a7bb199e9cd48a1e4c
|
||||
2026-07-19-package-invariant-runtime-contracts.zh.md: 07c15e80587a292fc94e3537980ee26cbae15115
|
||||
2026-07-19-package-invariant-runtime-contracts.md: 65986fc0b3aab695d8512d9e221052e1db83445f
|
||||
2026-07-19-package-invariant-runtime-contracts.zh.md: 0ae0bd305692ee71359c5500e477dee22575a9ef
|
||||
|
||||
@@ -10,7 +10,7 @@ The package-owned invariant seam made registration and publication exhaustive, b
|
||||
|
||||
Every package shape cannot use the same invariant. Cordis plugins own fibers, injections, effects, and services; service seams admit structural third-party implementations; stateful domains need event relations; pure libraries and bin packages expose algebra, parsing, normalization, or entrypoint constraints. The repository needs one enforceable obligation without moving those contracts back into a central product-aware package.
|
||||
|
||||
Vitest also mounts every companion globally. Companion modules therefore cannot eagerly import every product entrypoint before a test module establishes its hoisted mocks, and a name-based observer cannot mistake an anonymous child fiber that inherits its parent's display name for the package plugin itself.
|
||||
Vitest mounts each package test's owning companion globally, and one exhaustive topology mounts every companion. Companion modules therefore cannot eagerly import every product entrypoint before a test module establishes its hoisted mocks, and a name-based observer cannot mistake an anonymous child fiber that inherits its parent's display name for the package plugin itself.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -31,17 +31,17 @@ At implementation time this covers all 91 workspace packages: four stateful comp
|
||||
|
||||
### Product-independent observers
|
||||
|
||||
`observePluginInvariant` checks existing fibers immediately and future active fibers through global Cordis lifecycle events. A contract may supply an exact callback when that import is safe. Otherwise it matches `fiber.runtime.name`, the name declared by that fiber's own plugin runtime, rather than the inherited `fiber.name`; anonymous `ctx.inject()` children are therefore not misidentified as their parent package. The observer checks required injection keys, recursively collected effect labels, services provided by that exact fiber, and an optional owner validator. Config-dependent packages encode symmetric relations, such as automatic compaction owning both listeners or neither when disabled.
|
||||
`observePluginInvariant` checks existing fibers immediately and future active fibers through a callback/name index behind one root-shared Cordis lifecycle listener pair. A contract may supply an exact callback when that import is safe. Otherwise it matches `fiber.runtime.name`, the name declared by that fiber's own plugin runtime, rather than the inherited `fiber.name`; anonymous `ctx.inject()` children are therefore not misidentified as their parent package. The observer checks required injection keys, recursively collected effect labels, services provided by that exact fiber, and an optional owner validator. Config-dependent packages encode symmetric relations, such as automatic compaction owning both listeners or neither when disabled.
|
||||
|
||||
`observeServiceInvariant` checks the current service and every later binding. `serviceShapeViolation` validates callable members and non-empty string descriptors structurally instead of using `instanceof`, so conforming third-party backends and complete test doubles remain valid while incomplete stand-ins fail.
|
||||
|
||||
`assertInvariant` handles synchronous package algebra. Pure-package companions register an asynchronous child effect and dynamically import their owner inside that effect. This preserves atomic service-owned rollback while allowing the test module, Loader, or deployment to establish mocks and module resolution before the invariant samples the owner.
|
||||
`assertInvariant` handles package algebra. Pure-package companions return an asynchronous installer promise and dynamically import their owner during child startup. The service joins that promise for atomic rollback while allowing the test module, Loader, or deployment to establish mocks and module resolution before the invariant samples the owner.
|
||||
|
||||
### Gate and test execution
|
||||
|
||||
`verify-package-invariants` discovers every workspace package and retains the publication checks for the exact registration name, `./invariant` export, published files, invariant peer and development dependencies, TypeScript reference, and bundle entry. Its source check additionally parses the local `install` function, rejects a generated marker or empty body, requires a second failure-reporter parameter and its use, and rejects duplicate name-based plugin observers across packages. These AST checks are a minimum acceptance rule, not a claim that source shape proves semantic quality.
|
||||
|
||||
The Vitest setup host mounts `InvariantService` with `{ enabled: true }` and all 91 companions before an ordinary Cordis root's first plugin. The host joins companion startup to the test's root-level composition boundary, so asynchronous pure checks and plugin-observer setup fail the test rather than becoming background diagnostics. Focused selection, lifecycle, and owner suites build their own enabled topology to avoid duplicate registrations while still testing invariants.
|
||||
The Vitest setup host mounts `InvariantService` with `{ enabled: true }` before an ordinary Cordis root's first plugin and adds the current test package's companion. The host joins companion startup to the test's root-level composition boundary, so asynchronous pure checks and plugin-observer setup fail the test rather than becoming background diagnostics. One exhaustive topology mounts all 91 companions once to prove runtime registration and coverage; focused selection, lifecycle, and owner suites build their own enabled topology to avoid duplicate registrations while still testing invariants.
|
||||
|
||||
Helper tests reject invalid plugin names, missing injections, effects, services, custom relations, malformed service shapes, and failed assertions. Package suites then activate real plugins across their existing config and HMR paths. Test-only service stand-ins must implement the complete checked seam rather than bypass global invariants.
|
||||
|
||||
@@ -50,7 +50,7 @@ Helper tests reject invalid plugin names, missing injections, effects, services,
|
||||
- **Keep generated ownership-only companions.** Rejected because registration without an executable assertion cannot reject a broken package and makes the exhaustive gate misleading.
|
||||
- **Generate one synthetic assertion into every package.** Rejected because a universal assertion would again optimize for satisfying the gate instead of protecting an owner-specific contract.
|
||||
- **Move the per-package contract matrix into `dsh-invariants`.** Rejected because product imports, vocabulary, and change ownership would return to the central service.
|
||||
- **Import every owner entrypoint statically from its companion.** Rejected because the global test host would preload packages before hoisted mocks and shipped compositions would pay unrelated module initialization costs.
|
||||
- **Import every owner entrypoint statically from its companion.** Rejected because owning and exhaustive test hosts would preload packages before hoisted mocks and shipped compositions would pay unrelated module initialization costs.
|
||||
- **Require first-party service-class identity.** Rejected because service seams are structural extension boundaries; `instanceof` would reject valid external implementations and test doubles.
|
||||
- **Register invariants implicitly from package root entrypoints.** Rejected for the composition-order and hidden-effect reasons in the package-owned service RFC.
|
||||
|
||||
@@ -58,8 +58,8 @@ Helper tests reject invalid plugin names, missing injections, effects, services,
|
||||
|
||||
- Every package contributes an executable check; adding a package without one fails the top-level gate.
|
||||
- The invariant service remains product-independent while providing reusable lifecycle and shape observers.
|
||||
- Ordinary unit, snapshot, and e2e tests run with global invariant enablement and every companion registered.
|
||||
- Ordinary unit, snapshot, and e2e roots run with global invariant enablement and the test package's companion; one exhaustive topology registers every companion.
|
||||
- Plugin names used for name-based observation must be unique within one Cordis root; packages may opt into exact callback identity when safe.
|
||||
- Pure-package checks sample stable startup contracts. Mutable behavior must use an event, service, or plugin-fiber observer.
|
||||
- More companion work runs during tests and selected deployments, trading small startup cost for immediate package-attributed failures.
|
||||
- Relevant companion work runs during package tests and selected deployments, trading bounded startup cost for immediate package-attributed failures.
|
||||
- The original regex selection, blocklist precedence, registration uniqueness, rollback, disposal, and HMR contracts remain unchanged.
|
||||
|
||||
@@ -10,7 +10,7 @@ Status: implemented
|
||||
|
||||
不同包形态不能使用同一种不变式。Cordis 插件拥有 fiber、注入、effect 与服务;服务接缝允许结构兼容的第三方实现;有状态领域需要事件关系;纯库和 bin 包暴露代数、解析、规范化或入口约束。仓库需要一个可执行的统一义务,同时不能把这些契约重新移回了解产品语义的中央包。
|
||||
|
||||
Vitest 还会全局挂载每个伴随插件。因此伴随模块不能在测试模块建立 hoisted mock 之前急切导入所有产品入口;按名称观察时,也不能把继承父级显示名的匿名子 fiber 误认为包插件本身。
|
||||
Vitest 会为每个包测试全局挂载其所有者伴随插件,并由一个完整拓扑挂载全部伴随插件。因此伴随模块不能在测试模块建立 hoisted mock 之前急切导入所有产品入口;按名称观察时,也不能把继承父级显示名的匿名子 fiber 误认为包插件本身。
|
||||
|
||||
## 决策
|
||||
|
||||
@@ -31,17 +31,17 @@ Vitest 还会全局挂载每个伴随插件。因此伴随模块不能在测试
|
||||
|
||||
### 与产品无关的观察器
|
||||
|
||||
`observePluginInvariant` 会立即检查已有 fiber,并通过全局 Cordis 生命周期事件检查未来进入活跃状态的 fiber。安全导入时,契约可以提供准确 callback;否则匹配 `fiber.runtime.name`,即该 fiber 自身插件运行时声明的名称,而不是继承而来的 `fiber.name`,因此匿名 `ctx.inject()` 子级不会被误认成父包。观察器检查必要注入键、递归收集的 effect 标签、由该 fiber 准确提供的服务,以及可选的所有者验证器。依赖配置的包使用对称关系,例如自动压缩要么同时拥有两个监听器,要么在关闭时两个都没有。
|
||||
`observePluginInvariant` 会立即检查已有 fiber,并通过根上下文共享的一对 Cordis 生命周期监听器背后的 callback/名称索引检查未来进入活跃状态的 fiber。安全导入时,契约可以提供准确 callback;否则匹配 `fiber.runtime.name`,即该 fiber 自身插件运行时声明的名称,而不是继承而来的 `fiber.name`,因此匿名 `ctx.inject()` 子级不会被误认成父包。观察器检查必要注入键、递归收集的 effect 标签、由该 fiber 准确提供的服务,以及可选的所有者验证器。依赖配置的包使用对称关系,例如自动压缩要么同时拥有两个监听器,要么在关闭时两个都没有。
|
||||
|
||||
`observeServiceInvariant` 检查当前服务及之后的每次绑定。`serviceShapeViolation` 以结构方式验证可调用成员和非空字符串描述字段,而不使用 `instanceof`;因此符合契约的第三方后端和完整测试替身有效,不完整替身会失败。
|
||||
|
||||
`assertInvariant` 处理同步包代数。纯包伴随插件注册异步子 effect,并在该 effect 内动态导入所有者。这样既保留服务拥有的原子回滚,又允许测试模块、Loader 或部署先建立 mock 和模块解析,再由不变式采样所有者。
|
||||
`assertInvariant` 处理包代数。纯包伴随插件返回异步 installer promise,并在子 fiber 启动期间动态导入所有者。服务会等待该 promise 以保证原子回滚,同时允许测试模块、Loader 或部署先建立 mock 和模块解析,再由不变式采样所有者。
|
||||
|
||||
### 门禁与测试执行
|
||||
|
||||
`verify-package-invariants` 发现每个工作区包,并保留准确注册名、`./invariant` export、发布文件、不变式 peer 与开发依赖、TypeScript 引用和 bundle 入口的发布检查。源码检查还会解析本地 `install` 函数,拒绝生成标记或空函数体,要求第二个失败报告器参数及其使用,并拒绝跨包重复的按名称插件观察器。这些 AST 检查只是最低接收规则,并不宣称源码形状足以证明语义质量。
|
||||
|
||||
Vitest setup host 使用 `{ enabled: true }` 挂载 `InvariantService` 和全部 91 个伴随插件,然后才启动普通 Cordis 根上下文的第一个插件。host 会把伴随插件启动加入测试的根级组合边界,因此异步纯检查和插件观察器安装会让测试失败,而不会变成后台诊断。选择、生命周期和所有者聚焦套件自行构建启用的不变式拓扑,在避免重复注册的同时继续测试不变式。
|
||||
Vitest setup host 会在普通 Cordis 根上下文启动第一个插件前,以 `{ enabled: true }` 挂载 `InvariantService`,并添加当前测试包的伴随插件。host 会把伴随插件启动加入测试的根级组合边界,因此异步纯检查和插件观察器安装会让测试失败,而不会变成后台诊断。一个完整拓扑会一次挂载全部 91 个伴随插件,以证明运行时注册与覆盖率;选择、生命周期和所有者聚焦套件自行构建启用的不变式拓扑,在避免重复注册的同时继续测试不变式。
|
||||
|
||||
辅助测试会拒绝错误插件名、缺失注入、effect、服务或自定义关系、错误服务形状和失败断言。随后,包套件在已有配置与 HMR 路径上激活真实插件。测试专用服务替身必须实现完整的已检查接缝,不能绕过全局不变式。
|
||||
|
||||
@@ -50,7 +50,7 @@ Vitest setup host 使用 `{ enabled: true }` 挂载 `InvariantService` 和全部
|
||||
- **保留生成的仅声明所有权伴随插件。** 不予采纳,因为没有可执行断言的注册无法拒绝损坏的包,也会让完整门禁产生误导。
|
||||
- **为每个包生成一个合成断言。** 不予采纳,因为通用断言仍是在优化如何通过门禁,而不是保护所有者专属契约。
|
||||
- **把逐包契约矩阵移入 `dsh-invariants`。** 不予采纳,因为产品导入、词汇和变更所有权会重新回到中央服务。
|
||||
- **从伴随插件静态导入每个所有者入口。** 不予采纳,因为全局测试 host 会在 hoisted mock 之前预加载包,发布组合也会支付无关模块初始化成本。
|
||||
- **从伴随插件静态导入每个所有者入口。** 不予采纳,因为所有者测试 host 与完整测试 host 会在 hoisted mock 之前预加载包,发布组合也会支付无关模块初始化成本。
|
||||
- **要求第一方服务类身份。** 不予采纳,因为服务接缝是结构化扩展边界;`instanceof` 会拒绝有效的外部实现和测试替身。
|
||||
- **从包根入口隐式注册不变式。** 因包拥有服务 RFC 中的组合顺序与隐藏 effect 问题而不予采纳。
|
||||
|
||||
@@ -58,8 +58,8 @@ Vitest setup host 使用 `{ enabled: true }` 挂载 `InvariantService` 和全部
|
||||
|
||||
- 每个包都贡献可执行检查;新增包若没有检查,会在顶层门禁失败。
|
||||
- 不变式服务保持与产品无关,同时提供可复用的生命周期与形状观察器。
|
||||
- 普通单元、snapshot 与 e2e 测试均全局启用不变式并注册每个伴随插件。
|
||||
- 普通单元、snapshot 与 e2e 根上下文均全局启用不变式并注册当前测试包的伴随插件;一个完整拓扑注册全部伴随插件。
|
||||
- 用于按名称观察的插件名在一个 Cordis 根上下文内必须唯一;安全时包可以选择准确 callback 身份。
|
||||
- 纯包检查对稳定启动契约采样;可变行为必须使用事件、服务或插件 fiber 观察器。
|
||||
- 测试和被选部署会执行更多伴随工作,以少量启动成本换取即时且带包归属的失败。
|
||||
- 包测试和被选部署会执行相关伴随工作,以有界启动成本换取即时且带包归属的失败。
|
||||
- 原有正则选择、blocklist 优先级、注册唯一性、回滚、dispose 与 HMR 契约保持不变。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-19-package-owned-invariant-service.md: 3ffba4ff849b6e4be731586dc7d1b37f4c1a76eb
|
||||
2026-07-19-package-owned-invariant-service.zh.md: d8be195eccb1ab53439b6cdb90b725ae0889c8a6
|
||||
2026-07-19-package-owned-invariant-service.md: 552b088c1cafc2fa762487f57fa1d4ad64390f7e
|
||||
2026-07-19-package-owned-invariant-service.zh.md: 84bdc6a6a3baefe95713f8ce5b8a4e2af49b64eb
|
||||
|
||||
@@ -49,7 +49,7 @@ Blocklist matches override allowlist matches. Each list entry is a case-sensitiv
|
||||
|
||||
The public registration boundary is `ctx.invariants.register(packageName, installer)`. It reserves one active registration per full npm package name even when filters disable installation, and returns the effect disposer. Disposing the companion or service releases the reservation and all contribution state.
|
||||
|
||||
An enabled installer runs in a dedicated child Cordis fiber owned by the service. `InvariantInstaller.inject` declares the child fiber's service surface explicitly; the registry carries no product-specific dependency metadata. The installer receives a bound `fail(message)` reporter. Calling it throws an `Error` subclass named `InvariantError` with stable code `INVARIANT` and the registering `packageName`; it does not extend a product-package error base.
|
||||
An enabled installer runs in a dedicated child Cordis fiber owned by the service. `InvariantInstaller.inject` declares the child fiber's service surface explicitly; the registry carries no product-specific dependency metadata. The service joins a returned installer promise before registration succeeds, so asynchronous startup checks remain transactional. The installer receives a bound `fail(message)` reporter. Calling it throws an `Error` subclass named `InvariantError` with stable code `INVARIANT` and the registering `packageName`; it does not extend a product-package error base.
|
||||
|
||||
Registration setup is transactional. If an installer fails after registering listeners, the child fiber is disposed completely and the name reservation is released before the failure escapes. Filtered registrations create no child but retain their reservation until disposal. Reloading a companion therefore begins with one clean installer state; stateful contributions rebuild baselines from their owning services.
|
||||
|
||||
@@ -84,7 +84,7 @@ Service tests cover defaults, global disablement, allow/block selection, blockli
|
||||
|
||||
Composition tests cover standard-spine forwarding and generated SDK entries. Loader tests preserve each companion namespace, while built plain-Node smokes exercise the compiled subpath exports. The scoped-event freshness gate reruns its semantic Program analysis.
|
||||
|
||||
Every Vitest configuration loads a test host that mounts an explicitly enabled service and all package companions before an ordinary Cordis root's first plugin. Focused service and owner tests construct their own invariant topology so they can exercise disablement, filtering, rollback, and reload without duplicate ownership. Gate tests also execute every companion's `apply` function and verify that it calls `register` with its manifest name, rather than accepting source text alone.
|
||||
Every Vitest configuration loads a test host that mounts an explicitly enabled service before an ordinary Cordis root's first plugin and adds the current test package's companion. One exhaustive topology mounts all package companions once; focused service and owner tests construct their own invariant topology so they can exercise disablement, filtering, rollback, and reload without duplicate ownership. Gate tests also execute every companion's `apply` function and verify that it calls `register` with its manifest name, rather than accepting source text alone.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -101,5 +101,5 @@ Every Vitest configuration loads a test host that mounts an explicitly enabled s
|
||||
- Explicit companion entries make diagnostic cost and ownership visible in Cordis config and package exports.
|
||||
- One selected contribution adds one child fiber and its listener/state cost; filtered registrations retain only name ownership.
|
||||
- Regex sources are deployment configuration and remain fixed until the service reloads.
|
||||
- Ordinary Vitest roots install every selected companion, trading extra child fibers during tests for repository-wide invariant coverage and immediate fixture failures.
|
||||
- Ordinary Vitest roots install the owning test package's selected companion; one exhaustive topology pays the full child-fiber cost once for repository-wide registration coverage.
|
||||
- Session storage validation, snapshotting, freezing, provenance, and surface acceptance remain always on and are not affected by invariant selection.
|
||||
|
||||
@@ -49,7 +49,7 @@ blocklist 匹配优先于 allowlist 匹配。每个条目都是区分大小写
|
||||
|
||||
公开注册边界是 `ctx.invariants.register(packageName, installer)`。即使过滤器禁止安装,它也会为每个完整 npm 包名保留唯一的活跃注册,并返回 effect disposer。卸载伴随插件或服务都会释放注册名及全部贡献状态。
|
||||
|
||||
启用的 installer 在服务拥有的独立 Cordis 子 fiber 中运行。`InvariantInstaller.inject` 显式声明该子 fiber 的服务表面;注册服务不携带产品专用依赖元数据。installer 接收绑定后的 `fail(message)` 报告器。调用它会抛出名为 `InvariantError` 的 `Error` 子类,保留稳定代码 `INVARIANT` 并记录注册方 `packageName`;该错误不继承产品包中的错误基类。
|
||||
启用的 installer 在服务拥有的独立 Cordis 子 fiber 中运行。`InvariantInstaller.inject` 显式声明该子 fiber 的服务表面;注册服务不携带产品专用依赖元数据。服务会在注册成功前等待 installer 返回的 promise,因此异步启动检查仍具有事务性。installer 接收绑定后的 `fail(message)` 报告器。调用它会抛出名为 `InvariantError` 的 `Error` 子类,保留稳定代码 `INVARIANT` 并记录注册方 `packageName`;该错误不继承产品包中的错误基类。
|
||||
|
||||
注册启动是事务性的。如果 installer 在注册监听器后失败,子 fiber 会完整释放,并在失败向外传播前解除包名占用。被过滤的注册不创建子 fiber,但会保留占用直到 dispose。伴随插件重载时总会从干净的 installer 状态开始;有状态贡献从其所属服务重建基线。
|
||||
|
||||
@@ -84,7 +84,7 @@ Workspace 约束识别独立的不变式 bundle;包 exports、项目引用、
|
||||
|
||||
组合测试覆盖标准 spine 转发和生成的 SDK 条目。Loader 测试固定每个伴随命名空间,构建后的纯 Node smoke 覆盖编译子路径 export。scoped event 新鲜度门禁会重新执行语义 Program 分析。
|
||||
|
||||
每个 Vitest 配置都会加载测试宿主;在普通 Cordis 根上下文启动第一个插件之前,宿主会挂载显式启用的服务以及所有包的伴随插件。服务与所有者的聚焦测试自行构建不变式拓扑,从而在不发生重复所有权冲突的前提下覆盖关闭、过滤、回滚与重载。门禁测试还会执行每个伴随插件的 `apply` 函数,并验证它调用 `register` 时使用包清单中的包名,而不是只检查源码文本。
|
||||
每个 Vitest 配置都会加载测试宿主;在普通 Cordis 根上下文启动第一个插件之前,宿主会挂载显式启用的服务,并添加当前测试包的伴随插件。一个完整拓扑会一次挂载所有包的伴随插件;服务与所有者的聚焦测试自行构建不变式拓扑,从而在不发生重复所有权冲突的前提下覆盖关闭、过滤、回滚与重载。门禁测试还会执行每个伴随插件的 `apply` 函数,并验证它调用 `register` 时使用包清单中的包名,而不是只检查源码文本。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -101,5 +101,5 @@ Workspace 约束识别独立的不变式 bundle;包 exports、项目引用、
|
||||
- 显式伴随条目让诊断成本和所有权在 Cordis 配置与包 export 中可见。
|
||||
- 每个选中贡献增加一个子 fiber 及其监听器和状态成本;被过滤注册只保留包名占用。
|
||||
- 正则表达式源属于部署配置,在服务重载前保持固定。
|
||||
- 普通 Vitest 根上下文会安装每个被选中的伴随插件,以增加测试期间的子 fiber 为代价,换取覆盖整个仓库的不变式检查和对 fixture(测试前置数据)错误的即时反馈。
|
||||
- 普通 Vitest 根上下文会安装当前测试包中被选中的伴随插件;一个完整拓扑只支付一次全部子 fiber 成本,用于覆盖整个仓库的注册。
|
||||
- 会话存储验证、快照、冻结、provenance 与 surface 接受规则始终启用,不受不变式选择影响。
|
||||
|
||||
Reference in New Issue
Block a user