7.7 KiB
Agent Note: Executable package invariant contracts
Status: implemented
English | 中文
Problem
The package-owned invariant seam made registration and publication exhaustive, but its generated baseline treated package-name ownership as sufficient. An empty installer could satisfy the repository gate while observing no runtime state and rejecting no invalid state. That made the exhaustive count a wiring claim rather than protection for the package contract.
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 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
Every companion executes a package contract
Every workspace package keeps its separately published ./invariant companion and exact npm-name registration, but the installer must execute at least one package-specific check through the bound fail(message) reporter. The ownership-baseline generator and its root script entry are removed; generated markers, empty installers, and installers that never reference the reporter are repository errors.
The implemented contracts use four forms:
| Owner shape | Runtime contract |
|---|---|
| Stateful session, agent, scope, and agent-loop owners | Validate event ordering, enclosure, status transitions, scoped subjects, and reconstructable model requests. |
| Cordis plugin owners | Validate the plugin's own declared runtime name, required injections, owned effects, provided services, and package-specific all-or-none or config-dependent relations. |
| Cordis service seams | Validate the structural method and descriptor surface of current and future implementations. |
| Pure libraries, bins, and support packages | Validate stable parser mapping, protocol precedence, retention and timeout algebra, path resolution, normalization, environment scrubbing, or deliberately empty runtime entrypoints. |
At implementation time this covers all 91 workspace packages: four stateful companions, 62 plugin-fiber companions, eight service-shape companions, and 17 pure/bin/support companions.
Product-independent observers
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 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 } 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.
Alternatives considered
- 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 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;
instanceofwould 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.
Consequences
- 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 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.
- 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.