From 941b0411d80fba789bdd237946d406593ee13df9 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 20 Jul 2026 00:38:37 +0800 Subject: [PATCH] feat(invariants): implement package runtime checks --- docs/config-catalog.md | 2 +- docs/cordis-catalog/services.md | 2 +- docs/event-producer-consumer.md | 4 +- docs/rfc/INDEX.md | 1 + ...kage-invariant-runtime-contracts.i18n.yaml | 6 + ...-19-package-invariant-runtime-contracts.md | 65 +++++ ...-package-invariant-runtime-contracts.zh.md | 65 +++++ ...-package-owned-invariant-service.i18n.yaml | 4 +- ...6-07-19-package-owned-invariant-service.md | 8 +- ...7-19-package-owned-invariant-service.zh.md | 8 +- package.json | 3 +- packages/AGENTS.md | 2 +- packages/bash/bash-local/src/invariant.ts | 27 ++- packages/bash/bash-sandbox/src/invariant.ts | 29 ++- packages/bash/bash/src/invariant.ts | 20 +- packages/bash/tool-bash/src/invariant.ts | 33 ++- .../code-runtime-worker/src/invariant.ts | 24 +- .../code-runtime/src/invariant.ts | 28 ++- .../code-runtime/tests/service.spec.ts | 14 ++ .../compact/compact-basic/src/invariant.ts | 45 +++- .../compact-basic/tests/compact-basic.spec.ts | 25 +- packages/compact/compact/src/invariant.ts | 20 +- .../context/time-context/src/invariant.ts | 26 +- .../workspace-context/src/invariant.ts | 26 +- packages/cordis/tool-cordis/src/invariant.ts | 27 ++- packages/core/system-prompt/src/invariant.ts | 27 ++- packages/core/tools/src/invariant.ts | 30 ++- packages/examples/acp-demo/src/invariant.ts | 23 +- .../examples/acp-demo/tests/acp-agent.spec.ts | 9 +- .../agent-spine-demo/src/invariant.ts | 23 +- .../agent-spine-demo/tests/agent-core.spec.ts | 9 +- packages/examples/cli-demo/src/invariant.ts | 23 +- .../examples/cli-demo/tests/cli-demo.spec.ts | 9 +- .../examples/jsonrpc-demo/src/invariant.ts | 21 +- packages/examples/stdio-demo/src/invariant.ts | 23 +- .../stdio-demo/tests/stdio-agent.spec.ts | 9 +- packages/fs/fs-local/src/invariant.ts | 26 +- packages/fs/fs-policy/src/invariant.ts | 25 +- packages/fs/fs/src/invariant.ts | 20 +- packages/fs/tool-fs-search/src/invariant.ts | 28 ++- packages/fs/tool-fs/src/invariant.ts | 28 ++- .../guard/repeat-tool-guard/src/invariant.ts | 24 +- packages/hooks/hook-protocol/src/invariant.ts | 30 ++- packages/hooks/hooks-claude/src/invariant.ts | 39 ++- .../hooks-claude/tests/invariant.spec.ts | 26 ++ packages/hooks/hooks-codex/src/invariant.ts | 37 ++- .../hooks/hooks-codex/tests/invariant.spec.ts | 26 ++ packages/llm/llm-deepseek/src/invariant.ts | 26 +- packages/llm/llm-pi-ai/src/invariant.ts | 26 +- packages/llm/llm/src/invariant.ts | 26 +- packages/llm/token-meter/src/invariant.ts | 27 ++- packages/mcp/mcp-client/src/invariant.ts | 27 ++- .../sandbox/sandbox-local/src/invariant.ts | 26 +- packages/sandbox/sandbox/src/invariant.ts | 20 +- packages/sdk/create-sdk/src/invariant.ts | 32 ++- packages/sdk/helper/src/invariant.ts | 28 ++- packages/sdk/scripts/src/args.ts | 5 +- packages/sdk/scripts/src/forwarding.ts | 21 ++ packages/sdk/scripts/src/invariant.ts | 30 ++- packages/sdk/scripts/tests/scripts.spec.ts | 1 + .../src/invariant.ts | 26 +- .../src/invariant.ts | 26 +- .../session-persistence/src/invariant.ts | 17 +- .../session-query/src/invariant.ts | 29 ++- packages/skill/skill-local/src/invariant.ts | 26 +- packages/skill/skill/src/invariant.ts | 26 +- packages/skill/tool-skill/src/invariant.ts | 28 ++- packages/spill/spill-local/src/invariant.ts | 26 +- packages/spill/spill-policy/src/invariant.ts | 30 ++- .../spill-policy/tests/spill-policy.spec.ts | 11 + packages/spill/spill/src/invariant.ts | 20 +- .../subagent/subagent-acp/src/invariant.ts | 26 +- .../subagent/subagent-fork/src/invariant.ts | 26 +- .../subagent-inprocess/src/invariant.ts | 23 +- .../src/structured-protocol.ts | 10 + .../subagent-inprocess/src/structured.ts | 14 +- .../subagent/subagent-spawn/src/invariant.ts | 26 +- .../subagent-subprocess/src/invariant.ts | 28 ++- packages/subagent/subagent/src/invariant.ts | 26 +- .../subagent/tool-subagent/src/invariant.ts | 28 ++- .../support/acp-snapshot/src/invariant.ts | 33 ++- .../support/agent-loop-testkit/src/index.ts | 18 +- .../agent-loop-testkit/src/invariant.ts | 24 +- packages/support/invariants/README.md | 19 +- packages/support/invariants/src/index.ts | 176 +++++++++++++- packages/support/invariants/src/invariant.ts | 27 ++- .../support/invariants/tests/service.spec.ts | 222 +++++++++++++++++- packages/support/llm-replay/src/invariant.ts | 29 ++- .../support/loader-smoke/src/invariant.ts | 31 ++- packages/tasks/tasks/src/invariant.ts | 27 ++- packages/tasks/tool-tasks/src/invariant.ts | 28 ++- .../timeout/timeout-policy/src/invariant.ts | 26 +- packages/todo/tool-todo/src/invariant.ts | 26 +- packages/ui/acp/src/invariant.ts | 33 ++- packages/ui/app-boot/src/config-path.ts | 23 ++ packages/ui/app-boot/src/index.ts | 21 +- packages/ui/app-boot/src/invariant.ts | 27 ++- packages/ui/jsonrpc/src/invariant.ts | 26 +- packages/ui/permission/src/invariant.ts | 30 ++- .../ui/permission/tests/permission.spec.ts | 7 +- packages/ui/stdio/src/invariant.ts | 28 ++- packages/ui/tool-ask-user/src/invariant.ts | 27 ++- packages/ui/tui/src/invariant.ts | 29 ++- packages/ui/tui/tests/tui.snapshot.ts | 2 +- packages/ui/user-approval/src/invariant.ts | 27 ++- packages/ui/user-interaction/src/invariant.ts | 26 +- packages/util/brand/src/invariant.ts | 21 +- packages/util/home/src/invariant.ts | 26 +- packages/util/paths/src/invariant.ts | 27 ++- packages/util/retention/src/invariant.ts | 32 ++- packages/util/timeout/src/invariant.ts | 27 ++- packages/web/tool-web/src/invariant.ts | 28 ++- packages/web/web-fetch-local/src/invariant.ts | 26 +- .../web/web-search-deepseek/src/invariant.ts | 23 +- packages/web/web-search-exa/src/invariant.ts | 26 +- .../web-search-perplexity/src/invariant.ts | 23 +- packages/web/web/src/invariant.ts | 26 +- .../workflow/tool-workflow/src/invariant.ts | 28 ++- .../workflow-workerthread/src/invariant.ts | 26 +- packages/workflow/workflow/src/invariant.ts | 20 +- scripts/gen-package-invariants.ts | 44 ---- scripts/package-invariants.spec.ts | 112 ++++++++- scripts/package-invariants.ts | 150 ++++++++---- scripts/verify-package-invariants.ts | 21 ++ website/zh-CN/api/harness/invariants.md | 4 +- 125 files changed, 2317 insertions(+), 1161 deletions(-) create mode 100644 docs/rfc/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml create mode 100644 docs/rfc/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md create mode 100644 docs/rfc/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md create mode 100644 packages/hooks/hooks-claude/tests/invariant.spec.ts create mode 100644 packages/hooks/hooks-codex/tests/invariant.spec.ts create mode 100644 packages/sdk/scripts/src/forwarding.ts create mode 100644 packages/subagent/subagent-inprocess/src/structured-protocol.ts create mode 100644 packages/ui/app-boot/src/config-path.ts delete mode 100644 scripts/gen-package-invariants.ts create mode 100644 scripts/verify-package-invariants.ts diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 4ea9c7a300..76663117b8 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -396,7 +396,7 @@ export interface Config { } ``` -Source: [`packages/support/invariants/src/index.ts:16`](../packages/support/invariants/src/index.ts) +Source: [`packages/support/invariants/src/index.ts:15`](../packages/support/invariants/src/index.ts) ## `@deepseek-ai/dsh-jsonrpc` diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index ca5eb4232b..7da704df75 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -495,7 +495,7 @@ Package-owned invariant registry with global and regex-based selection. register(packageName: string, installer: InvariantInstaller): () => void ``` -Source: [`packages/support/invariants/src/index.ts:95`](../../packages/support/invariants/src/index.ts) +Source: [`packages/support/invariants/src/index.ts:261`](../../packages/support/invariants/src/index.ts) ## `ctx.llm` — `LlmService` diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 29f8763460..b1177e74a5 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -55,6 +55,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event string | Dispatchers | Listeners | | --- | --- | --- | | `internal/dispatch` | - | [`scope`](../packages/core/scope), [`session`](../packages/core/session) | -| `internal/status` | - | [`agent`](../packages/core/agent) | +| `internal/plugin` | - | [`invariants`](../packages/support/invariants) | +| `internal/service` | - | [`invariants`](../packages/support/invariants) | +| `internal/status` | - | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants) | Maintenance mode: generated: Cordis event declarations and producer/listener edges are resolved from the repository TypeScript Program. diff --git a/docs/rfc/INDEX.md b/docs/rfc/INDEX.md index c7e542276e..e4ee5fab8d 100644 --- a/docs/rfc/INDEX.md +++ b/docs/rfc/INDEX.md @@ -167,6 +167,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; | [Initiating Agent scope over AsyncLocalStorage](implemented/architecture/2026-07-15-agent-initiator-scope.md) | 2026-07-15 | | [Advisory LLM catalogs and per-session ACP model selection](implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md) | 2026-07-15 | | [Replay token meter service](implemented/architecture/2026-07-15-replay-token-meter-service.md) | 2026-07-15 | +| [Executable package invariant contracts](implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md) | 2026-07-19 | | [Package-owned invariant service seam](implemented/architecture/2026-07-19-package-owned-invariant-service.md) | 2026-07-19 | ### Process diff --git a/docs/rfc/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml new file mode 100644 index 0000000000..f234facc6e --- /dev/null +++ b/docs/rfc/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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: 57a768f2e3cc954d02f74a9dca46680e59a06403 +2026-07-19-package-invariant-runtime-contracts.zh.md: 7a289da17414cc1fcac3109b4a96b2f047b16a89 diff --git a/docs/rfc/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md b/docs/rfc/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md new file mode 100644 index 0000000000..57a768f2e3 --- /dev/null +++ b/docs/rfc/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md @@ -0,0 +1,65 @@ +# RFC: Executable package invariant contracts + +Status: implemented + +English | [中文](2026-07-19-package-invariant-runtime-contracts.zh.md) + +## 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 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. + +## 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 90 workspace packages: four stateful companions, 62 plugin-fiber companions, eight service-shape companions, and 16 pure/bin/support companions. + +### 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. + +`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. + +### 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 90 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 four stateful-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 the global test host 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. + +## 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 tests run with global invariant enablement and every companion registered. +- 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. +- The original regex selection, blocklist precedence, registration uniqueness, rollback, disposal, and HMR contracts remain unchanged. diff --git a/docs/rfc/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md b/docs/rfc/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md new file mode 100644 index 0000000000..7a289da174 --- /dev/null +++ b/docs/rfc/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md @@ -0,0 +1,65 @@ +# RFC: 可执行的包不变式契约 + +Status: implemented + +[English](2026-07-19-package-invariant-runtime-contracts.md) | 中文 + +## 问题 + +包拥有的不变式接缝让注册与发布覆盖完整,但生成的基线把包名所有权视为充分条件。空 installer 可以通过仓库门禁,却不观察任何运行时状态,也不拒绝任何无效状态。这样一来,完整计数只能证明接线存在,不能保护包契约。 + +不同包形态不能使用同一种不变式。Cordis 插件拥有 fiber、注入、effect 与服务;服务接缝允许结构兼容的第三方实现;有状态领域需要事件关系;纯库和 bin 包暴露代数、解析、规范化或入口约束。仓库需要一个可执行的统一义务,同时不能把这些契约重新移回了解产品语义的中央包。 + +Vitest 还会全局挂载每个伴随插件。因此伴随模块不能在测试模块建立 hoisted mock 之前急切导入所有产品入口;按名称观察时,也不能把继承父级显示名的匿名子 fiber 误认为包插件本身。 + +## 决策 + +### 每个伴随插件都执行包契约 + +每个工作区包保留独立发布的 `./invariant` 伴随插件和准确 npm 包名注册,但 installer 必须通过绑定的 `fail(message)` 报告器执行至少一个包专属检查。删除所有权基线生成器及其根脚本入口;生成标记、空 installer 和从不引用报告器的 installer 都属于仓库错误。 + +实现后的契约采用四种形态: + +| 所有者形态 | 运行时契约 | +|---|---| +| 有状态的 session、agent、scope 与 agent-loop 所有者 | 验证事件顺序、包围关系、状态转换、作用域主体和可重建的模型请求。 | +| Cordis 插件所有者 | 验证插件自身声明的运行时名称、必要注入、拥有的 effect、提供的服务,以及包专属的全有或全无关系或配置依赖关系。 | +| Cordis 服务接缝 | 验证当前和未来实现的结构化方法与描述字段表面。 | +| 纯库、bin 与支持包 | 验证稳定的解析映射、协议优先级、保留与超时代数、路径解析、规范化、环境清理或刻意为空的运行时入口。 | + +实现时覆盖全部 90 个工作区包:四个有状态伴随插件、62 个插件 fiber 伴随插件、八个服务形状伴随插件和 16 个纯库、bin 或支持包伴随插件。 + +### 与产品无关的观察器 + +`observePluginInvariant` 会立即检查已有 fiber,并通过全局 Cordis 生命周期事件检查未来进入活跃状态的 fiber。安全导入时,契约可以提供准确 callback;否则匹配 `fiber.runtime.name`,即该 fiber 自身插件运行时声明的名称,而不是继承而来的 `fiber.name`,因此匿名 `ctx.inject()` 子级不会被误认成父包。观察器检查必要注入键、递归收集的 effect 标签、由该 fiber 准确提供的服务,以及可选的所有者验证器。依赖配置的包使用对称关系,例如自动压缩要么同时拥有两个监听器,要么在关闭时两个都没有。 + +`observeServiceInvariant` 检查当前服务及之后的每次绑定。`serviceShapeViolation` 以结构方式验证可调用成员和非空字符串描述字段,而不使用 `instanceof`;因此符合契约的第三方后端和完整测试替身有效,不完整替身会失败。 + +`assertInvariant` 处理同步包代数。纯包伴随插件注册异步子 effect,并在该 effect 内动态导入所有者。这样既保留服务拥有的原子回滚,又允许测试模块、Loader 或部署先建立 mock 和模块解析,再由不变式采样所有者。 + +### 门禁与测试执行 + +`verify-package-invariants` 发现每个工作区包,并保留准确注册名、`./invariant` export、发布文件、不变式 peer 与开发依赖、TypeScript 引用和 bundle 入口的发布检查。源码检查还会解析本地 `install` 函数,拒绝生成标记或空函数体,要求第二个失败报告器参数及其使用,并拒绝跨包重复的按名称插件观察器。这些 AST 检查只是最低接收规则,并不宣称源码形状足以证明语义质量。 + +Vitest setup host 使用 `{ enabled: true }` 挂载 `InvariantService` 和全部 90 个伴随插件,然后才启动普通 Cordis 根上下文的第一个插件。host 会把伴随插件启动加入测试的根级组合边界,因此异步纯检查和插件观察器安装会让测试失败,而不会变成后台诊断。选择、生命周期和四个有状态所有者的聚焦套件自行构建启用的不变式拓扑,在避免重复注册的同时继续测试不变式。 + +辅助测试会拒绝错误插件名、缺失注入、effect、服务或自定义关系、错误服务形状和失败断言。随后,包套件在已有配置与 HMR 路径上激活真实插件。测试专用服务替身必须实现完整的已检查接缝,不能绕过全局不变式。 + +## 考虑过的替代方案 + +- **保留生成的仅声明所有权伴随插件。** 不予采纳,因为没有可执行断言的注册无法拒绝损坏的包,也会让完整门禁产生误导。 +- **为每个包生成一个合成断言。** 不予采纳,因为通用断言仍是在优化如何通过门禁,而不是保护所有者专属契约。 +- **把逐包契约矩阵移入 `dsh-invariants`。** 不予采纳,因为产品导入、词汇和变更所有权会重新回到中央服务。 +- **从伴随插件静态导入每个所有者入口。** 不予采纳,因为全局测试 host 会在 hoisted mock 之前预加载包,发布组合也会支付无关模块初始化成本。 +- **要求第一方服务类身份。** 不予采纳,因为服务接缝是结构化扩展边界;`instanceof` 会拒绝有效的外部实现和测试替身。 +- **从包根入口隐式注册不变式。** 因包拥有服务 RFC 中的组合顺序与隐藏 effect 问题而不予采纳。 + +## 后果 + +- 每个包都贡献可执行检查;新增包若没有检查,会在顶层门禁失败。 +- 不变式服务保持与产品无关,同时提供可复用的生命周期与形状观察器。 +- 普通单元、snapshot 与 e2e 测试均全局启用不变式并注册每个伴随插件。 +- 用于按名称观察的插件名在一个 Cordis 根上下文内必须唯一;安全时包可以选择准确 callback 身份。 +- 纯包检查对稳定启动契约采样;可变行为必须使用事件、服务或插件 fiber 观察器。 +- 测试和被选部署会执行更多伴随工作,以少量启动成本换取即时且带包归属的失败。 +- 原有正则选择、blocklist 优先级、注册唯一性、回滚、dispose 与 HMR 契约保持不变。 diff --git a/docs/rfc/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml index dfe64b7110..c3d4aa812a 100644 --- a/docs/rfc/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml @@ -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: bf27bb4e951988bc124509b49dea9b5509f8e6f3 -2026-07-19-package-owned-invariant-service.zh.md: c9dfbb65bf4fd497f2f592f7741923dcef3e51fb +2026-07-19-package-owned-invariant-service.md: 32a5a0798c4121be01ae1a51ddb4e864a2663de5 +2026-07-19-package-owned-invariant-service.zh.md: 9ebeda7575452fd29245bffb1354e56e596f711e diff --git a/docs/rfc/implemented/architecture/2026-07-19-package-owned-invariant-service.md b/docs/rfc/implemented/architecture/2026-07-19-package-owned-invariant-service.md index bf27bb4e95..32a5a0798c 100644 --- a/docs/rfc/implemented/architecture/2026-07-19-package-owned-invariant-service.md +++ b/docs/rfc/implemented/architecture/2026-07-19-package-owned-invariant-service.md @@ -18,7 +18,7 @@ Package ownership must also be exhaustive. Without a mechanical repository rule, `@deepseek-ai/dsh-invariants` is a product-independent Cordis service plugin that registers `ctx.invariants`. It owns configuration, registration uniqueness, child-fiber lifecycle, and package-attributed failures. It imports no session, agent, scope, or agent-loop package and contains none of their checks. -Every workspace package publishes a `./invariant` companion plugin that registers its exact full npm name. A package with no relational check uses a generated ownership-only installer: it reserves the name through the real service boundary but installs no listeners. Package root entrypoints do not import or register diagnostics implicitly, so loading a root package does not change runtime checking or require the invariant service. +Every workspace package publishes a `./invariant` companion plugin that registers its exact full npm name and installs an executable package-specific contract. Generated ownership-only installers are forbidden by the follow-up [runtime-contract RFC](2026-07-19-package-invariant-runtime-contracts.md). Package root entrypoints do not import or register diagnostics implicitly, so loading a root package does not change runtime checking or require the invariant service. ### Configuration and selection @@ -64,9 +64,9 @@ The former functional-plugin entrypoint and one-argument `InvariantError` constr | `@deepseek-ai/dsh-scope/invariant` | `@deepseek-ai/dsh-scope` | scoped-event carrier presence and subject consistency | | `@deepseek-ai/dsh-agent-loop/invariant` | `@deepseek-ai/dsh-agent-loop` | model-request reconstruction | -These four owners contain stateful checks and focused tests. Every other package carries a generated baseline companion until it gains a relational assertion. Every companion is a separately bundled `./invariant` export with its own declarations and Loader-safe namespace plugin shape; the service package's own companion imports its local service type to avoid a self-dependency. +These four owners contain stateful checks and focused tests. Other owners check their plugin fibers and effects, structural service implementations, or stable pure-library algebra. Every companion is a separately bundled `./invariant` export with its own declarations and Loader-safe namespace plugin shape; the service package's own companion imports its local service type to avoid a self-dependency. -`verify-package-invariants` discovers every workspace package and rejects missing or stale companion source, foreign or unresolved registration names, missing `./invariant` exports or published files, missing invariant peer/development dependencies and project references, and bundle overrides that omit the companion entry. The generator writes only missing or marked ownership baselines, so a package-owned implementation is never replaced. +`verify-package-invariants` discovers every workspace package and rejects missing companion source, generated markers, empty or reporter-free installers, foreign or unresolved registration names, missing `./invariant` exports or published files, missing invariant peer/development dependencies and project references, and bundle overrides that omit the companion entry. ### Scoped-event semantic map @@ -96,7 +96,7 @@ Every Vitest configuration loads a test host that mounts an explicitly enabled s ## Consequences - Product packages own and test their relational assertions while the service stays product-independent. -- Every package pays the small publication and dependency cost of an invariant companion, including packages whose generated baseline currently installs no listeners. +- Every package pays the publication, dependency, and runtime-check cost of an executable invariant companion. - Standard compositions can disable all checks or select package names without changing their plugin tree. - 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. diff --git a/docs/rfc/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md b/docs/rfc/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md index c9dfbb65bf..9ebeda7575 100644 --- a/docs/rfc/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md +++ b/docs/rfc/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md @@ -18,7 +18,7 @@ Status: implemented `@deepseek-ai/dsh-invariants` 是与产品无关的 Cordis 服务插件,注册 `ctx.invariants`。它只负责配置、注册唯一性、子 fiber 生命周期和带包归属的失败;不导入 session、agent、scope 或 agent-loop 包,也不包含这些包的检查。 -工作区内的每个包都发布 `./invariant` 伴随插件,并注册自己完整且准确的 npm 包名。没有关系检查的包使用生成的仅声明所有权 installer:它通过真实服务边界占用包名,但不安装监听器。包的根入口不会隐式导入或注册诊断,因此加载根包不会改变运行时检查,也不要求不变式服务存在。 +工作区内的每个包都发布 `./invariant` 伴随插件,注册自己完整且准确的 npm 包名,并安装可执行的包专属契约。后续的[运行时契约 RFC](2026-07-19-package-invariant-runtime-contracts.md) 禁止生成的仅声明所有权 installer。包的根入口不会隐式导入或注册诊断,因此加载根包不会改变运行时检查,也不要求不变式服务存在。 ### 配置与选择 @@ -64,9 +64,9 @@ blocklist 匹配优先于 allowlist 匹配。每个条目都是区分大小写 | `@deepseek-ai/dsh-scope/invariant` | `@deepseek-ai/dsh-scope` | scoped event carrier 存在性与主体一致性 | | `@deepseek-ai/dsh-agent-loop/invariant` | `@deepseek-ai/dsh-agent-loop` | 模型请求重建 | -这四个所有者保存有状态检查与聚焦测试。其他每个包在获得关系断言之前,都带有生成的基线伴随插件。每个伴随入口都是单独打包的 `./invariant` export,具有独立声明和对 Loader 安全的命名空间插件形态;服务包自身的伴随插件导入本地服务类型,避免形成自依赖。 +这四个所有者保存有状态检查与聚焦测试。其他所有者检查自己的插件 fiber 与 effect、结构化服务实现或稳定的纯库代数。每个伴随入口都是单独打包的 `./invariant` export,具有独立声明和对 Loader 安全的命名空间插件形态;服务包自身的伴随插件导入本地服务类型,避免形成自依赖。 -`verify-package-invariants` 会发现每个工作区包,并拒绝缺失或陈旧的伴随插件源码、外部或无法解析的注册名、缺失的 `./invariant` export 或发布文件、缺失的不变式对等依赖(peer dependency)、开发依赖及项目引用,以及遗漏伴随入口的自定义构建配置。生成器只写入缺失或带生成标记的所有权基线,因此绝不会替换包自行维护的实现。 +`verify-package-invariants` 会发现每个工作区包,并拒绝缺失的伴随插件源码、生成标记、空 installer、不使用失败报告器的 installer、外部或无法解析的注册名、缺失的 `./invariant` export 或发布文件、缺失的不变式对等依赖(peer dependency)、开发依赖及项目引用,以及遗漏伴随入口的自定义构建配置。 ### Scoped event 语义映射 @@ -96,7 +96,7 @@ Workspace 约束识别独立的不变式 bundle;包 exports、项目引用、 ## 后果 - 产品包拥有并测试自己的关系断言,服务保持与产品无关。 -- 每个包都要承担不变式伴随插件带来的少量发布与依赖成本,包括目前只安装生成基线、不添加监听器的包。 +- 每个包都要承担可执行不变式伴随插件带来的发布、依赖与运行时检查成本。 - 标准组合无需改变插件树即可关闭全部检查或按包名选择。 - 显式伴随条目让诊断成本和所有权在 Cordis 配置与包 export 中可见。 - 每个选中贡献增加一个子 fiber 及其监听器和状态成本;被过滤注册只保留包名占用。 diff --git a/package.json b/package.json index abf1e59178..f62e92f754 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,7 @@ "verify-md-links": "tsx scripts/verify-md-links.ts", "verify-doc-refs": "tsx scripts/verify-doc-refs.ts", "verify-package-paths": "tsx scripts/verify-package-paths.ts", - "gen-package-invariants": "tsx scripts/gen-package-invariants.ts", - "verify-package-invariants": "tsx scripts/gen-package-invariants.ts --check", + "verify-package-invariants": "tsx scripts/verify-package-invariants.ts", "verify-package-readme-model-experience": "tsx scripts/verify-package-readme-model-experience.ts", "verify-mermaid": "tsx scripts/verify-mermaid.ts", "verify-rfc-classification": "tsx scripts/verify-rfc-classification.ts", diff --git a/packages/AGENTS.md b/packages/AGENTS.md index 1735c44978..b5fcf47273 100644 --- a/packages/AGENTS.md +++ b/packages/AGENTS.md @@ -16,7 +16,7 @@ These package-specific rules supplement the repo-wide [conventions](../AGENTS.md - **Publish state only at its commit point.** Emit each notification and update derived state only after the success boundary that makes it true; derive caches, prompts, UI echoes, replay, and query views from one authoritative source. - **Apply bounds to the complete result.** Enforce byte, token, item, and time limits where the complete emitted or retained value, including wrappers and metadata, is known; test tiny and exact limits, oversized single chunks, and multibyte byte limits. - **Registry contributions prove disposal.** Add the HMR-safety test required by the [testing policy](../docs/testing.md): dispose the contributing fiber and observe removal. -- **Every package owns an invariant companion.** Publish `./invariant`, register the manifest's exact npm name, and keep the generated ownership baseline until relational checks exist. `verify-package-invariants` gates source and publication wiring ([rationale](../docs/rfc/implemented/architecture/2026-07-19-package-owned-invariant-service.md)). +- **Every package owns executable invariants.** Publish `./invariant`, register its exact name, and enforce a package runtime contract with the bound reporter; generated, empty, and reporter-free installers fail `verify-package-invariants` ([rationale](../docs/rfc/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md)). Naming notes: diff --git a/packages/bash/bash-local/src/invariant.ts b/packages/bash/bash-local/src/invariant.ts index 8af5f91b22..397bf0d6dc 100644 --- a/packages/bash/bash-local/src/invariant.ts +++ b/packages/bash/bash-local/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-bash-local`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-bash-local/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-bash-local`. @module @deepseek-ai/dsh-bash-local/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-bash-local' @@ -17,8 +10,19 @@ export const name = 'bash-local-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'LocalBashExecutor', + effects: [ + 'ctx.provide("bash")', + 'local bash teardown', + ], + services: [ + 'bash', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +31,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/bash/bash-sandbox/src/invariant.ts b/packages/bash/bash-sandbox/src/invariant.ts index e74190fa82..f758c90f6f 100644 --- a/packages/bash/bash-sandbox/src/invariant.ts +++ b/packages/bash/bash-sandbox/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-bash-sandbox`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-bash-sandbox/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-bash-sandbox`. @module @deepseek-ai/dsh-bash-sandbox/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-bash-sandbox' @@ -17,8 +10,21 @@ export const name = 'bash-sandbox-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'SandboxBashExecutor', + inject: [ + 'sandbox', + ], + effects: [ + 'ctx.provide("bash")', + ], + services: [ + 'bash', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +33,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/bash/bash/src/invariant.ts b/packages/bash/bash/src/invariant.ts index 350f68bfc7..91f135b1bb 100644 --- a/packages/bash/bash/src/invariant.ts +++ b/packages/bash/bash/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-bash`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-bash/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-bash`. @module @deepseek-ai/dsh-bash/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-bash' @@ -17,8 +10,12 @@ export const name = 'bash-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Validate every implementation bound to this package's service seam. */ +const install: InvariantInstaller = (ctx, fail) => { + observeServiceInvariant(ctx, fail, 'bash', value => serviceShapeViolation(value, { + methods: ['resolve', 'run', 'start'], + })) +} /** * Register this package's invariant companion. @@ -27,4 +24,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/bash/tool-bash/src/invariant.ts b/packages/bash/tool-bash/src/invariant.ts index 286089c7ff..ca63f9ff77 100644 --- a/packages/bash/tool-bash/src/invariant.ts +++ b/packages/bash/tool-bash/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-tool-bash`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-tool-bash/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-bash`. @module @deepseek-ai/dsh-tool-bash/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-bash' @@ -17,8 +10,25 @@ export const name = 'tool-bash-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'tool-bash', + inject: [ + 'tools', + 'bash', + 'systemPrompt', + ], + effects: [ + 'ctx.provide("bashEnv")', + 'bashEnv.register()', + 'tools.register()', + ], + services: [ + 'bashEnv', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +37,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/code-runtime/code-runtime-worker/src/invariant.ts b/packages/code-runtime/code-runtime-worker/src/invariant.ts index 41b3eab511..63daf58ae9 100644 --- a/packages/code-runtime/code-runtime-worker/src/invariant.ts +++ b/packages/code-runtime/code-runtime-worker/src/invariant.ts @@ -1,14 +1,10 @@ /** - * Generated invariant ownership companion for `@deepseek-ai/dsh-code-runtime-worker`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts + * Package-owned runtime contract checks for `@deepseek-ai/dsh-code-runtime-worker`. * @module @deepseek-ai/dsh-code-runtime-worker/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-code-runtime-worker' @@ -17,8 +13,19 @@ export const name = 'code-runtime-worker-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'WorkerCodeRuntime', + effects: [ + 'ctx.provide("codeRuntime")', + 'worker code-runtime teardown', + ], + services: [ + 'codeRuntime', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +34,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/code-runtime/code-runtime/src/invariant.ts b/packages/code-runtime/code-runtime/src/invariant.ts index 6102927d77..1ee564c60b 100644 --- a/packages/code-runtime/code-runtime/src/invariant.ts +++ b/packages/code-runtime/code-runtime/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-code-runtime`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-code-runtime/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-code-runtime`. @module @deepseek-ai/dsh-code-runtime/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-code-runtime' @@ -17,8 +10,20 @@ export const name = 'code-runtime-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Validate every implementation bound to this package's service seam. */ +const install: InvariantInstaller = (ctx, fail) => { + observeServiceInvariant(ctx, fail, 'codeRuntime', (value) => { + const violation = serviceShapeViolation(value, { + methods: ['run'], + stringProperties: ['language', 'isolation'], + }) + if (violation !== undefined) return violation + const service = value as { language: string; isolation: string } + return /^[a-z][a-z0-9-]*$/.test(service.language) && /^[a-z][a-z0-9-]*$/.test(service.isolation) + ? undefined + : 'code runtime language and isolation must be lowercase identifiers' + }) +} /** * Register this package's invariant companion. @@ -27,4 +32,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/code-runtime/code-runtime/tests/service.spec.ts b/packages/code-runtime/code-runtime/tests/service.spec.ts index 7811ef0531..01736ea0ab 100644 --- a/packages/code-runtime/code-runtime/tests/service.spec.ts +++ b/packages/code-runtime/code-runtime/tests/service.spec.ts @@ -84,4 +84,18 @@ describe('CodeRuntime service seam', () => { const { ctx } = await setup() await expect(ctx.plugin(StubRuntime)).rejects.toThrow(/registered/) }) + + it.each([ + [{ language: 'typescript', isolation: 'worker' }, /must expose method "run"/], + [{ language: 'TypeScript', isolation: 'worker', run() {} }, /must be lowercase identifiers/], + ])('rejects an invalid runtime implementation through the package invariant', async (value, message) => { + const ctx = new Context() + const invalidRuntime = { + name: 'invalid-code-runtime', + apply(child: Context) { + child.provide('codeRuntime', value as unknown as CodeRuntime) + }, + } + await expect(ctx.plugin(invalidRuntime)).rejects.toThrow(message) + }) }) diff --git a/packages/compact/compact-basic/src/invariant.ts b/packages/compact/compact-basic/src/invariant.ts index f72c0e5898..3e034a2f58 100644 --- a/packages/compact/compact-basic/src/invariant.ts +++ b/packages/compact/compact-basic/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-compact-basic`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-compact-basic/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-compact-basic`. @module @deepseek-ai/dsh-compact-basic/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-compact-basic' @@ -17,8 +10,37 @@ export const name = 'compact-basic-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'BasicCompactService', + inject: [ + 'llm', + 'tokenMeter', + ], + effects: [ + 'ctx.provide("compact")', + ], + services: [ + 'compact', + ], + validate: (fiber, effectLabels) => { + const automaticEffects = [ + 'ctx.on("agent/post-step")', + 'ctx.on("agent/request-error")', + ] + const installed = automaticEffects.filter(label => effectLabels.has(label)).length + const automatic = (fiber.config as { auto?: boolean }).auto !== false + if (automatic && installed !== automaticEffects.length) { + return 'automatic compaction must install both pressure and overflow listeners' + } + if (!automatic && installed !== 0) { + return 'auto:false must install neither automatic compaction listener' + } + return undefined + }, + }) +} /** * Register this package's invariant companion. @@ -27,4 +49,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/compact/compact-basic/tests/compact-basic.spec.ts b/packages/compact/compact-basic/tests/compact-basic.spec.ts index 31992793f0..762289c9d9 100644 --- a/packages/compact/compact-basic/tests/compact-basic.spec.ts +++ b/packages/compact/compact-basic/tests/compact-basic.spec.ts @@ -5,7 +5,7 @@ import type { BasicCompactConfig } from '@deepseek-ai/dsh-compact-basic' import { selectCompactableRange } from '@deepseek-ai/dsh-compact-basic/src/region.ts' import { toolPairingBalancedAfter, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compact' import { resolveConfig } from '@deepseek-ai/dsh-compact-basic/src/config.ts' -import type { CompactionResult } from '@deepseek-ai/dsh-compact' +import type { CompactService, CompactionResult } from '@deepseek-ai/dsh-compact' import LlmService, { CallId, CONTEXT_WINDOW_EXCEEDED_CODE, LlmAdapter } from '@deepseek-ai/dsh-llm' import type { ContentBlock, GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' @@ -198,6 +198,29 @@ describe('compact configuration and defaults', () => { expect(() => resolveConfig(config as BasicCompactConfig, ctx.tokenMeter)).toThrow(pattern) } }) + + it.each([ + [{ auto: true }, false, /must install both pressure and overflow listeners/], + [{ auto: false }, true, /must install neither automatic compaction listener/], + ])('rejects an inconsistent automatic-listener topology through the package invariant', async (config, installListener, message) => { + const ctx = new Context() + await ctx.plugin(LlmService) + await ctx.plugin(TokenMeterService) + const invalidCompact = { + name: 'BasicCompactService', + inject: ['llm', 'tokenMeter'], + apply(child: Context, _config: { auto?: boolean }) { + child.provide('compact', { + compactIfNeeded() {}, + compactRegion() {}, + } as unknown as CompactService) + if (installListener) { + child.effect(() => () => {}, 'ctx.on("agent/post-step")') + } + }, + } + await expect(ctx.plugin(invalidCompact, config)).rejects.toThrow(message) + }) }) describe('pressure measurement and retention', () => { diff --git a/packages/compact/compact/src/invariant.ts b/packages/compact/compact/src/invariant.ts index f7a2cbffd1..974f7a29ca 100644 --- a/packages/compact/compact/src/invariant.ts +++ b/packages/compact/compact/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-compact`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-compact/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-compact`. @module @deepseek-ai/dsh-compact/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-compact' @@ -17,8 +10,12 @@ export const name = 'compact-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Validate every implementation bound to this package's service seam. */ +const install: InvariantInstaller = (ctx, fail) => { + observeServiceInvariant(ctx, fail, 'compact', value => serviceShapeViolation(value, { + methods: ['compactIfNeeded', 'compactRegion'], + })) +} /** * Register this package's invariant companion. @@ -27,4 +24,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/context/time-context/src/invariant.ts b/packages/context/time-context/src/invariant.ts index 64fb98ac81..f1497f4c4a 100644 --- a/packages/context/time-context/src/invariant.ts +++ b/packages/context/time-context/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-time-context`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-time-context/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-time-context`. @module @deepseek-ai/dsh-time-context/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-time-context' @@ -17,8 +10,18 @@ export const name = 'time-context-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'time-context', + inject: [ + 'agents', + ], + effects: [ + 'ctx.on("agent/pre-step")', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/context/workspace-context/src/invariant.ts b/packages/context/workspace-context/src/invariant.ts index f3e76225c0..0258acff14 100644 --- a/packages/context/workspace-context/src/invariant.ts +++ b/packages/context/workspace-context/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-workspace-context`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-workspace-context/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-workspace-context`. @module @deepseek-ai/dsh-workspace-context/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-workspace-context' @@ -17,8 +10,18 @@ export const name = 'workspace-context-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'workspace-context', + effects: [ + 'ctx.on("session/event")', + 'ctx.on("agent/session-prefix")', + 'ctx.on("tools/post-execute")', + 'ctx.on("tools/result")', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/cordis/tool-cordis/src/invariant.ts b/packages/cordis/tool-cordis/src/invariant.ts index a3f375b69f..8d09b3bb9e 100644 --- a/packages/cordis/tool-cordis/src/invariant.ts +++ b/packages/cordis/tool-cordis/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-tool-cordis`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-tool-cordis/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-cordis`. @module @deepseek-ai/dsh-tool-cordis/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-cordis' @@ -17,8 +10,19 @@ export const name = 'tool-cordis-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'tool-cordis', + inject: [ + 'tools', + ], + effects: [ + 'ctx.plugin()', + 'tools.register()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +31,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/core/system-prompt/src/invariant.ts b/packages/core/system-prompt/src/invariant.ts index 5117f93ce0..36052bb90a 100644 --- a/packages/core/system-prompt/src/invariant.ts +++ b/packages/core/system-prompt/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-system-prompt`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-system-prompt/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-system-prompt`. @module @deepseek-ai/dsh-system-prompt/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-system-prompt' @@ -17,8 +10,19 @@ export const name = 'system-prompt-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'SystemPrompt', + effects: [ + 'ctx.provide("systemPrompt")', + 'systemPrompt.section()', + ], + services: [ + 'systemPrompt', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +31,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/core/tools/src/invariant.ts b/packages/core/tools/src/invariant.ts index 7c5743e1d4..c5b53e67fa 100644 --- a/packages/core/tools/src/invariant.ts +++ b/packages/core/tools/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-tools`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-tools/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tools`. @module @deepseek-ai/dsh-tools/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tools' @@ -17,8 +10,22 @@ export const name = 'tools-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'ToolRegistry', + inject: [ + 'systemPrompt', + ], + effects: [ + 'ctx.provide("tools")', + 'systemPrompt.tools()', + ], + services: [ + 'tools', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +34,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/examples/acp-demo/src/invariant.ts b/packages/examples/acp-demo/src/invariant.ts index d8f2ff17dc..b8801dd71e 100644 --- a/packages/examples/acp-demo/src/invariant.ts +++ b/packages/examples/acp-demo/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-acp-demo`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-acp-demo/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-acp-demo`. @module @deepseek-ai/dsh-acp-demo/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-acp-demo' @@ -17,8 +10,15 @@ export const name = 'acp-demo-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'acp-demo', + effects: [ + 'ctx.plugin()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +27,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/examples/acp-demo/tests/acp-agent.spec.ts b/packages/examples/acp-demo/tests/acp-agent.spec.ts index 10ee749f8c..d8d6f85db5 100644 --- a/packages/examples/acp-demo/tests/acp-agent.spec.ts +++ b/packages/examples/acp-demo/tests/acp-agent.spec.ts @@ -21,7 +21,14 @@ import * as acpAgent from '../src/index.ts' */ async function mount(config: acpAgent.Config, withBash = false): Promise { const ctx = new Context() - if (withBash) ctx.provide('bash', { sandboxMode: undefined }) + if (withBash) { + ctx.provide('bash', { + sandboxMode: undefined, + resolve() { throw new Error('composition test does not execute bash') }, + run() { throw new Error('composition test does not execute bash') }, + start() { throw new Error('composition test does not execute bash') }, + }) + } await ctx.plugin(acpAgent, config) // The bundle mounts its children inside apply() (not awaited there); let their // fibers settle so the spine services are ready. diff --git a/packages/examples/agent-spine-demo/src/invariant.ts b/packages/examples/agent-spine-demo/src/invariant.ts index 913b5ca2ab..75ab162567 100644 --- a/packages/examples/agent-spine-demo/src/invariant.ts +++ b/packages/examples/agent-spine-demo/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-agent-spine-demo`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-agent-spine-demo/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-agent-spine-demo`. @module @deepseek-ai/dsh-agent-spine-demo/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-agent-spine-demo' @@ -17,8 +10,15 @@ export const name = 'agent-spine-demo-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'agent-spine-demo', + effects: [ + 'ctx.plugin()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +27,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/examples/agent-spine-demo/tests/agent-core.spec.ts b/packages/examples/agent-spine-demo/tests/agent-core.spec.ts index 555a675cba..37bde87b31 100644 --- a/packages/examples/agent-spine-demo/tests/agent-core.spec.ts +++ b/packages/examples/agent-spine-demo/tests/agent-core.spec.ts @@ -48,7 +48,14 @@ async function mount(config: agentCore.Config, withBash = false): Promise {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'cli-demo', + effects: [ + 'ctx.plugin()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +27,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/examples/cli-demo/tests/cli-demo.spec.ts b/packages/examples/cli-demo/tests/cli-demo.spec.ts index 343d6184d7..4e55f387fa 100644 --- a/packages/examples/cli-demo/tests/cli-demo.spec.ts +++ b/packages/examples/cli-demo/tests/cli-demo.spec.ts @@ -22,7 +22,14 @@ async function skillConfig(catalogDescriptionMaxLength?: number): Promise { const ctx = new Context() - if (withBash) ctx.provide('bash', { sandboxMode: undefined }) + if (withBash) { + ctx.provide('bash', { + sandboxMode: undefined, + resolve() { throw new Error('composition test does not execute bash') }, + run() { throw new Error('composition test does not execute bash') }, + start() { throw new Error('composition test does not execute bash') }, + }) + } contexts.push(ctx) await ctx.plugin(cliDemo, config) await new Promise(resolve => setTimeout(resolve, 80)) diff --git a/packages/examples/jsonrpc-demo/src/invariant.ts b/packages/examples/jsonrpc-demo/src/invariant.ts index 21f19faf0f..f65eee8165 100644 --- a/packages/examples/jsonrpc-demo/src/invariant.ts +++ b/packages/examples/jsonrpc-demo/src/invariant.ts @@ -1,14 +1,8 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-jsonrpc-demo`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-jsonrpc-demo/invariant - */ +/** Package-owned runtime contract for @deepseek-ai/dsh-jsonrpc-demo. @module @deepseek-ai/dsh-jsonrpc-demo/invariant */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-jsonrpc-demo' @@ -17,8 +11,15 @@ export const name = 'jsonrpc-demo-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert that Loader configuration, rather than a hidden root plugin, owns composition. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const packageEntry = await import('./index.ts') + assertInvariant(fail, Object.keys(packageEntry).length === 0, + 'the JSON-RPC demo library entrypoint must remain empty because cordis.yml owns composition') + return () => {} + }, 'jsonrpc-demo: validate bin-only entrypoint') +} /** * Register this package's invariant companion. diff --git a/packages/examples/stdio-demo/src/invariant.ts b/packages/examples/stdio-demo/src/invariant.ts index 5d51c3cd9b..8e24a3b94b 100644 --- a/packages/examples/stdio-demo/src/invariant.ts +++ b/packages/examples/stdio-demo/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-stdio-demo`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-stdio-demo/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-stdio-demo`. @module @deepseek-ai/dsh-stdio-demo/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-stdio-demo' @@ -17,8 +10,15 @@ export const name = 'stdio-demo-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'stdio-demo', + effects: [ + 'ctx.plugin()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +27,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/examples/stdio-demo/tests/stdio-agent.spec.ts b/packages/examples/stdio-demo/tests/stdio-agent.spec.ts index c8ca063151..7bd309a916 100644 --- a/packages/examples/stdio-demo/tests/stdio-agent.spec.ts +++ b/packages/examples/stdio-demo/tests/stdio-agent.spec.ts @@ -18,7 +18,14 @@ import * as stdioAgent from '../src/index.ts' */ async function mount(config: stdioAgent.Config, withBash = false): Promise { const ctx = new Context() - if (withBash) ctx.provide('bash', { sandboxMode: undefined }) + if (withBash) { + ctx.provide('bash', { + sandboxMode: undefined, + resolve() { throw new Error('composition test does not execute bash') }, + run() { throw new Error('composition test does not execute bash') }, + start() { throw new Error('composition test does not execute bash') }, + }) + } await ctx.plugin(stdioAgent, config) // The app mounts its children inside apply() (not awaited there); let their // fibers settle so the spine services + the pre-created agent are ready. diff --git a/packages/fs/fs-local/src/invariant.ts b/packages/fs/fs-local/src/invariant.ts index 0c89f2299b..6469bffae6 100644 --- a/packages/fs/fs-local/src/invariant.ts +++ b/packages/fs/fs-local/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-fs-local`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-fs-local/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-fs-local`. @module @deepseek-ai/dsh-fs-local/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-fs-local' @@ -17,8 +10,18 @@ export const name = 'fs-local-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'LocalFileSystem', + effects: [ + 'ctx.provide("fs")', + ], + services: [ + 'fs', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/fs/fs-policy/src/invariant.ts b/packages/fs/fs-policy/src/invariant.ts index 11f155d2ec..de77ac303d 100644 --- a/packages/fs/fs-policy/src/invariant.ts +++ b/packages/fs/fs-policy/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-fs-policy`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-fs-policy/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-fs-policy`. @module @deepseek-ai/dsh-fs-policy/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-fs-policy' @@ -17,8 +10,17 @@ export const name = 'fs-policy-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'fs-policy', + effects: [ + 'ctx.on("fs/write-intent")', + 'ctx.on("fs/edit-intent")', + 'ctx.on("fs/observed")', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +29,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/fs/fs/src/invariant.ts b/packages/fs/fs/src/invariant.ts index 55895b4b24..586f962247 100644 --- a/packages/fs/fs/src/invariant.ts +++ b/packages/fs/fs/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-fs`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-fs/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-fs`. @module @deepseek-ai/dsh-fs/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-fs' @@ -17,8 +10,12 @@ export const name = 'fs-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Validate every implementation bound to this package's service seam. */ +const install: InvariantInstaller = (ctx, fail) => { + observeServiceInvariant(ctx, fail, 'fs', value => serviceShapeViolation(value, { + methods: ['resolve', 'stat', 'lstat', 'readText', 'streamText', 'listDir', 'writeText', 'editText'], + })) +} /** * Register this package's invariant companion. @@ -27,4 +24,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/fs/tool-fs-search/src/invariant.ts b/packages/fs/tool-fs-search/src/invariant.ts index f0055b611a..65da611560 100644 --- a/packages/fs/tool-fs-search/src/invariant.ts +++ b/packages/fs/tool-fs-search/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-tool-fs-search`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-tool-fs-search/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-fs-search`. @module @deepseek-ai/dsh-tool-fs-search/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-fs-search' @@ -17,8 +10,20 @@ export const name = 'tool-fs-search-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'tool-fs-search', + inject: [ + 'tools', + 'systemPrompt', + 'bash', + ], + effects: [ + 'tools.register()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +32,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/fs/tool-fs/src/invariant.ts b/packages/fs/tool-fs/src/invariant.ts index 7a683d978f..caa31a13b5 100644 --- a/packages/fs/tool-fs/src/invariant.ts +++ b/packages/fs/tool-fs/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-tool-fs`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-tool-fs/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-fs`. @module @deepseek-ai/dsh-tool-fs/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-fs' @@ -17,8 +10,20 @@ export const name = 'tool-fs-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'tool-fs', + inject: [ + 'tools', + 'fs', + 'systemPrompt', + ], + effects: [ + 'tools.register()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +32,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/guard/repeat-tool-guard/src/invariant.ts b/packages/guard/repeat-tool-guard/src/invariant.ts index 05c4df1a66..6bbdd1b539 100644 --- a/packages/guard/repeat-tool-guard/src/invariant.ts +++ b/packages/guard/repeat-tool-guard/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-repeat-tool-guard`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-repeat-tool-guard/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-repeat-tool-guard`. @module @deepseek-ai/dsh-repeat-tool-guard/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-repeat-tool-guard' @@ -17,8 +10,16 @@ export const name = 'repeat-tool-guard-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'repeat-tool-guard', + effects: [ + 'ctx.on("tools/post-execute")', + 'ctx.on("agent/prompt-submit")', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +28,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/hooks/hook-protocol/src/invariant.ts b/packages/hooks/hook-protocol/src/invariant.ts index 9893493644..6503791f1c 100644 --- a/packages/hooks/hook-protocol/src/invariant.ts +++ b/packages/hooks/hook-protocol/src/invariant.ts @@ -1,14 +1,8 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-hook-protocol`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-hook-protocol/invariant - */ +/** Package-owned runtime contracts for @deepseek-ai/dsh-hook-protocol. @module @deepseek-ai/dsh-hook-protocol/invariant */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-hook-protocol' @@ -17,8 +11,24 @@ export const name = 'hook-protocol-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert blocking-exit decoding and restrictive merge precedence. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const { parseHookOutput } = await import('./codec.ts') + const { mergeHookOutputs } = await import('./merge.ts') + const blocked = parseHookOutput(2, '', ' denied ') + assertInvariant(fail, blocked.decision === 'block' && blocked.reason === 'denied', + 'exit 2 must decode as a block whose reason is trimmed stderr') + + const merged = mergeHookOutputs([ + { exitCode: 0, stderr: '', stdout: '', decision: 'allow', reason: 'permitted' }, + { exitCode: 0, stderr: '', stdout: '', decision: 'deny', reason: 'forbidden' }, + ]) + assertInvariant(fail, merged.decision === 'deny' && merged.reason === 'forbidden', + 'deny must override allow and retain only the winning decision reason') + return () => {} + }, 'hook-protocol: validate decode and merge algebra') +} /** * Register this package's invariant companion. diff --git a/packages/hooks/hooks-claude/src/invariant.ts b/packages/hooks/hooks-claude/src/invariant.ts index 5c6002f7f5..d092d78a00 100644 --- a/packages/hooks/hooks-claude/src/invariant.ts +++ b/packages/hooks/hooks-claude/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-hooks-claude`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-hooks-claude/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-hooks-claude`. @module @deepseek-ai/dsh-hooks-claude/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-hooks-claude' @@ -17,8 +10,31 @@ export const name = 'hooks-claude-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'hooks-claude', + inject: [ + 'bash', + ], + validate: (_fiber, effectLabels) => { + const hookEffects = [ + 'hooks-claude: drain detached hook runs', + 'ctx.on("agent/session-start")', + 'ctx.on("agent/prompt-submit")', + 'ctx.on("tools/pre-execute")', + 'ctx.on("tools/post-execute")', + 'ctx.on("agent/turn-continuation")', + 'ctx.on("subagent/start")', + 'ctx.on("subagent/end")', + ] + const installed = hookEffects.filter(label => effectLabels.has(label)).length + return installed === 0 || installed === hookEffects.length + ? undefined + : 'a readable Claude hook config must install its complete listener set atomically' + }, + }) +} /** * Register this package's invariant companion. @@ -27,4 +43,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/hooks/hooks-claude/tests/invariant.spec.ts b/packages/hooks/hooks-claude/tests/invariant.spec.ts new file mode 100644 index 0000000000..1f7513cc64 --- /dev/null +++ b/packages/hooks/hooks-claude/tests/invariant.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import type { BashExecutor } from '@deepseek-ai/dsh-bash' + +describe('Claude hook package invariant', () => { + it('rejects a partially installed hook listener set', async () => { + const ctx = new Context() + await ctx.plugin({ + name: 'claude-invariant-bash', + apply(child: Context) { + child.provide('bash', { + resolve() {}, + async run() {}, + start() {}, + } as unknown as BashExecutor) + }, + }) + await expect(ctx.plugin({ + name: 'hooks-claude', + inject: ['bash'], + apply(child: Context) { + child.effect(() => () => {}, 'ctx.on("agent/session-start")') + }, + })).rejects.toThrow(/must install its complete listener set atomically/) + }) +}) diff --git a/packages/hooks/hooks-codex/src/invariant.ts b/packages/hooks/hooks-codex/src/invariant.ts index 1b8f03a057..4fba6cea2d 100644 --- a/packages/hooks/hooks-codex/src/invariant.ts +++ b/packages/hooks/hooks-codex/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-hooks-codex`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-hooks-codex/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-hooks-codex`. @module @deepseek-ai/dsh-hooks-codex/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-hooks-codex' @@ -17,8 +10,29 @@ export const name = 'hooks-codex-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'hooks-codex', + inject: [ + 'bash', + ], + validate: (_fiber, effectLabels) => { + const hookEffects = [ + 'hooks-codex: drain detached hook runs', + 'ctx.on("agent/session-start")', + 'ctx.on("agent/prompt-submit")', + 'ctx.on("tools/pre-execute")', + 'ctx.on("tools/post-execute")', + 'ctx.on("agent/turn-continuation")', + ] + const installed = hookEffects.filter(label => effectLabels.has(label)).length + return installed === 0 || installed === hookEffects.length + ? undefined + : 'a readable Codex hook config must install its complete listener set atomically' + }, + }) +} /** * Register this package's invariant companion. @@ -27,4 +41,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/hooks/hooks-codex/tests/invariant.spec.ts b/packages/hooks/hooks-codex/tests/invariant.spec.ts new file mode 100644 index 0000000000..bd6ee595f9 --- /dev/null +++ b/packages/hooks/hooks-codex/tests/invariant.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import type { BashExecutor } from '@deepseek-ai/dsh-bash' + +describe('Codex hook package invariant', () => { + it('rejects a partially installed hook listener set', async () => { + const ctx = new Context() + await ctx.plugin({ + name: 'codex-invariant-bash', + apply(child: Context) { + child.provide('bash', { + resolve() {}, + async run() {}, + start() {}, + } as unknown as BashExecutor) + }, + }) + await expect(ctx.plugin({ + name: 'hooks-codex', + inject: ['bash'], + apply(child: Context) { + child.effect(() => () => {}, 'ctx.on("agent/session-start")') + }, + })).rejects.toThrow(/must install its complete listener set atomically/) + }) +}) diff --git a/packages/llm/llm-deepseek/src/invariant.ts b/packages/llm/llm-deepseek/src/invariant.ts index c3e6b5e153..3327cec448 100644 --- a/packages/llm/llm-deepseek/src/invariant.ts +++ b/packages/llm/llm-deepseek/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-llm-deepseek`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-llm-deepseek/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-llm-deepseek`. @module @deepseek-ai/dsh-llm-deepseek/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-llm-deepseek' @@ -17,8 +10,18 @@ export const name = 'llm-deepseek-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'llm-deepseek', + inject: [ + 'llm', + ], + effects: [ + 'llm.registerAdapter()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/llm/llm-pi-ai/src/invariant.ts b/packages/llm/llm-pi-ai/src/invariant.ts index 4ada7a1a51..ba546fb902 100644 --- a/packages/llm/llm-pi-ai/src/invariant.ts +++ b/packages/llm/llm-pi-ai/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-llm-pi-ai`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-llm-pi-ai/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-llm-pi-ai`. @module @deepseek-ai/dsh-llm-pi-ai/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-llm-pi-ai' @@ -17,8 +10,18 @@ export const name = 'llm-pi-ai-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'llm-pi-ai', + inject: [ + 'llm', + ], + effects: [ + 'llm.registerAdapter()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/llm/llm/src/invariant.ts b/packages/llm/llm/src/invariant.ts index a8a56ce245..a4b8b50a3f 100644 --- a/packages/llm/llm/src/invariant.ts +++ b/packages/llm/llm/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-llm`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-llm/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-llm`. @module @deepseek-ai/dsh-llm/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-llm' @@ -17,8 +10,18 @@ export const name = 'llm-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'LlmService', + effects: [ + 'ctx.provide("llm")', + ], + services: [ + 'llm', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/llm/token-meter/src/invariant.ts b/packages/llm/token-meter/src/invariant.ts index 00ceb567be..838a9409e8 100644 --- a/packages/llm/token-meter/src/invariant.ts +++ b/packages/llm/token-meter/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-token-meter`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-token-meter/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-token-meter`. @module @deepseek-ai/dsh-token-meter/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-token-meter' @@ -17,8 +10,19 @@ export const name = 'token-meter-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'TokenMeterService', + effects: [ + 'ctx.provide("tokenMeter")', + 'ctx.on("session/event")', + ], + services: [ + 'tokenMeter', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +31,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/mcp/mcp-client/src/invariant.ts b/packages/mcp/mcp-client/src/invariant.ts index d9e75e9955..3cb9dd6005 100644 --- a/packages/mcp/mcp-client/src/invariant.ts +++ b/packages/mcp/mcp-client/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-mcp-client`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-mcp-client/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-mcp-client`. @module @deepseek-ai/dsh-mcp-client/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-mcp-client' @@ -17,8 +10,19 @@ export const name = 'mcp-client-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'mcp-client', + inject: [ + 'tools', + ], + effects: [ + 'mcp-client.serverName', + 'mcp-client.connection', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +31,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/sandbox/sandbox-local/src/invariant.ts b/packages/sandbox/sandbox-local/src/invariant.ts index 3582962f94..da6897f755 100644 --- a/packages/sandbox/sandbox-local/src/invariant.ts +++ b/packages/sandbox/sandbox-local/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-sandbox-local`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-sandbox-local/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-sandbox-local`. @module @deepseek-ai/dsh-sandbox-local/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-sandbox-local' @@ -17,8 +10,18 @@ export const name = 'sandbox-local-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'LocalSandboxProvider', + effects: [ + 'ctx.provide("sandbox")', + ], + services: [ + 'sandbox', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/sandbox/sandbox/src/invariant.ts b/packages/sandbox/sandbox/src/invariant.ts index b220f11717..cd1f767df0 100644 --- a/packages/sandbox/sandbox/src/invariant.ts +++ b/packages/sandbox/sandbox/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-sandbox`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-sandbox/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-sandbox`. @module @deepseek-ai/dsh-sandbox/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-sandbox' @@ -17,8 +10,12 @@ export const name = 'sandbox-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Validate every implementation bound to this package's service seam. */ +const install: InvariantInstaller = (ctx, fail) => { + observeServiceInvariant(ctx, fail, 'sandbox', value => serviceShapeViolation(value, { + methods: ['confine'], + })) +} /** * Register this package's invariant companion. @@ -27,4 +24,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/sdk/create-sdk/src/invariant.ts b/packages/sdk/create-sdk/src/invariant.ts index 87a697e438..66a95e6a38 100644 --- a/packages/sdk/create-sdk/src/invariant.ts +++ b/packages/sdk/create-sdk/src/invariant.ts @@ -1,14 +1,8 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/create-sdk`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/create-sdk/invariant - */ +/** Package-owned runtime contracts for @deepseek-ai/create-sdk. @module @deepseek-ai/create-sdk/invariant */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/create-sdk' @@ -17,8 +11,26 @@ export const name = 'create-sdk-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert the bin-only entrypoint and its core argument mapping. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const { parseCreateArgs } = await import('./args.ts') + const packageEntry = await import('./index.ts') + assertInvariant(fail, Object.keys(packageEntry).length === 0, + 'the create-sdk library entrypoint must remain empty because the package is bin-only') + const parsed = parseCreateArgs([ + 'workspace', '--provider=custom', '--base-url=https://example.test', '--interface=embed', '--no-install', + ]) + assertInvariant(fail, + parsed.directory === 'workspace' + && parsed.provider === 'custom' + && parsed.baseURL === 'https://example.test' + && parsed.runInterface === 'embed' + && parsed.install === false, + 'create-sdk arguments must preserve directory, provider, base URL, interface, and negative install flags') + return () => {} + }, 'create-sdk: validate bin and argument contracts') +} /** * Register this package's invariant companion. diff --git a/packages/sdk/helper/src/invariant.ts b/packages/sdk/helper/src/invariant.ts index 63a6fc2055..e91fb012d5 100644 --- a/packages/sdk/helper/src/invariant.ts +++ b/packages/sdk/helper/src/invariant.ts @@ -1,14 +1,8 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-helper`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-helper/invariant - */ +/** Package-owned runtime contracts for @deepseek-ai/dsh-helper. @module @deepseek-ai/dsh-helper/invariant */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-helper' @@ -17,8 +11,22 @@ export const name = 'helper-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert FeatureId's zero-cost representation and boundary validation. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const { featureId } = await import('./ids.ts') + assertInvariant(fail, featureId('local-plugin') === 'local-plugin', + 'a valid feature id must preserve its runtime string value') + let rejected = false + try { + featureId('Invalid Feature') + } catch (error) { + rejected = error instanceof Error + } + assertInvariant(fail, rejected, 'feature ids must reject values outside lowercase kebab-case') + return () => {} + }, 'dsh-helper: validate feature identities') +} /** * Register this package's invariant companion. diff --git a/packages/sdk/scripts/src/args.ts b/packages/sdk/scripts/src/args.ts index 1b91269592..a1f0a2eb0d 100644 --- a/packages/sdk/scripts/src/args.ts +++ b/packages/sdk/scripts/src/args.ts @@ -6,6 +6,7 @@ import { parseArgs as parseNodeArgs } from 'node:util' import { Command } from 'commander' +import { splitForwardedArgs } from './forwarding.ts' /** Commands implemented by the dsh-sdk launcher. */ type DshSdkCommand = 'start' | 'dev' | 'build' | 'config' @@ -33,9 +34,7 @@ export function parseDshSdkArgs(argv: readonly string[]): DshSdkArgs { if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') { return { forwarded: [], help: true } } - const separator = argv.indexOf('--') - const launcherArgv = separator === -1 ? argv : argv.slice(0, separator) - const passthrough = separator === -1 ? [] : argv.slice(separator + 1) + const { launcher: launcherArgv, forwarded: passthrough } = splitForwardedArgs(argv) let parsed: DshSdkArgs | undefined const program = new Command() .name('dsh-sdk') diff --git a/packages/sdk/scripts/src/forwarding.ts b/packages/sdk/scripts/src/forwarding.ts new file mode 100644 index 0000000000..56fa85f564 --- /dev/null +++ b/packages/sdk/scripts/src/forwarding.ts @@ -0,0 +1,21 @@ +/** Argument-delimiter handling shared by the SDK launcher and its invariant. */ + +/** Launcher-owned arguments and opaque arguments following `--`. */ +export interface ForwardedArgumentSplit { + /** Arguments parsed by the SDK launcher. */ + readonly launcher: readonly string[] + /** Arguments passed unchanged to the selected project command. */ + readonly forwarded: readonly string[] +} + +/** + * Split the first `--` delimiter without interpreting either side. + * @param argv - complete user argument vector. + * @returns launcher arguments and post-delimiter arguments. + */ +export function splitForwardedArgs(argv: readonly string[]): ForwardedArgumentSplit { + const separator = argv.indexOf('--') + return separator === -1 + ? { launcher: argv, forwarded: [] } + : { launcher: argv.slice(0, separator), forwarded: argv.slice(separator + 1) } +} diff --git a/packages/sdk/scripts/src/invariant.ts b/packages/sdk/scripts/src/invariant.ts index fd0a0ef55b..316e7ebed1 100644 --- a/packages/sdk/scripts/src/invariant.ts +++ b/packages/sdk/scripts/src/invariant.ts @@ -1,14 +1,8 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-scripts`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-scripts/invariant - */ +/** Package-owned runtime contracts for @deepseek-ai/dsh-scripts. @module @deepseek-ai/dsh-scripts/invariant */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-scripts' @@ -17,8 +11,24 @@ export const name = 'scripts-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert the launcher's opaque post-separator forwarding boundary. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const { splitForwardedArgs } = await import('./forwarding.ts') + const plain = splitForwardedArgs(['dev', 'src/index.ts']) + const separated = splitForwardedArgs(['dev', 'src/index.ts', '--', '--inspect', '9229']) + assertInvariant(fail, + plain.launcher.length === 2 + && plain.forwarded.length === 0 + && separated.launcher.length === 2 + && separated.launcher[1] === 'src/index.ts' + && separated.forwarded.length === 2 + && separated.forwarded[0] === '--inspect' + && separated.forwarded[1] === '9229', + 'dsh-sdk must split the first delimiter without interpreting forwarded runtime arguments') + return () => {} + }, 'dsh-sdk: validate command argument contracts') +} /** * Register this package's invariant companion. diff --git a/packages/sdk/scripts/tests/scripts.spec.ts b/packages/sdk/scripts/tests/scripts.spec.ts index 8d110799ea..6f1af04a4e 100644 --- a/packages/sdk/scripts/tests/scripts.spec.ts +++ b/packages/sdk/scripts/tests/scripts.spec.ts @@ -132,6 +132,7 @@ describe('Commander launcher arguments', () => { expect(parseDshSdkArgs(['start'])).toEqual({ command: 'start', forwarded: [], help: false }) expect(parseDshSdkArgs(['dev', 'index.ts'])).toMatchObject({ command: 'dev', target: 'index.ts' }) expect(parseDshSdkArgs(['-h'])).toMatchObject({ help: true }) + expect(parseDshSdkArgs(['--help'])).toMatchObject({ help: true }) expect(() => parseDshSdkArgs(['unknown'])).toThrow() expect(() => parseDshSdkArgs(['config', 'extra'])).toThrow() expect(() => parseDshSdkArgs(['config', '--', 'extra'])).toThrow('does not accept forwarded') diff --git a/packages/session-persistence/session-persistence-jsonl/src/invariant.ts b/packages/session-persistence/session-persistence-jsonl/src/invariant.ts index 12c65db1c4..a3f3854cff 100644 --- a/packages/session-persistence/session-persistence-jsonl/src/invariant.ts +++ b/packages/session-persistence/session-persistence-jsonl/src/invariant.ts @@ -1,14 +1,10 @@ /** - * Generated invariant ownership companion for `@deepseek-ai/dsh-session-persistence-jsonl`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts + * Package-owned runtime contract checks for `@deepseek-ai/dsh-session-persistence-jsonl`. * @module @deepseek-ai/dsh-session-persistence-jsonl/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-session-persistence-jsonl' @@ -17,8 +13,21 @@ export const name = 'session-persistence-jsonl-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'SessionPersistenceJsonl', + inject: [ + 'sessions', + ], + effects: [ + 'ctx.provide("sessionPersistence")', + ], + services: [ + 'sessionPersistence', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +36,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/session-persistence/session-persistence-sqlite/src/invariant.ts b/packages/session-persistence/session-persistence-sqlite/src/invariant.ts index a9b04cf5f5..8eb41c061b 100644 --- a/packages/session-persistence/session-persistence-sqlite/src/invariant.ts +++ b/packages/session-persistence/session-persistence-sqlite/src/invariant.ts @@ -1,14 +1,10 @@ /** - * Generated invariant ownership companion for `@deepseek-ai/dsh-session-persistence-sqlite`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts + * Package-owned runtime contract checks for `@deepseek-ai/dsh-session-persistence-sqlite`. * @module @deepseek-ai/dsh-session-persistence-sqlite/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-session-persistence-sqlite' @@ -17,8 +13,21 @@ export const name = 'session-persistence-sqlite-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'SessionPersistenceSqlite', + inject: [ + 'sessions', + ], + effects: [ + 'ctx.provide("sessionPersistence")', + ], + services: [ + 'sessionPersistence', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +36,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/session-persistence/session-persistence/src/invariant.ts b/packages/session-persistence/session-persistence/src/invariant.ts index cc7cc8fa2c..c62805d300 100644 --- a/packages/session-persistence/session-persistence/src/invariant.ts +++ b/packages/session-persistence/session-persistence/src/invariant.ts @@ -1,14 +1,10 @@ /** - * Generated invariant ownership companion for `@deepseek-ai/dsh-session-persistence`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts + * Package-owned runtime contract checks for `@deepseek-ai/dsh-session-persistence`. * @module @deepseek-ai/dsh-session-persistence/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-session-persistence' @@ -17,8 +13,12 @@ export const name = 'session-persistence-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Validate every implementation bound to this package's service seam. */ +const install: InvariantInstaller = (ctx, fail) => { + observeServiceInvariant(ctx, fail, 'sessionPersistence', value => serviceShapeViolation(value, { + methods: ['locate', 'create', 'append', 'load', 'list'], + })) +} /** * Register this package's invariant companion. @@ -27,4 +27,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/session-query/session-query/src/invariant.ts b/packages/session-query/session-query/src/invariant.ts index 91bcee721e..0f7a93acde 100644 --- a/packages/session-query/session-query/src/invariant.ts +++ b/packages/session-query/session-query/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-session-query`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-session-query/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-session-query`. @module @deepseek-ai/dsh-session-query/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-session-query' @@ -17,8 +10,21 @@ export const name = 'session-query-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'SessionQueryService', + inject: [ + 'sessions', + ], + effects: [ + 'ctx.provide("sessionQuery")', + ], + services: [ + 'sessionQuery', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +33,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/skill/skill-local/src/invariant.ts b/packages/skill/skill-local/src/invariant.ts index 475d02bb8f..7afa8c5023 100644 --- a/packages/skill/skill-local/src/invariant.ts +++ b/packages/skill/skill-local/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-skill-local`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-skill-local/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-skill-local`. @module @deepseek-ai/dsh-skill-local/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-skill-local' @@ -17,8 +10,18 @@ export const name = 'skill-local-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'skill-local', + inject: [ + 'skills', + ], + effects: [ + 'skills.registerProvider()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/skill/skill/src/invariant.ts b/packages/skill/skill/src/invariant.ts index c1d4091bb2..6a673db92a 100644 --- a/packages/skill/skill/src/invariant.ts +++ b/packages/skill/skill/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-skill`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-skill/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-skill`. @module @deepseek-ai/dsh-skill/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-skill' @@ -17,8 +10,18 @@ export const name = 'skill-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'SkillService', + effects: [ + 'ctx.provide("skills")', + ], + services: [ + 'skills', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/skill/tool-skill/src/invariant.ts b/packages/skill/tool-skill/src/invariant.ts index abf4ba3961..8e6d6bbc3c 100644 --- a/packages/skill/tool-skill/src/invariant.ts +++ b/packages/skill/tool-skill/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-tool-skill`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-tool-skill/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-skill`. @module @deepseek-ai/dsh-tool-skill/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-skill' @@ -17,8 +10,20 @@ export const name = 'tool-skill-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'tool-skill', + inject: [ + 'tools', + 'skills', + ], + effects: [ + 'tools.register()', + 'ctx.on("agent/session-prefix")', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +32,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/spill/spill-local/src/invariant.ts b/packages/spill/spill-local/src/invariant.ts index d638ffa2a9..b769007d7d 100644 --- a/packages/spill/spill-local/src/invariant.ts +++ b/packages/spill/spill-local/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-spill-local`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-spill-local/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-spill-local`. @module @deepseek-ai/dsh-spill-local/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-spill-local' @@ -17,8 +10,18 @@ export const name = 'spill-local-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'LocalSpillStore', + effects: [ + 'ctx.provide("spillStore")', + ], + services: [ + 'spillStore', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/spill/spill-policy/src/invariant.ts b/packages/spill/spill-policy/src/invariant.ts index d4aa544ecd..4af3a81f70 100644 --- a/packages/spill/spill-policy/src/invariant.ts +++ b/packages/spill/spill-policy/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-spill-policy`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-spill-policy/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-spill-policy`. @module @deepseek-ai/dsh-spill-policy/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-spill-policy' @@ -17,8 +10,22 @@ export const name = 'spill-policy-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'spill-policy', + inject: [ + 'tools', + ], + validate: (fiber, effectLabels) => { + const installed = effectLabels.has('ctx.on("tools/post-execute")') + const enabled = (fiber.config as { maxInlineBytes?: number }).maxInlineBytes !== undefined + return installed === enabled + ? undefined + : 'the post-execute spill policy listener must exist exactly when maxInlineBytes is configured' + }, + }) +} /** * Register this package's invariant companion. @@ -27,4 +34,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/spill/spill-policy/tests/spill-policy.spec.ts b/packages/spill/spill-policy/tests/spill-policy.spec.ts index 2449f26a8c..f0cbbbd2a0 100644 --- a/packages/spill/spill-policy/tests/spill-policy.spec.ts +++ b/packages/spill/spill-policy/tests/spill-policy.spec.ts @@ -109,6 +109,17 @@ describe('config validation', () => { it('rejects a fractional maxInlineBytes at load', async () => { await expect(setup({ maxInlineBytes: 1.5 })).rejects.toThrow(/non-negative integer/) }) + + it('rejects a configured policy that omits its post-execute listener', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + await expect(ctx.plugin({ + name: 'spill-policy', + inject: ['tools'], + apply(_child: Context, _config: { maxInlineBytes?: number }) {}, + }, { maxInlineBytes: 10 })).rejects.toThrow(/listener must exist exactly when maxInlineBytes is configured/) + }) }) describe('oversized plain-text replacement', () => { diff --git a/packages/spill/spill/src/invariant.ts b/packages/spill/spill/src/invariant.ts index 714e43f3a6..ba971b8bc2 100644 --- a/packages/spill/spill/src/invariant.ts +++ b/packages/spill/spill/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-spill`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-spill/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-spill`. @module @deepseek-ai/dsh-spill/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-spill' @@ -17,8 +10,12 @@ export const name = 'spill-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Validate every implementation bound to this package's service seam. */ +const install: InvariantInstaller = (ctx, fail) => { + observeServiceInvariant(ctx, fail, 'spillStore', value => serviceShapeViolation(value, { + methods: ['saveText'], + })) +} /** * Register this package's invariant companion. @@ -27,4 +24,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/subagent/subagent-acp/src/invariant.ts b/packages/subagent/subagent-acp/src/invariant.ts index a5828fd4c1..0c17843ff7 100644 --- a/packages/subagent/subagent-acp/src/invariant.ts +++ b/packages/subagent/subagent-acp/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-subagent-acp`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-subagent-acp/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-subagent-acp`. @module @deepseek-ai/dsh-subagent-acp/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-acp' @@ -17,8 +10,18 @@ export const name = 'subagent-acp-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'subagent-acp', + inject: [ + 'subagents', + ], + effects: [ + 'subagents.registerProvider()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/subagent/subagent-fork/src/invariant.ts b/packages/subagent/subagent-fork/src/invariant.ts index c6903fd82d..2fc8a0f696 100644 --- a/packages/subagent/subagent-fork/src/invariant.ts +++ b/packages/subagent/subagent-fork/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-subagent-fork`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-subagent-fork/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-subagent-fork`. @module @deepseek-ai/dsh-subagent-fork/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-fork' @@ -17,8 +10,18 @@ export const name = 'subagent-fork-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'subagent-fork', + inject: [ + 'subagents', + ], + effects: [ + 'subagents.registerProvider()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/subagent/subagent-inprocess/src/invariant.ts b/packages/subagent/subagent-inprocess/src/invariant.ts index 0eac204104..a007200d73 100644 --- a/packages/subagent/subagent-inprocess/src/invariant.ts +++ b/packages/subagent/subagent-inprocess/src/invariant.ts @@ -1,14 +1,8 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-subagent-inprocess`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-subagent-inprocess/invariant - */ +/** Package-owned runtime contracts for @deepseek-ai/dsh-subagent-inprocess. @module @deepseek-ai/dsh-subagent-inprocess/invariant */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-inprocess' @@ -17,8 +11,17 @@ export const name = 'subagent-inprocess-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert that structured-output guidance names the tool it actually installs. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const { STRUCTURED_OUTPUT_INSTRUCTION, STRUCTURED_OUTPUT_TOOL } = await import('./structured-protocol.ts') + assertInvariant(fail, /^[a-z][a-z0-9_]*$/.test(STRUCTURED_OUTPUT_TOOL), + 'the structured-output tool must retain a stable lowercase protocol name') + assertInvariant(fail, STRUCTURED_OUTPUT_INSTRUCTION.includes(STRUCTURED_OUTPUT_TOOL), + 'the structured-output instruction must name the exact installed tool') + return () => {} + }, 'subagent-inprocess: validate structured-output protocol') +} /** * Register this package's invariant companion. diff --git a/packages/subagent/subagent-inprocess/src/structured-protocol.ts b/packages/subagent/subagent-inprocess/src/structured-protocol.ts new file mode 100644 index 0000000000..8ae87e73b0 --- /dev/null +++ b/packages/subagent/subagent-inprocess/src/structured-protocol.ts @@ -0,0 +1,10 @@ +/** Model-facing constants shared by structured child execution and its invariant. */ + +/** The model-facing tool name a structured child must call to finish. */ +export const STRUCTURED_OUTPUT_TOOL = 'structured_output' + +/** The terminal structured-result instruction appended to a child request. */ +export const STRUCTURED_OUTPUT_INSTRUCTION + = 'When you have your final answer, you MUST report it by calling the ' + + `\`${STRUCTURED_OUTPUT_TOOL}\` tool with arguments matching its parameter schema exactly. ` + + 'Do not finish with a plain text answer: only the tool call counts as your result.' diff --git a/packages/subagent/subagent-inprocess/src/structured.ts b/packages/subagent/subagent-inprocess/src/structured.ts index 09aa2d24b7..cecb7d173e 100644 --- a/packages/subagent/subagent-inprocess/src/structured.ts +++ b/packages/subagent/subagent-inprocess/src/structured.ts @@ -15,19 +15,9 @@ import type { ContinuationStop } from '@deepseek-ai/dsh-agent' import type { ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm' import type { ToolExecution } from '@deepseek-ai/dsh-tools' import { ToolArgsError, validateStructuredValue, type StructuredOutputSchema } from '@deepseek-ai/dsh-tools' +import { STRUCTURED_OUTPUT_INSTRUCTION, STRUCTURED_OUTPUT_TOOL } from './structured-protocol.ts' -/** The model-facing tool name a structured child must call to finish. */ -export const STRUCTURED_OUTPUT_TOOL = 'structured_output' - -/** - * The instruction registered as the child's trailing (order-190, the end of - * the tool-guidance band) scoped prompt section: the demand travels with the - * tool, as ordinary prompt state of exactly one agent. - */ -export const STRUCTURED_OUTPUT_INSTRUCTION - = 'When you have your final answer, you MUST report it by calling the ' - + `\`${STRUCTURED_OUTPUT_TOOL}\` tool with arguments matching its parameter schema exactly. ` - + 'Do not finish with a plain text answer: only the tool call counts as your result.' +export { STRUCTURED_OUTPUT_INSTRUCTION, STRUCTURED_OUTPUT_TOOL } from './structured-protocol.ts' /** One structured run's live handle: read the captured value once the child settles. */ export interface StructuredAttachment { diff --git a/packages/subagent/subagent-spawn/src/invariant.ts b/packages/subagent/subagent-spawn/src/invariant.ts index d179a2b72a..28201593e4 100644 --- a/packages/subagent/subagent-spawn/src/invariant.ts +++ b/packages/subagent/subagent-spawn/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-subagent-spawn`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-subagent-spawn/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-subagent-spawn`. @module @deepseek-ai/dsh-subagent-spawn/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-spawn' @@ -17,8 +10,18 @@ export const name = 'subagent-spawn-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'subagent-spawn', + inject: [ + 'subagents', + ], + effects: [ + 'subagents.registerProvider()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/subagent/subagent-subprocess/src/invariant.ts b/packages/subagent/subagent-subprocess/src/invariant.ts index 22dac32814..ed29ecc855 100644 --- a/packages/subagent/subagent-subprocess/src/invariant.ts +++ b/packages/subagent/subagent-subprocess/src/invariant.ts @@ -1,24 +1,32 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-subagent-subprocess`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-subagent-subprocess/invariant - */ +/** Package-owned runtime contracts for @deepseek-ai/dsh-subagent-subprocess. @module @deepseek-ai/dsh-subagent-subprocess/invariant */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-subprocess' +const SENSITIVE_ENV_PATTERN = /KEY|SECRET|TOKEN/i /** Cordis companion plugin name. */ export const name = 'subagent-subprocess-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert ambient credential scrubbing and explicit credential precedence. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const { buildChildEnv } = await import('./index.ts') + const scrubbed = buildChildEnv({}) + const ambientSensitiveNames = Object.keys(process.env).filter(key => SENSITIVE_ENV_PATTERN.test(key)) + assertInvariant(fail, ambientSensitiveNames.every(key => !Object.hasOwn(scrubbed, key)), + 'subprocess environments must omit every credential-shaped ambient variable') + + const explicit = buildChildEnv({ DSH_INVARIANT_TOKEN: 'explicit-child-value' }) + assertInvariant(fail, explicit.DSH_INVARIANT_TOKEN === 'explicit-child-value', + 'explicit child credentials must be applied after ambient scrubbing') + return () => {} + }, 'subagent-subprocess: validate child environment isolation') +} /** * Register this package's invariant companion. diff --git a/packages/subagent/subagent/src/invariant.ts b/packages/subagent/subagent/src/invariant.ts index 3a79592ee1..3217a3dd9a 100644 --- a/packages/subagent/subagent/src/invariant.ts +++ b/packages/subagent/subagent/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-subagent`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-subagent/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-subagent`. @module @deepseek-ai/dsh-subagent/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-subagent' @@ -17,8 +10,18 @@ export const name = 'subagent-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'SubagentService', + effects: [ + 'ctx.provide("subagents")', + ], + services: [ + 'subagents', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/subagent/tool-subagent/src/invariant.ts b/packages/subagent/tool-subagent/src/invariant.ts index 08881e4b2b..b1bfa209a1 100644 --- a/packages/subagent/tool-subagent/src/invariant.ts +++ b/packages/subagent/tool-subagent/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-tool-subagent`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-tool-subagent/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-subagent`. @module @deepseek-ai/dsh-tool-subagent/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-subagent' @@ -17,8 +10,20 @@ export const name = 'tool-subagent-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'tool-subagent', + inject: [ + 'tools', + 'subagents', + ], + effects: [ + 'ctx.on("subagent/provider-added")', + 'ctx.on("subagent/provider-removed")', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +32,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/support/acp-snapshot/src/invariant.ts b/packages/support/acp-snapshot/src/invariant.ts index 3579b96cf5..fc568d7b37 100644 --- a/packages/support/acp-snapshot/src/invariant.ts +++ b/packages/support/acp-snapshot/src/invariant.ts @@ -1,14 +1,8 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-acp-snapshot`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-acp-snapshot/invariant - */ +/** Package-owned runtime contracts for @deepseek-ai/dsh-acp-snapshot. @module @deepseek-ai/dsh-acp-snapshot/invariant */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-acp-snapshot' @@ -17,8 +11,27 @@ export const name = 'acp-snapshot-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert stable JSON-RPC correlation and volatile-value tokenization. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const { normalizeStdout } = await import('./normalize.ts') + const sessionId = '12345678-1234-1234-1234-123456789abc' + const volatile = { sessionIds: [sessionId], cwd: '/tmp/dsh-acp-invariant' } + const raw = [ + JSON.stringify({ jsonrpc: '2.0', id: 'request-7', result: { cwd: volatile.cwd } }), + JSON.stringify({ jsonrpc: '2.0', id: 'request-7', result: { sessionId } }), + ].join('\n') + const normalized = normalizeStdout(raw, volatile) + assertInvariant(fail, + normalized.includes('"id":1') + && normalized.includes('"cwd":"{{cwd}}"') + && normalized.includes('"sessionId":"{{sessionId}}"'), + 'ACP normalization must preserve RPC correlation while tokenizing cwd and session ids') + assertInvariant(fail, normalizeStdout(normalized, volatile) === normalized, + 'ACP stdout normalization must be idempotent') + return () => {} + }, 'acp-snapshot: validate stable transcript normalization') +} /** * Register this package's invariant companion. diff --git a/packages/support/agent-loop-testkit/src/index.ts b/packages/support/agent-loop-testkit/src/index.ts index c7b0cb7304..ca7148ee23 100644 --- a/packages/support/agent-loop-testkit/src/index.ts +++ b/packages/support/agent-loop-testkit/src/index.ts @@ -6,12 +6,7 @@ */ import type { Context } from 'cordis' -import AgentRegistry from '@deepseek-ai/dsh-agent' -import LlmService from '@deepseek-ai/dsh-llm' -import SessionStore from '@deepseek-ai/dsh-session' -import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import type { Config as SystemPromptConfig } from '@deepseek-ai/dsh-system-prompt' -import ToolRegistry from '@deepseek-ai/dsh-tools' import type { Config as ToolRegistryConfig } from '@deepseek-ai/dsh-tools' /** Configuration forwarded to the prerequisite service plugins. */ @@ -38,6 +33,19 @@ export async function mountAgentLoopTestDependencies( ctx: Context, options: AgentLoopTestDependenciesOptions = {}, ): Promise { + const [ + { default: LlmService }, + { default: SessionStore }, + { default: SystemPrompt }, + { default: ToolRegistry }, + { default: AgentRegistry }, + ] = await Promise.all([ + import('@deepseek-ai/dsh-llm'), + import('@deepseek-ai/dsh-session'), + import('@deepseek-ai/dsh-system-prompt'), + import('@deepseek-ai/dsh-tools'), + import('@deepseek-ai/dsh-agent'), + ]) await ctx.plugin(LlmService) await ctx.plugin(SessionStore) await ctx.plugin(SystemPrompt, options.systemPrompt ?? {}) diff --git a/packages/support/agent-loop-testkit/src/invariant.ts b/packages/support/agent-loop-testkit/src/invariant.ts index fc2554aa77..fd72038d3f 100644 --- a/packages/support/agent-loop-testkit/src/invariant.ts +++ b/packages/support/agent-loop-testkit/src/invariant.ts @@ -1,14 +1,8 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-agent-loop-testkit`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-agent-loop-testkit/invariant - */ +/** Package-owned runtime contracts for @deepseek-ai/dsh-agent-loop-testkit. @module @deepseek-ai/dsh-agent-loop-testkit/invariant */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-agent-loop-testkit' @@ -17,8 +11,18 @@ export const name = 'agent-loop-testkit-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert the awaitable helper shape and optional-options call boundary. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const { mountAgentLoopTestDependencies } = await import('./index.ts') + assertInvariant(fail, + mountAgentLoopTestDependencies.constructor.name === 'AsyncFunction', + 'the prerequisite mount helper must remain awaitable so tests cannot race service activation') + assertInvariant(fail, mountAgentLoopTestDependencies.length === 1, + 'the prerequisite mount helper must keep its options argument optional') + return () => {} + }, 'agent-loop-testkit: validate prerequisite mount boundary') +} /** * Register this package's invariant companion. diff --git a/packages/support/invariants/README.md b/packages/support/invariants/README.md index c49771ded9..74e8b34310 100644 --- a/packages/support/invariants/README.md +++ b/packages/support/invariants/README.md @@ -24,9 +24,17 @@ The service owns every registration fiber, while the returned disposer also belo ## Package companions -An ownership-only generated baseline installs no listeners but still reserves its package name through the real service boundary. A package replaces that marked file when it gains a relational check, retaining the same registration. `pnpm run verify-package-invariants` checks every package's source registration, export, published files, dependencies, TypeScript reference, and bundle entry. +Every companion installs at least one executable, package-specific contract and reports failure through its bound reporter. There is no generated or ownership-only baseline. `pnpm run verify-package-invariants` rejects generated markers, empty installers, installers that ignore the reporter, duplicate name-based plugin observers, incorrect registration names, and incomplete export, publication, dependency, TypeScript-reference, or bundle wiring. -Four companions currently install stateful checks: +Packages select the narrowest runtime form that protects their public contract: + +| Package shape | Companion check | +|---|---| +| Cordis plugin | `observePluginInvariant` validates the plugin's own declared name, required injections, owned effect group, provided services, and optional package-specific relation for existing, late, and HMR-activated fibers. | +| Cordis service seam | `observeServiceInvariant` plus `serviceShapeViolation` validates current and future structural implementations, including conforming third-party backends and test doubles. | +| Pure library, bin, or support package | `assertInvariant` checks stable protocol algebra, parser mapping, path/timeout/retention rules, normalization, or entrypoint shape in a child effect. | + +Four companions additionally install stateful event and request checks: | Companion | Registration | Checks | |---|---|---| @@ -35,7 +43,7 @@ Four companions currently install stateful checks: | `@deepseek-ai/dsh-scope/invariant` | `@deepseek-ai/dsh-scope` | scoped-event carrier presence and subject consistency | | `@deepseek-ai/dsh-agent-loop/invariant` | `@deepseek-ai/dsh-agent-loop` | loop-built model-request reconstruction from the session log | -The root entrypoint of each owner remains independent of diagnostics. Loading the service alone installs no checks; loading a companion without the service remains pending on its declared `invariants` dependency. +The root entrypoint of each owner remains independent of diagnostics. Loading the service alone installs no checks; loading a companion without the service remains pending on its declared `invariants` dependency. Name-based plugin observers match only a fiber's own declared runtime name, not anonymous child fibers that inherit a parent display name. They avoid importing the product entrypoint before it is loaded; pure-library checks likewise defer owner imports into the installer child so Vitest mocks and deployment loaders establish their module boundary first. ## Composition @@ -54,7 +62,7 @@ ctx.plugin(InvariantService, { ctx.plugin(SessionInvariant) ``` -The standard agent spine mounts the service and the four stateful companions. Custom compositions choose the companions they want and may disable or filter them without changing package entrypoints. Vitest mounts every package companion against an explicitly enabled service for ordinary Cordis roots, so baseline ownership and stateful checks execute across unit, snapshot, and e2e suites; focused invariant-service tests construct their own topology to exercise filtering and lifecycle behavior. +The standard agent spine mounts the service and the four stateful companions. Custom compositions explicitly add the companions for the packages whose contracts they want checked and may disable or filter them without changing package entrypoints. Vitest mounts every package companion against an explicitly enabled service for ordinary Cordis roots, so all package checks execute across unit, snapshot, and e2e suites; focused invariant-service tests construct their own topology to exercise filtering and lifecycle behavior. ## Model Experience @@ -62,6 +70,7 @@ None, as the service and companions observe runtime events and requests but neve ## Known Limitations and Deferred Work -- Stateful checks cover only the four listed package contracts; other companions reserve ownership but add no listeners until their packages gain relational assertions. +- A name-based plugin observer assumes Cordis plugin names are unique within one root; a package can provide the exact callback when importing it does not preload an unrelated runtime. +- Pure-library contracts are sampled when their companion child activates rather than observed continuously; mutable package behavior belongs on an event, service, or plugin-fiber observer. - Request reconstruction covers frozen loop-built requests with a live session id; direct one-shot calls remain outside that companion's marker contract. - Regular-expression filters are fixed for the service lifetime; changing them requires ordinary Cordis plugin reload. diff --git a/packages/support/invariants/src/index.ts b/packages/support/invariants/src/index.ts index 4bbf218e97..374012fdbd 100644 --- a/packages/support/invariants/src/index.ts +++ b/packages/support/invariants/src/index.ts @@ -1,14 +1,13 @@ /** * Configurable registry for package-owned runtime invariant contributions. - * Every workspace package registers its name from a `./invariant` companion; - * ordinary package entrypoints stay independent of diagnostics, and packages - * without relational checks use an ownership-only installer. + * Every workspace package registers checks from a `./invariant` companion; + * ordinary package entrypoints stay independent of diagnostics. * * @module @deepseek-ai/dsh-invariants */ -import { Context, Service } from 'cordis' -import type { Inject } from 'cordis' +import { Context, FiberState, Service } from 'cordis' +import type { Fiber, Inject, Plugin } from 'cordis' import z from 'schemastery' import type Schema from 'schemastery' @@ -42,6 +41,173 @@ export interface InvariantInstaller { readonly inject?: Inject } +/** Runtime facts one package expects from its Cordis plugin fiber. */ +export interface PluginInvariantContract { + /** Exact plugin value when checking it does not preload an unrelated runtime; otherwise matching uses `name`. */ + readonly plugin?: Plugin + /** Exact Cordis display name for the plugin fiber. */ + readonly name: string + /** Required service injections that must be present when the fiber activates. */ + readonly inject?: readonly string[] + /** Required owned effect labels; an inner array means at least one alternative must exist. */ + readonly effects?: readonly (string | readonly string[])[] + /** Services the active fiber must provide. */ + readonly services?: readonly string[] + /** Optional package-owned validation after the structural checks pass. */ + readonly validate?: (fiber: Fiber, effectLabels: ReadonlySet) => string | undefined +} + +/** Collect all live effect labels below a plugin fiber. */ +function collectEffectLabels(fiber: Fiber): ReadonlySet { + const labels = new Set() + const visit = (effects: ReturnType): void => { + for (const effect of effects) { + labels.add(effect.label) + visit(effect.children) + } + } + visit(fiber.getEffects()) + return labels +} + +/** + * Observe one package plugin and fail whenever an active fiber violates its + * declared name, dependency, effect, service, or package-specific contract. + * Existing fibers are checked immediately; later starts and HMR activations + * are checked through Cordis lifecycle events. + * @param ctx - invariant child context that owns the observers. + * @param fail - reporter bound to the package that owns the plugin. + * @param contract - expected runtime facts for the package plugin. + * @returns nothing after lifecycle observers are installed. + */ +export function observePluginInvariant( + ctx: Context, + fail: InvariantFailure, + contract: PluginInvariantContract, +): void { + const callback = contract.plugin === undefined ? undefined : ctx.registry.resolve(contract.plugin) + if (contract.plugin !== undefined && callback === undefined) { + fail('invariant contract does not identify a Cordis plugin') + } + + const inspect = (fiber: Fiber): void => { + const matches = callback === undefined + ? fiber.runtime?.name === contract.name + : fiber.runtime?.callback === callback + if (!matches || fiber.state !== FiberState.ACTIVE) return + if (callback !== undefined && fiber.name !== contract.name) { + fail(`active plugin name must be ${JSON.stringify(contract.name)}, got ${JSON.stringify(fiber.name)}`) + } + const injections = new Set(Object.keys(fiber.inject)) + for (const service of contract.inject ?? []) { + if (!injections.has(service)) fail(`active plugin must inject ${JSON.stringify(service)}`) + } + + const effectLabels = collectEffectLabels(fiber) + for (const requirement of contract.effects ?? []) { + const alternatives = typeof requirement === 'string' ? [requirement] : requirement + if (!alternatives.some(label => effectLabels.has(label))) { + fail(`active plugin must own effect ${alternatives.map(label => JSON.stringify(label)).join(' or ')}`) + } + } + for (const service of contract.services ?? []) { + const provided = Reflect.ownKeys(fiber.ctx.reflect.store).some((key) => { + const implementation = fiber.ctx.reflect.store[key as symbol] + return implementation?.fiber === fiber && implementation.name === service + }) + if (!provided) fail(`active plugin must provide service ${JSON.stringify(service)}`) + } + const message = contract.validate?.(fiber, effectLabels) + if (message !== undefined) fail(message) + } + + if (contract.plugin === undefined) { + for (const runtime of ctx.registry.values()) { + for (const fiber of runtime.fibers) inspect(fiber) + } + } else { + for (const fiber of ctx.registry.get(contract.plugin)?.fibers ?? []) inspect(fiber) + } + ctx.on('internal/plugin', inspect, { global: true }) + ctx.on('internal/status', inspect, { global: true }) +} + +/** + * Validate every current and future implementation bound to one Cordis service. + * @param ctx - invariant child context that owns the service observer. + * @param fail - reporter bound to the package that owns the service seam. + * @param serviceName - Cordis service name to observe. + * @param validate - returns the violated contract, or `undefined` for a valid implementation. + * @returns nothing after the current binding is checked and the observer is installed. + */ +export function observeServiceInvariant( + ctx: Context, + fail: InvariantFailure, + serviceName: string, + validate: (value: unknown) => string | undefined, +): void { + const inspect = (value: unknown): void => { + if (value === undefined) return + const message = validate(value) + if (message !== undefined) fail(message) + } + const current: unknown = ctx.get(serviceName) + inspect(current) + ctx.on('internal/service', (name, value: unknown) => { + if (name === serviceName) inspect(value) + }, { global: true }) +} + +/** Structural runtime surface required from a Cordis service implementation. */ +export interface ServiceShapeInvariant { + /** Members that must be callable. */ + readonly methods: readonly string[] + /** Members that must be non-empty strings. */ + readonly stringProperties?: readonly string[] +} + +/** + * Describe the first missing member in a structural service implementation. + * This deliberately accepts test doubles and third-party implementations that + * satisfy the seam without inheriting the first-party abstract service class. + * @param value - candidate service implementation. + * @param shape - callable and string members owned by the service package. + * @returns the violated shape, or `undefined` when the candidate conforms. + */ +export function serviceShapeViolation( + value: unknown, + shape: ServiceShapeInvariant, +): string | undefined { + if ((typeof value !== 'object' && typeof value !== 'function') || value === null) { + return 'service implementation must be an object' + } + const record = value as Record + for (const method of shape.methods) { + if (typeof record[method] !== 'function') return `service implementation must expose method ${JSON.stringify(method)}` + } + for (const property of shape.stringProperties ?? []) { + if (typeof record[property] !== 'string' || record[property].length === 0) { + return `service implementation must expose non-empty string ${JSON.stringify(property)}` + } + } + return undefined +} + +/** + * Report a failed package-owned synchronous invariant. + * @param fail - reporter bound to the package that owns the assertion. + * @param condition - condition that must hold. + * @param message - violated contract when `condition` is false. + * @returns nothing when the condition holds. + */ +export function assertInvariant( + fail: InvariantFailure, + condition: unknown, + message: string, +): void { + if (!condition) fail(message) +} + /** Internal effect shape used to join child startup before a companion loads. */ interface PendingInvariantRegistration extends PromiseLike<() => void> { (): void | Promise diff --git a/packages/support/invariants/src/invariant.ts b/packages/support/invariants/src/invariant.ts index b6b4d8ae48..bd9efcb74c 100644 --- a/packages/support/invariants/src/invariant.ts +++ b/packages/support/invariants/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-invariants`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-invariants/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-invariants`. @module @deepseek-ai/dsh-invariants/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from './index.ts' +import InvariantService, { observePluginInvariant, type InvariantInstaller } from './index.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-invariants' @@ -17,8 +10,19 @@ export const name = 'invariants-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + plugin: InvariantService, + name: 'InvariantService', + effects: [ + 'ctx.provide("invariants")', + ], + services: [ + 'invariants', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +31,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/support/invariants/tests/service.spec.ts b/packages/support/invariants/tests/service.spec.ts index afeae22fb7..44f8d61139 100644 --- a/packages/support/invariants/tests/service.spec.ts +++ b/packages/support/invariants/tests/service.spec.ts @@ -1,10 +1,20 @@ import { describe, expect, it, vi } from 'vitest' import { Context, Service } from 'cordis' -import InvariantService, { InvariantError, type Config } from '@deepseek-ai/dsh-invariants' +import InvariantService, { + InvariantError, + assertInvariant, + observePluginInvariant, + observeServiceInvariant, + serviceShapeViolation, + type Config, + type InvariantInstaller, + type PluginInvariantContract, +} from '@deepseek-ai/dsh-invariants' declare module 'cordis' { interface Context { invariantProbe: InvariantProbeService + watchedInvariantProbe: WatchedInvariantProbeService } interface Events { @@ -18,6 +28,12 @@ class InvariantProbeService extends Service { } } +class WatchedInvariantProbeService extends Service { + constructor(ctx: Context) { + super(ctx, 'watchedInvariantProbe') + } +} + interface RuntimeRegistration extends PromiseLike<() => void> { (): void | Promise } @@ -264,3 +280,207 @@ describe('InvariantService lifecycle', () => { expect(() => service.register('@deepseek-ai/dsh-session', () => {})).toThrow(/inactive/i) }) }) + +describe('package-owned invariant helpers', () => { + async function registerInstaller( + ctx: Context, + packageName: string, + installer: InvariantInstaller, + ): Promise<() => void> { + const registration = runtimeRegistration(ctx.invariants.register(packageName, installer)) + const dispose = await Promise.resolve(registration) + return dispose + } + + function effectPlugin(options: { + name?: string + inject?: string[] + effect?: string + service?: string + } = {}) { + return { + name: options.name ?? 'effect-probe', + inject: options.inject ?? [], + apply(ctx: Context) { + if (options.service !== undefined) ctx.provide(options.service, {}) + if (options.effect !== undefined) { + ctx.effect(() => { + ctx.effect(() => () => {}, `${options.effect}.child`) + return () => {} + }, options.effect) + } + }, + } + } + + async function expectPluginViolation( + contract: PluginInvariantContract, + plugin: ReturnType, + message: RegExp, + ): Promise { + const { ctx } = await setup() + await registerInstaller(ctx, `@deepseek-ai/${contract.name}`, (child, fail) => { + observePluginInvariant(child, fail, contract) + }) + await expect(Promise.resolve(ctx.plugin(plugin))).rejects.toThrow(message) + } + + it('checks existing and later plugin fibers, including nested effects and alternatives', async () => { + const { ctx } = await setup() + await ctx.plugin(InvariantProbeService) + const plugin = effectPlugin({ + inject: ['invariantProbe'], + effect: 'probe.effect', + service: 'pluginProbe', + }) + await ctx.plugin(plugin) + const validated = vi.fn(() => undefined) + await registerInstaller(ctx, '@deepseek-ai/dsh-existing-probe', (child, fail) => { + observePluginInvariant(child, fail, { + plugin, + name: 'effect-probe', + inject: ['invariantProbe'], + effects: [['missing.effect', 'probe.effect.child']], + services: ['pluginProbe'], + validate: validated, + }) + }) + expect(validated).toHaveBeenCalledOnce() + + const later = effectPlugin({ name: 'later-probe', effect: 'later.effect' }) + await registerInstaller(ctx, '@deepseek-ai/dsh-later-probe', (child, fail) => { + observePluginInvariant(child, fail, { + plugin: later, + name: 'later-probe', + effects: ['later.effect'], + }) + }) + await ctx.plugin(later) + }) + + it('matches package plugins by Cordis name without importing their callback', async () => { + const { ctx } = await setup() + const plugin = { + name: 'name-only-probe', + apply(pluginCtx: Context) { + pluginCtx.effect(() => () => {}, 'name-only.effect') + pluginCtx.inject([], () => {}) + }, + } + await registerInstaller(ctx, '@deepseek-ai/dsh-name-only-probe', (child, fail) => { + observePluginInvariant(child, fail, { + name: 'name-only-probe', + effects: ['name-only.effect'], + }) + }) + await ctx.plugin(plugin) + }) + + it('rejects a contract that does not identify a plugin', async () => { + const { ctx } = await setup() + const registration = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-invalid-plugin', (child, fail) => { + observePluginInvariant(child, fail, { + plugin: {} as never, + name: 'invalid-plugin', + }) + })) + await expect(Promise.resolve(registration)).rejects.toThrow(/does not identify a Cordis plugin/) + }) + + it('rejects wrong plugin names, missing injections, effects, services, and custom checks', async () => { + const wrongName = effectPlugin({ name: 'actual-name', effect: 'probe.effect' }) + await expectPluginViolation({ + plugin: wrongName, + name: 'expected-name', + }, wrongName, /plugin name must be "expected-name"/) + + const missingInjection = effectPlugin({ effect: 'probe.effect' }) + await expectPluginViolation({ + plugin: missingInjection, + name: 'effect-probe', + inject: ['missingService'], + }, missingInjection, /must inject "missingService"/) + + const missingEffect = effectPlugin() + await expectPluginViolation({ + plugin: missingEffect, + name: 'effect-probe', + effects: [['first.effect', 'second.effect']], + }, missingEffect, /must own effect "first.effect" or "second.effect"/) + + const missingService = effectPlugin({ effect: 'probe.effect' }) + await expectPluginViolation({ + plugin: missingService, + name: 'effect-probe', + services: ['missingService'], + }, missingService, /must provide service "missingService"/) + + const invalidCustom = effectPlugin({ effect: 'probe.effect' }) + await expectPluginViolation({ + plugin: invalidCustom, + name: 'effect-probe', + validate: () => 'custom plugin contract failed', + }, invalidCustom, /custom plugin contract failed/) + }) + + it('checks existing and future service implementations while ignoring unrelated changes', async () => { + const existing = await setup() + await existing.ctx.plugin(WatchedInvariantProbeService) + await registerInstaller(existing.ctx, '@deepseek-ai/dsh-existing-service', (child, fail) => { + observeServiceInvariant(child, fail, 'watchedInvariantProbe', value => ( + value instanceof WatchedInvariantProbeService ? undefined : 'wrong watched service' + )) + }) + + const future = await setup() + await registerInstaller(future.ctx, '@deepseek-ai/dsh-future-service', (child, fail) => { + observeServiceInvariant(child, fail, 'watchedInvariantProbe', value => ( + value instanceof WatchedInvariantProbeService ? undefined : 'wrong watched service' + )) + }) + await future.ctx.plugin(InvariantProbeService) + await future.ctx.plugin(WatchedInvariantProbeService) + + const invalid = await setup() + await registerInstaller(invalid.ctx, '@deepseek-ai/dsh-invalid-service', (child, fail) => { + observeServiceInvariant(child, fail, 'watchedInvariantProbe', () => 'wrong watched service') + }) + await expect(Promise.resolve(invalid.ctx.plugin(WatchedInvariantProbeService))) + .rejects.toThrow(/wrong watched service/) + }) + + it('reports synchronous package assertions through the bound failure reporter', async () => { + const { ctx } = await setup() + const valid = await registerInstaller(ctx, '@deepseek-ai/dsh-valid-assertion', (_child, fail) => { + assertInvariant(fail, true, 'must stay true') + }) + valid() + + const invalid = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-invalid-assertion', (_child, fail) => { + assertInvariant(fail, false, 'must stay true') + })) + await expect(Promise.resolve(invalid)).rejects.toThrow(/must stay true/) + }) + + it('accepts structural service implementations and test doubles', () => { + expect(serviceShapeViolation({ kind: 'probe', run() {} }, { + methods: ['run'], + stringProperties: ['kind'], + })).toBeUndefined() + expect(serviceShapeViolation(Object.assign(() => {}, { run() {} }), { + methods: ['run'], + })).toBeUndefined() + }) + + it.each([ + { value: null, message: 'service implementation must be an object' }, + { value: 42, message: 'service implementation must be an object' }, + { value: {}, message: 'service implementation must expose method "run"' }, + { value: { run() {}, kind: '' }, message: 'service implementation must expose non-empty string "kind"' }, + ])('rejects invalid structural service implementations: $message', ({ value, message }) => { + expect(serviceShapeViolation(value, { + methods: ['run'], + stringProperties: ['kind'], + })).toBe(message) + }) +}) diff --git a/packages/support/llm-replay/src/invariant.ts b/packages/support/llm-replay/src/invariant.ts index 50295cbedf..9c82c33686 100644 --- a/packages/support/llm-replay/src/invariant.ts +++ b/packages/support/llm-replay/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-llm-replay`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-llm-replay/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-llm-replay`. @module @deepseek-ai/dsh-llm-replay/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-llm-replay' @@ -17,8 +10,21 @@ export const name = 'llm-replay-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'llm-replay', + inject: [ + 'llm', + ], + effects: [ + [ + 'llm.registerAdapter()', + 'ctx.on("llm/stream")', + ], + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +33,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/support/loader-smoke/src/invariant.ts b/packages/support/loader-smoke/src/invariant.ts index 9265a5f8b4..826d33dd30 100644 --- a/packages/support/loader-smoke/src/invariant.ts +++ b/packages/support/loader-smoke/src/invariant.ts @@ -1,14 +1,8 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-loader-smoke`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-loader-smoke/invariant - */ +/** Package-owned runtime contracts for @deepseek-ai/dsh-loader-smoke. @module @deepseek-ai/dsh-loader-smoke/invariant */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-loader-smoke' @@ -17,8 +11,25 @@ export const name = 'loader-smoke-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert default source mode and plain-Node built-artifact launch resolution. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const { resolveExampleLaunch, resolveExampleMode } = await import('./index.ts') + assertInvariant(fail, resolveExampleMode('') === 'src', + 'an empty example-mode selection must preserve source-mode development') + const launch = resolveExampleLaunch({ + srcBin: '/workspace/probe/src/bin.ts', + mode: 'lib', + }) + assertInvariant(fail, + launch.command === process.execPath + && launch.args.length === 1 + && launch.args[0] === '/workspace/probe/lib/bin.js' + && launch.env.TSX_TSCONFIG_PATH === undefined, + 'built example launches must use plain Node, the derived lib entry, and no tsx paths map') + return () => {} + }, 'loader-smoke: validate source and built launch resolution') +} /** * Register this package's invariant companion. diff --git a/packages/tasks/tasks/src/invariant.ts b/packages/tasks/tasks/src/invariant.ts index 468fe664b9..ce3bf24175 100644 --- a/packages/tasks/tasks/src/invariant.ts +++ b/packages/tasks/tasks/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-tasks`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-tasks/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tasks`. @module @deepseek-ai/dsh-tasks/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tasks' @@ -17,8 +10,19 @@ export const name = 'tasks-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'TaskService', + effects: [ + 'ctx.provide("tasks")', + 'tasks teardown', + ], + services: [ + 'tasks', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +31,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/tasks/tool-tasks/src/invariant.ts b/packages/tasks/tool-tasks/src/invariant.ts index fded38c895..e4af0931d2 100644 --- a/packages/tasks/tool-tasks/src/invariant.ts +++ b/packages/tasks/tool-tasks/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-tool-tasks`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-tool-tasks/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-tasks`. @module @deepseek-ai/dsh-tool-tasks/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-tasks' @@ -17,8 +10,20 @@ export const name = 'tool-tasks-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'tool-tasks', + inject: [ + 'tools', + 'tasks', + 'systemPrompt', + ], + effects: [ + 'tools.register()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +32,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/timeout/timeout-policy/src/invariant.ts b/packages/timeout/timeout-policy/src/invariant.ts index 9e7b5b7d4b..b0e564dbbd 100644 --- a/packages/timeout/timeout-policy/src/invariant.ts +++ b/packages/timeout/timeout-policy/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-timeout-policy`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-timeout-policy/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-timeout-policy`. @module @deepseek-ai/dsh-timeout-policy/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-timeout-policy' @@ -17,8 +10,18 @@ export const name = 'timeout-policy-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'timeout-policy', + inject: [ + 'tools', + ], + effects: [ + 'ctx.on("tools/execute")', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/todo/tool-todo/src/invariant.ts b/packages/todo/tool-todo/src/invariant.ts index a5980342f3..9ad57156f4 100644 --- a/packages/todo/tool-todo/src/invariant.ts +++ b/packages/todo/tool-todo/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-tool-todo`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-tool-todo/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-todo`. @module @deepseek-ai/dsh-tool-todo/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-todo' @@ -17,8 +10,18 @@ export const name = 'tool-todo-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'tool-todo', + inject: [ + 'tools', + ], + effects: [ + 'tools.register()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/ui/acp/src/invariant.ts b/packages/ui/acp/src/invariant.ts index 2c081fc48c..1b98d6630c 100644 --- a/packages/ui/acp/src/invariant.ts +++ b/packages/ui/acp/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-acp`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-acp/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-acp`. @module @deepseek-ai/dsh-acp/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-acp' @@ -17,8 +10,25 @@ export const name = 'acp-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'acp', + inject: [ + 'agents', + 'sessionPersistence', + 'tools', + 'userInteraction', + 'llm', + 'systemPrompt', + ], + effects: [ + 'userInteraction.registerProvider()', + 'ctx.on("session/event")', + 'acp.connection', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +37,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/ui/app-boot/src/config-path.ts b/packages/ui/app-boot/src/config-path.ts new file mode 100644 index 0000000000..bdfb933feb --- /dev/null +++ b/packages/ui/app-boot/src/config-path.ts @@ -0,0 +1,23 @@ +/** Snapshot-aware application configuration path selection. @module @deepseek-ai/dsh-app-boot/config-path */ + +import { basename, dirname, resolve } from 'node:path' + +/** + * Resolve the config to boot. Replay swaps a `cordis.yml` basename for + * `cordis.snapshot.yml` in the same directory; every other mode keeps the path. + * @param configPath - requested config path, absolute or relative to `cwd`. + * @param snapshotMode - bin `$DSH_SNAPSHOT`; only `replay` swaps the basename. + * @param cwd - base for a relative `configPath`. + * @returns the absolute path of the config to boot. + */ +export function resolveConfigPath( + configPath: string, + snapshotMode: string | undefined, + cwd: string = process.cwd(), +): string { + const absolute = resolve(cwd, configPath) + if (snapshotMode !== 'replay') return absolute + const dir = dirname(absolute) + const replayName = basename(absolute).replace(/cordis\.ya?ml$/, 'cordis.snapshot.yml') + return resolve(dir, replayName) +} diff --git a/packages/ui/app-boot/src/index.ts b/packages/ui/app-boot/src/index.ts index 91ab0d3a2f..e929449287 100644 --- a/packages/ui/app-boot/src/index.ts +++ b/packages/ui/app-boot/src/index.ts @@ -6,29 +6,12 @@ */ import { pathToFileURL } from 'node:url' -import { basename, dirname, resolve } from 'node:path' +import { dirname, resolve } from 'node:path' import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' import Include from '@cordisjs/plugin-include' -/** - * Resolve the config to boot. Replay swaps a `cordis.yml` basename for - * `cordis.snapshot.yml` in the same directory; every other mode keeps the path. - * @param configPath - the requested config path (absolute, or relative to `cwd`). - * @param snapshotMode - the bin's `$DSH_SNAPSHOT` value; only `'replay'` swaps the - * basename. - * @param cwd - the base a relative `configPath` resolves against. - * @returns the absolute path of the config to boot. - */ -export function resolveConfigPath( - configPath: string, snapshotMode: string | undefined, cwd: string = process.cwd(), -): string { - const absolute = resolve(cwd, configPath) - if (snapshotMode !== 'replay') return absolute - const dir = dirname(absolute) - const replayName = basename(absolute).replace(/cordis\.ya?ml$/, 'cordis.snapshot.yml') - return resolve(dir, replayName) -} +export { resolveConfigPath } from './config-path.ts' /** * Load the optional gitignored `.env` from `dir`. Missing files fall back to the diff --git a/packages/ui/app-boot/src/invariant.ts b/packages/ui/app-boot/src/invariant.ts index 498d967799..30e2cfea69 100644 --- a/packages/ui/app-boot/src/invariant.ts +++ b/packages/ui/app-boot/src/invariant.ts @@ -1,14 +1,9 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-app-boot`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-app-boot/invariant - */ +/** Package-owned runtime contracts for @deepseek-ai/dsh-app-boot. @module @deepseek-ai/dsh-app-boot/invariant */ /* jscpd:ignore-start */ +import { resolve } from 'node:path' import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-app-boot' @@ -17,8 +12,20 @@ export const name = 'app-boot-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert ordinary and replay config-path selection. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const { resolveConfigPath } = await import('./config-path.ts') + const cwd = '/tmp/dsh-app-boot-invariant' + const ordinary = resolveConfigPath('cordis.yml', undefined, cwd) + const replay = resolveConfigPath('cordis.yml', 'replay', cwd) + assertInvariant(fail, ordinary === resolve(cwd, 'cordis.yml'), + 'ordinary app boot must retain the requested config basename') + assertInvariant(fail, replay === resolve(cwd, 'cordis.snapshot.yml'), + 'snapshot replay must select cordis.snapshot.yml in the requested config directory') + return () => {} + }, 'app-boot: validate ordinary and replay config selection') +} /** * Register this package's invariant companion. diff --git a/packages/ui/jsonrpc/src/invariant.ts b/packages/ui/jsonrpc/src/invariant.ts index 552c312481..aad2ac9f41 100644 --- a/packages/ui/jsonrpc/src/invariant.ts +++ b/packages/ui/jsonrpc/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-jsonrpc`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-jsonrpc/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-jsonrpc`. @module @deepseek-ai/dsh-jsonrpc/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-jsonrpc' @@ -17,8 +10,18 @@ export const name = 'jsonrpc-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'jsonrpc', + inject: [ + 'agents', + ], + effects: [ + 'jsonrpc.serve', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/ui/permission/src/invariant.ts b/packages/ui/permission/src/invariant.ts index 1a774e2e4c..24191712bf 100644 --- a/packages/ui/permission/src/invariant.ts +++ b/packages/ui/permission/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-permission`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-permission/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-permission`. @module @deepseek-ai/dsh-permission/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-permission' @@ -17,8 +10,22 @@ export const name = 'permission-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'PermissionService', + inject: [ + 'bash', + 'approval', + ], + effects: [ + 'ctx.provide("permission")', + ], + services: [ + 'permission', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +34,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/ui/permission/tests/permission.spec.ts b/packages/ui/permission/tests/permission.spec.ts index 50b630bfd1..cf08a0bafb 100644 --- a/packages/ui/permission/tests/permission.spec.ts +++ b/packages/ui/permission/tests/permission.spec.ts @@ -12,7 +12,12 @@ async function mounted(options: { approvalDefault?: ApprovalPolicy | undefined } = {}): Promise { const ctx = new Context() - ctx.provide('bash', { sandboxMode: 'bashDefault' in options ? options.bashDefault : 'workspace-write' }) + ctx.provide('bash', { + sandboxMode: 'bashDefault' in options ? options.bashDefault : 'workspace-write', + resolve() { throw new Error('permission tests do not execute bash') }, + run() { throw new Error('permission tests do not execute bash') }, + start() { throw new Error('permission tests do not execute bash') }, + }) ctx.provide('approval', { config: { policy: 'approvalDefault' in options ? options.approvalDefault : 'ask' } }) await ctx.plugin(PermissionService, options.config ?? {}) return ctx diff --git a/packages/ui/stdio/src/invariant.ts b/packages/ui/stdio/src/invariant.ts index 443440a215..56bf12cda3 100644 --- a/packages/ui/stdio/src/invariant.ts +++ b/packages/ui/stdio/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-stdio`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-stdio/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-stdio`. @module @deepseek-ai/dsh-stdio/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-stdio' @@ -17,8 +10,20 @@ export const name = 'stdio-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'ui-stdio', + inject: [ + 'agents', + 'userInteraction', + ], + effects: [ + 'ctx.on("session/event")', + 'userInteraction.registerProvider()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +32,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/ui/tool-ask-user/src/invariant.ts b/packages/ui/tool-ask-user/src/invariant.ts index eebb1ced42..2b18cad1f3 100644 --- a/packages/ui/tool-ask-user/src/invariant.ts +++ b/packages/ui/tool-ask-user/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-tool-ask-user`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-tool-ask-user/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-ask-user`. @module @deepseek-ai/dsh-tool-ask-user/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-ask-user' @@ -17,8 +10,19 @@ export const name = 'tool-ask-user-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'tool-ask-user', + inject: [ + 'tools', + 'userInteraction', + ], + effects: [ + 'tools.register()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +31,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/ui/tui/src/invariant.ts b/packages/ui/tui/src/invariant.ts index cba5fd9d2e..d3014146d7 100644 --- a/packages/ui/tui/src/invariant.ts +++ b/packages/ui/tui/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-tui`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-tui/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tui`. @module @deepseek-ai/dsh-tui/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tui' @@ -17,8 +10,21 @@ export const name = 'tui-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'ui-tui', + inject: [ + 'agents', + 'userInteraction', + 'tools', + ], + effects: [ + 'ctx.on("session/event")', + 'userInteraction.registerProvider()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +33,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/ui/tui/tests/tui.snapshot.ts b/packages/ui/tui/tests/tui.snapshot.ts index d60b317965..11ec395d90 100644 --- a/packages/ui/tui/tests/tui.snapshot.ts +++ b/packages/ui/tui/tests/tui.snapshot.ts @@ -94,7 +94,7 @@ async function disposeSnapshot(harness: SnapshotHarness): Promise { async function configureAdvancedTools(ctx: Context): Promise { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry, { mode: 'code' }) - ctx.provide('workflows', {} as never) + ctx.provide('workflows', { start() {} } as never) await ctx.plugin(ToolWorkflow, { toolName: 'workflow', maxResultChars: 50_000 }) await ctx.plugin(ToolCordis, { vmTimeoutMs: 5_000 }) } diff --git a/packages/ui/user-approval/src/invariant.ts b/packages/ui/user-approval/src/invariant.ts index 73dee4f9f1..9a905be06f 100644 --- a/packages/ui/user-approval/src/invariant.ts +++ b/packages/ui/user-approval/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-user-approval`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-user-approval/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-user-approval`. @module @deepseek-ai/dsh-user-approval/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-user-approval' @@ -17,8 +10,19 @@ export const name = 'user-approval-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'ApprovalService', + effects: [ + 'ctx.provide("approval")', + 'ctx.on("agent/pre-step")', + ], + services: [ + 'approval', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +31,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/ui/user-interaction/src/invariant.ts b/packages/ui/user-interaction/src/invariant.ts index 262681fc06..85029b692b 100644 --- a/packages/ui/user-interaction/src/invariant.ts +++ b/packages/ui/user-interaction/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-user-interaction`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-user-interaction/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-user-interaction`. @module @deepseek-ai/dsh-user-interaction/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-user-interaction' @@ -17,8 +10,18 @@ export const name = 'user-interaction-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'UserInteractionService', + effects: [ + 'ctx.provide("userInteraction")', + ], + services: [ + 'userInteraction', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/util/brand/src/invariant.ts b/packages/util/brand/src/invariant.ts index e932e7e35e..5ac98489cb 100644 --- a/packages/util/brand/src/invariant.ts +++ b/packages/util/brand/src/invariant.ts @@ -1,14 +1,8 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-brand`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-brand/invariant - */ +/** Package-owned runtime contract for @deepseek-ai/dsh-brand. @module @deepseek-ai/dsh-brand/invariant */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-brand' @@ -17,8 +11,15 @@ export const name = 'brand-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert that the nominal-type primitive remains erased at runtime. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const brandRuntime = await import('./index.ts') + assertInvariant(fail, Object.keys(brandRuntime).length === 0, + 'the branded-id primitive must remain type-only with no runtime exports') + return () => {} + }, 'brand: validate type-only runtime erasure') +} /** * Register this package's invariant companion. diff --git a/packages/util/home/src/invariant.ts b/packages/util/home/src/invariant.ts index 5874a57c1c..651d877e00 100644 --- a/packages/util/home/src/invariant.ts +++ b/packages/util/home/src/invariant.ts @@ -1,14 +1,9 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-home`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-home/invariant - */ +/** Package-owned runtime contracts for @deepseek-ai/dsh-home. @module @deepseek-ai/dsh-home/invariant */ /* jscpd:ignore-start */ +import { resolve } from 'node:path' import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-home' @@ -17,8 +12,19 @@ export const name = 'home-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert the canonical environment key and configured-path precedence. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const { DSH_HOME_ENV, resolveDshHome } = await import('./index.ts') + const environmentKey: string = DSH_HOME_ENV + assertInvariant(fail, environmentKey === ['DSH', 'HOME'].join('_'), + 'the canonical Harness home environment key must remain DSH_HOME') + const configured = 'relative-invariant-home' + assertInvariant(fail, resolveDshHome(configured) === resolve(configured), + 'an explicitly configured Harness home must normalize to an absolute path') + return () => {} + }, 'home: validate canonical DSH home resolution') +} /** * Register this package's invariant companion. diff --git a/packages/util/paths/src/invariant.ts b/packages/util/paths/src/invariant.ts index c2cedfbb0d..ce0ddb653c 100644 --- a/packages/util/paths/src/invariant.ts +++ b/packages/util/paths/src/invariant.ts @@ -1,14 +1,10 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-paths`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-paths/invariant - */ +/** Package-owned runtime contracts for @deepseek-ai/dsh-paths. @module @deepseek-ai/dsh-paths/invariant */ /* jscpd:ignore-start */ +import { homedir } from 'node:os' +import { join, resolve } from 'node:path' import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-paths' @@ -17,8 +13,19 @@ export const name = 'paths-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert tilde expansion and explicit-over-environment home precedence. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const { DSH_HOME_ENV, expandHomePath, resolveDshHome } = await import('./index.ts') + assertInvariant(fail, expandHomePath('~/invariant-probe') === join(homedir(), 'invariant-probe'), + 'supported tilde prefixes must expand against the operating-system home') + const configured = 'relative-invariant-home' + const resolved = resolveDshHome(configured, { [DSH_HOME_ENV]: '/ignored-environment-home' }) + assertInvariant(fail, resolved === resolve(configured), + 'an explicit DSH home must override the environment and normalize to an absolute path') + return () => {} + }, 'paths: validate DSH home resolution') +} /** * Register this package's invariant companion. diff --git a/packages/util/retention/src/invariant.ts b/packages/util/retention/src/invariant.ts index 7516f9e5ed..380e4e3d4b 100644 --- a/packages/util/retention/src/invariant.ts +++ b/packages/util/retention/src/invariant.ts @@ -1,14 +1,8 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-retention`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-retention/invariant - */ +/** Package-owned runtime contracts for @deepseek-ai/dsh-retention. @module @deepseek-ai/dsh-retention/invariant */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-retention' @@ -17,8 +11,26 @@ export const name = 'retention-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert exact head-retention accounting after the budget is exceeded. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const { ItemRetainer } = await import('./index.ts') + const retainer = new ItemRetainer({ kind: 'head', maxItems: 2 }) + retainer.push('first') + retainer.push('second') + retainer.push('third') + const result = retainer.finish() + assertInvariant(fail, + result.items.join(',') === 'first,second' + && result.seen === 3 + && result.kept === 2 + && result.truncated + && result.omitted.kind === 'exact' + && result.omitted.count === 1, + 'head retention must keep the prefix and report exact seen, kept, and omitted counts') + return () => {} + }, 'retention: validate exact head accounting') +} /** * Register this package's invariant companion. diff --git a/packages/util/timeout/src/invariant.ts b/packages/util/timeout/src/invariant.ts index 15140bb880..8302380eaa 100644 --- a/packages/util/timeout/src/invariant.ts +++ b/packages/util/timeout/src/invariant.ts @@ -1,14 +1,8 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-timeout`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-timeout/invariant - */ +/** Package-owned runtime contracts for @deepseek-ai/dsh-timeout. @module @deepseek-ai/dsh-timeout/invariant */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-timeout' @@ -17,8 +11,21 @@ export const name = 'timeout-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Assert default-before-cap arithmetic and capability-code classification. */ +const install: InvariantInstaller = (ctx, fail) => { + ctx.effect(async () => { + const { clampTimeout, TimeoutReason, timeoutOf } = await import('./index.ts') + assertInvariant(fail, + clampTimeout(undefined, 50, 30) === 30 && clampTimeout(20, 50, 30) === 20, + 'timeout resolution must apply the default before capping and preserve smaller requests') + const reason = new TimeoutReason('INVARIANT_TIMEOUT', 25) + assertInvariant(fail, timeoutOf({ reason }, 'INVARIANT_TIMEOUT') === reason, + 'timeout classification must recover a matching capability-owned reason') + assertInvariant(fail, timeoutOf({ reason }, 'FOREIGN_TIMEOUT') === undefined, + 'timeout classification must reject a reason owned by another capability') + return () => {} + }, 'timeout: validate resolution and reason classification') +} /** * Register this package's invariant companion. diff --git a/packages/web/tool-web/src/invariant.ts b/packages/web/tool-web/src/invariant.ts index 008fe2f5e1..1cceb0f166 100644 --- a/packages/web/tool-web/src/invariant.ts +++ b/packages/web/tool-web/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-tool-web`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-tool-web/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-web`. @module @deepseek-ai/dsh-tool-web/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-web' @@ -17,8 +10,20 @@ export const name = 'tool-web-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'tool-web', + inject: [ + 'tools', + 'web', + 'systemPrompt', + ], + effects: [ + 'tools.register()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +32,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/web/web-fetch-local/src/invariant.ts b/packages/web/web-fetch-local/src/invariant.ts index ef61e2a611..5a29395715 100644 --- a/packages/web/web-fetch-local/src/invariant.ts +++ b/packages/web/web-fetch-local/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-web-fetch-local`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-web-fetch-local/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-web-fetch-local`. @module @deepseek-ai/dsh-web-fetch-local/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-web-fetch-local' @@ -17,8 +10,18 @@ export const name = 'web-fetch-local-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'web-fetch-local', + inject: [ + 'web', + ], + effects: [ + 'web.registerProvider()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/web/web-search-deepseek/src/invariant.ts b/packages/web/web-search-deepseek/src/invariant.ts index 8781949be9..bfed9260dc 100644 --- a/packages/web/web-search-deepseek/src/invariant.ts +++ b/packages/web/web-search-deepseek/src/invariant.ts @@ -1,14 +1,10 @@ /** - * Generated invariant ownership companion for `@deepseek-ai/dsh-web-search-deepseek`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts + * Package-owned runtime contract checks for `@deepseek-ai/dsh-web-search-deepseek`. * @module @deepseek-ai/dsh-web-search-deepseek/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-web-search-deepseek' @@ -17,8 +13,18 @@ export const name = 'web-search-deepseek-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'web-search-deepseek', + inject: [ + 'web', + ], + effects: [ + 'web.registerProvider()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +33,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/web/web-search-exa/src/invariant.ts b/packages/web/web-search-exa/src/invariant.ts index a2dd956625..8d5da8d433 100644 --- a/packages/web/web-search-exa/src/invariant.ts +++ b/packages/web/web-search-exa/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-web-search-exa`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-web-search-exa/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-web-search-exa`. @module @deepseek-ai/dsh-web-search-exa/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-web-search-exa' @@ -17,8 +10,18 @@ export const name = 'web-search-exa-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'web-search-exa', + inject: [ + 'web', + ], + effects: [ + 'web.registerProvider()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/web/web-search-perplexity/src/invariant.ts b/packages/web/web-search-perplexity/src/invariant.ts index fe82c79dae..7669f6e53a 100644 --- a/packages/web/web-search-perplexity/src/invariant.ts +++ b/packages/web/web-search-perplexity/src/invariant.ts @@ -1,14 +1,10 @@ /** - * Generated invariant ownership companion for `@deepseek-ai/dsh-web-search-perplexity`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts + * Package-owned runtime contract checks for `@deepseek-ai/dsh-web-search-perplexity`. * @module @deepseek-ai/dsh-web-search-perplexity/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-web-search-perplexity' @@ -17,8 +13,18 @@ export const name = 'web-search-perplexity-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'web-search-perplexity', + inject: [ + 'web', + ], + effects: [ + 'web.registerProvider()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +33,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/web/web/src/invariant.ts b/packages/web/web/src/invariant.ts index b9b1b0d45d..395679cf36 100644 --- a/packages/web/web/src/invariant.ts +++ b/packages/web/web/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-web`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-web/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-web`. @module @deepseek-ai/dsh-web/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-web' @@ -17,8 +10,18 @@ export const name = 'web-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'WebService', + effects: [ + 'ctx.provide("web")', + ], + services: [ + 'web', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +30,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/workflow/tool-workflow/src/invariant.ts b/packages/workflow/tool-workflow/src/invariant.ts index cd1f0e475b..966e4b3613 100644 --- a/packages/workflow/tool-workflow/src/invariant.ts +++ b/packages/workflow/tool-workflow/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-tool-workflow`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-tool-workflow/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-workflow`. @module @deepseek-ai/dsh-tool-workflow/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-workflow' @@ -17,8 +10,20 @@ export const name = 'tool-workflow-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'tool-workflow', + inject: [ + 'tools', + 'workflows', + 'systemPrompt', + ], + effects: [ + 'tools.register()', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +32,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/workflow/workflow-workerthread/src/invariant.ts b/packages/workflow/workflow-workerthread/src/invariant.ts index 6bcc40862c..27c0865d79 100644 --- a/packages/workflow/workflow-workerthread/src/invariant.ts +++ b/packages/workflow/workflow-workerthread/src/invariant.ts @@ -1,14 +1,10 @@ /** - * Generated invariant ownership companion for `@deepseek-ai/dsh-workflow-workerthread`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts + * Package-owned runtime contract checks for `@deepseek-ai/dsh-workflow-workerthread`. * @module @deepseek-ai/dsh-workflow-workerthread/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-workflow-workerthread' @@ -17,8 +13,21 @@ export const name = 'workflow-workerthread-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Install checks for this package's active plugin fibers. */ +const install: InvariantInstaller = (ctx, fail) => { + observePluginInvariant(ctx, fail, { + name: 'WorkerWorkflowEngine', + inject: [ + 'subagents', + ], + effects: [ + 'ctx.provide("workflows")', + ], + services: [ + 'workflows', + ], + }) +} /** * Register this package's invariant companion. @@ -27,4 +36,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/workflow/workflow/src/invariant.ts b/packages/workflow/workflow/src/invariant.ts index b552021ca4..a37d480e69 100644 --- a/packages/workflow/workflow/src/invariant.ts +++ b/packages/workflow/workflow/src/invariant.ts @@ -1,14 +1,7 @@ -/** - * Generated invariant ownership companion for `@deepseek-ai/dsh-workflow`. - * Replace this file with package-owned checks while preserving its registration. - * - * @generated scripts/gen-package-invariants.ts - * @module @deepseek-ai/dsh-workflow/invariant - */ +/** Package-owned runtime contract checks for `@deepseek-ai/dsh-workflow`. @module @deepseek-ai/dsh-workflow/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-workflow' @@ -17,8 +10,12 @@ export const name = 'workflow-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} +/** Validate every implementation bound to this package's service seam. */ +const install: InvariantInstaller = (ctx, fail) => { + observeServiceInvariant(ctx, fail, 'workflows', value => serviceShapeViolation(value, { + methods: ['start'], + })) +} /** * Register this package's invariant companion. @@ -27,4 +24,3 @@ const install: InvariantInstaller = () => {} */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/scripts/gen-package-invariants.ts b/scripts/gen-package-invariants.ts deleted file mode 100644 index d30ce5a3eb..0000000000 --- a/scripts/gen-package-invariants.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** Generate or verify package-owned invariant companion baselines. */ - -import { readFileSync, writeFileSync } from 'node:fs' -import { resolve } from 'node:path' -import { - GENERATED_INVARIANT_MARKER, - collectPackageInvariantViolations, - formatPackageInvariantViolation, - packageInvariantOwners, - renderBaselineInvariant, -} from './package-invariants.ts' - -const root = resolve(import.meta.dirname, '..') -const check = process.argv.includes('--check') - -if (!check) { - let generated = 0 - for (const owner of packageInvariantOwners(root)) { - const path = resolve(root, owner.sourcePath) - let current: string | undefined - try { - current = readFileSync(path, 'utf8') - } catch (error) { - if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error - } - if (current !== undefined && !current.includes(GENERATED_INVARIANT_MARKER)) continue - const expected = renderBaselineInvariant(owner) - if (current === expected) continue - writeFileSync(path, expected) - generated += 1 - } - console.log(`gen-package-invariants: wrote ${generated} generated baseline companion(s).`) -} - -const violations = collectPackageInvariantViolations(root) -if (violations.length > 0) { - console.error('verify-package-invariants: violations found:') - for (const violation of violations) { - console.error(` ${formatPackageInvariantViolation(root, violation)}`) - } - process.exit(1) -} - -console.log(`verify-package-invariants: ${packageInvariantOwners(root).length} package companion(s) conform.`) diff --git a/scripts/package-invariants.spec.ts b/scripts/package-invariants.spec.ts index 698969b3d7..6ef9fce852 100644 --- a/scripts/package-invariants.spec.ts +++ b/scripts/package-invariants.spec.ts @@ -4,8 +4,6 @@ import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { collectPackageInvariantViolations, - packageInvariantOwners, - renderBaselineInvariant, } from './package-invariants.ts' const roots: string[] = [] @@ -14,6 +12,18 @@ afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) }) +function handwrittenInvariant(packageName: string): string { + return ` +export const name = 'probe-invariant' +export const inject = ['invariants'] +const install = (_ctx: unknown, fail: (message: string) => never) => { + if (typeof ${JSON.stringify(packageName)} !== 'string') fail('package name must remain a string') +} +export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) => + Promise.resolve(ctx.invariants.register(${JSON.stringify(packageName)}, install)) +` +} + function fixture(options: { packageName?: string source?: string @@ -47,8 +57,7 @@ function fixture(options: { writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({ references: options.invariantReference === false ? [] : [{ path: '../../support/invariants' }], }, null, 2)}\n`) - const owner = packageInvariantOwners(root)[0]! - writeFileSync(join(dir, 'src/invariant.ts'), options.source ?? renderBaselineInvariant(owner)) + writeFileSync(join(dir, 'src/invariant.ts'), options.source ?? handwrittenInvariant(packageName)) writeFileSync( join(dir, 'tsdown.config.ts'), options.buildEntry === false ? "export default { entry: ['lib/types/index.js'] }\n" : "export default { entry: ['lib/types/index.js', 'lib/types/invariant.js'] }\n", @@ -56,8 +65,43 @@ function fixture(options: { return root } +function addConformingPackage(root: string, slug: string, packageName: string, source: string): void { + const dir = join(root, `packages/core/${slug}`) + mkdirSync(join(dir, 'src'), { recursive: true }) + writeFileSync(join(dir, 'package.json'), `${JSON.stringify({ + name: packageName, + exports: { + './invariant': { + types: './lib/types/invariant.d.ts', + default: './lib/invariant.js', + }, + }, + files: ['lib/invariant.js'], + peerDependencies: { '@deepseek-ai/dsh-invariants': '^0.0.1' }, + devDependencies: { '@deepseek-ai/dsh-invariants': 'workspace:^' }, + }, null, 2)}\n`) + writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({ + references: [{ path: '../../support/invariants' }], + }, null, 2)}\n`) + writeFileSync(join(dir, 'src/invariant.ts'), source) + writeFileSync(join(dir, 'tsdown.config.ts'), "export default { entry: ['lib/types/invariant.js'] }\n") +} + +function nameObservedInvariant(packageName: string, pluginName: string): string { + return ` +import { observePluginInvariant } from '@deepseek-ai/dsh-invariants' +export const name = 'probe-invariant' +export const inject = ['invariants'] +const install = (ctx: never, fail: (message: string) => never) => { + observePluginInvariant(ctx, fail, { name: ${JSON.stringify(pluginName)} }) +} +export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) => + Promise.resolve(ctx.invariants.register(${JSON.stringify(packageName)}, install)) +` +} + describe('package invariant gate', () => { - it('accepts a generated owner companion with publication metadata', () => { + it('accepts a hand-owned checking companion with publication metadata', () => { expect(collectPackageInvariantViolations(fixture())).toEqual([]) }) @@ -82,9 +126,10 @@ describe('package invariant gate', () => { export const name = 'probe-invariant' export const inject = ['invariants'] const selected = process.env.PACKAGE_NAME -export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) => { - ctx.invariants.register('@deepseek-ai/dsh-foreign', () => {}) - return ctx.invariants.register(selected!, () => {}) +const install = (_ctx: unknown, fail: (message: string) => never) => { fail('probe') } +export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) => { + ctx.invariants.register('@deepseek-ai/dsh-foreign', install) + return ctx.invariants.register(selected!, install) } ` const violations = collectPackageInvariantViolations(fixture({ source })) @@ -94,11 +139,52 @@ export const apply = (ctx: { invariants: { register(name: string, install: () => ])) }) - it('rejects edits to a generated baseline', () => { - const root = fixture() - const path = join(root, 'packages/core/probe/src/invariant.ts') - writeFileSync(path, `${renderBaselineInvariant(packageInvariantOwners(root)[0]!)}// stale\n`) + it('rejects generated markers and empty or reporter-free installers', () => { + const generated = fixture({ + source: `/** @generated scripts/gen-package-invariants.ts */\n${handwrittenInvariant('@deepseek-ai/dsh-probe')}`, + }) + expect(collectPackageInvariantViolations(generated).map(violation => violation.message)) + .toContain('invariant companions must be hand-owned and may not carry @generated markers') + + const empty = fixture({ + source: ` +export const name = 'probe-invariant' +export const inject = ['invariants'] +const install = () => {} +export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) => + Promise.resolve(ctx.invariants.register('@deepseek-ai/dsh-probe', install)) +`, + }) + expect(collectPackageInvariantViolations(empty).map(violation => violation.message)) + .toEqual(expect.arrayContaining([ + 'install function must contain a package-owned invariant check', + 'install function must accept the bound failure reporter as its second parameter', + ])) + + const unused = fixture({ + source: ` +export const name = 'probe-invariant' +export const inject = ['invariants'] +const install = (_ctx: unknown, _fail: (message: string) => never) => { void 0 } +export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) => + Promise.resolve(ctx.invariants.register('@deepseek-ai/dsh-probe', install)) +`, + }) + expect(collectPackageInvariantViolations(unused).map(violation => violation.message)) + .toContain('install function must use its bound failure reporter') + }) + + it('rejects duplicate name-based plugin observers across packages', () => { + const root = fixture({ + source: nameObservedInvariant('@deepseek-ai/dsh-probe', 'shared-runtime-name'), + }) + addConformingPackage( + root, + 'probe-two', + '@deepseek-ai/dsh-probe-two', + nameObservedInvariant('@deepseek-ai/dsh-probe-two', 'shared-runtime-name'), + ) expect(collectPackageInvariantViolations(root).map(violation => violation.message)) - .toContain('generated baseline is stale; run pnpm run gen-package-invariants') + .toContain('name-based plugin invariant "shared-runtime-name" is already owned by "@deepseek-ai/dsh-probe-two"') }) }) diff --git a/scripts/package-invariants.ts b/scripts/package-invariants.ts index 45fb64829f..6448888680 100644 --- a/scripts/package-invariants.ts +++ b/scripts/package-invariants.ts @@ -1,16 +1,13 @@ /** - * Package-invariant companion discovery, generation, and structural checks. + * Package-invariant companion discovery and structural checks. * The runtime registry stays product-independent; this gate makes ownership * exhaustive across packages without centralizing package checks. */ import { existsSync, globSync, readFileSync } from 'node:fs' -import { basename, dirname, relative, resolve, sep } from 'node:path' +import { dirname, relative, resolve, sep } from 'node:path' import ts from 'typescript' -/** Marker identifying baseline companions owned by this generator. */ -export const GENERATED_INVARIANT_MARKER = '@generated scripts/gen-package-invariants.ts' - interface PackageManifest { name?: string exports?: Record @@ -53,53 +50,26 @@ export function packageInvariantOwners(root: string): PackageInvariantOwner[] { }) } -/** Render the generated ownership-only companion for a package without custom checks. */ -export function renderBaselineInvariant(owner: PackageInvariantOwner): string { - const serviceImport = owner.packageName === '@deepseek-ai/dsh-invariants' - ? './index.ts' - : '@deepseek-ai/dsh-invariants' - const pluginName = `${basename(owner.dir)}-invariant` - return `/** - * Generated invariant ownership companion for \`${owner.packageName}\`. - * Replace this file with package-owned checks while preserving its registration. - * - * ${GENERATED_INVARIANT_MARKER} - * @module ${owner.packageName}/invariant - */ - -/* jscpd:ignore-start */ -import type { Context } from 'cordis' -import type { InvariantInstaller } from '${serviceImport}' - -const PACKAGE_NAME = '${owner.packageName}' - -/** Cordis companion plugin name. */ -export const name = '${pluginName}' -/** Services required before the companion can register. */ -export const inject = ['invariants'] - -/** Reserve this package's invariant ownership until it adds relational checks. */ -const install: InvariantInstaller = () => {} - -/** - * Register this package's invariant companion. - * @param ctx - Cordis context carrying the invariant service. - * @returns the installed registration's disposer after setup succeeds. - */ -export const apply = (ctx: Context): Promise<() => void> => - Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ -` -} - /** Return all violations of the package-invariant companion contract. */ export function collectPackageInvariantViolations(root: string): PackageInvariantViolation[] { const violations: PackageInvariantViolation[] = [] + const observedPluginNames = new Map() for (const owner of packageInvariantOwners(root)) { const manifest = readManifest(resolve(root, owner.manifestPath)) checkManifest(owner, manifest, violations) checkBuild(owner, root, violations) - checkSource(owner, root, violations) + for (const pluginName of checkSource(owner, root, violations)) { + const existing = observedPluginNames.get(pluginName) + if (existing === undefined) { + observedPluginNames.set(pluginName, owner) + } else { + addViolation( + violations, + owner.sourcePath, + `name-based plugin invariant ${JSON.stringify(pluginName)} is already owned by ${JSON.stringify(existing.packageName)}`, + ) + } + } } return violations } @@ -181,19 +151,18 @@ function checkSource( owner: PackageInvariantOwner, root: string, violations: PackageInvariantViolation[], -): void { +): string[] { const absolutePath = resolve(root, owner.sourcePath) if (!existsSync(absolutePath)) { addViolation(violations, owner.sourcePath, 'missing package-owned invariant companion') - return + return [] } const sourceText = readFileSync(absolutePath, 'utf8') - if (sourceText.includes(GENERATED_INVARIANT_MARKER) - && sourceText !== renderBaselineInvariant(owner)) { + if (sourceText.includes('@generated')) { addViolation( violations, owner.sourcePath, - 'generated baseline is stale; run pnpm run gen-package-invariants', + 'invariant companions must be hand-owned and may not carry @generated markers', ) } @@ -237,6 +206,87 @@ function checkSource( addViolation(violations, owner.sourcePath, `must named-export ${exportedName}`) } } + checkInstaller(owner, sourceFile, violations) + return nameOnlyObservedPlugins(sourceFile) +} + +function nameOnlyObservedPlugins(sourceFile: ts.SourceFile): string[] { + const names: string[] = [] + const visit = (node: ts.Node): void => { + if (ts.isCallExpression(node) + && ts.isIdentifier(node.expression) + && node.expression.text === 'observePluginInvariant') { + const contract = node.arguments[2] + if (contract !== undefined && ts.isObjectLiteralExpression(contract)) { + let hasExactPlugin = false + let name: string | undefined + for (const property of contract.properties) { + if (!ts.isPropertyAssignment(property)) continue + const key = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name) + ? property.name.text + : undefined + if (key === 'plugin') hasExactPlugin = true + if (key === 'name') name = stringValue(property.initializer, new Map()) + } + if (!hasExactPlugin && name !== undefined) names.push(name) + } + } + ts.forEachChild(node, visit) + } + visit(sourceFile) + return names +} + +function checkInstaller( + owner: PackageInvariantOwner, + sourceFile: ts.SourceFile, + violations: PackageInvariantViolation[], +): void { + let initializer: ts.Expression | undefined + for (const statement of sourceFile.statements) { + if (!ts.isVariableStatement(statement)) continue + for (const declaration of statement.declarationList.declarations) { + if (ts.isIdentifier(declaration.name) + && declaration.name.text === 'install' + && declaration.initializer !== undefined) initializer = declaration.initializer + } + } + const installer = initializer === undefined ? undefined : installerFunction(initializer) + if (installer === undefined) { + addViolation(violations, owner.sourcePath, 'must declare a local install function for package-owned checks') + return + } + if (ts.isBlock(installer.body) && installer.body.statements.length === 0) { + addViolation(violations, owner.sourcePath, 'install function must contain a package-owned invariant check') + } + const reporter = installer.parameters[1]?.name + if (reporter === undefined || !ts.isIdentifier(reporter)) { + addViolation(violations, owner.sourcePath, 'install function must accept the bound failure reporter as its second parameter') + return + } + if (!usesIdentifier(installer.body, reporter.text)) { + addViolation(violations, owner.sourcePath, 'install function must use its bound failure reporter') + } +} + +function usesIdentifier(node: ts.Node, name: string): boolean { + return ts.isIdentifier(node) && node.text === name + || node.getChildren().some(child => usesIdentifier(child, name)) +} + +function installerFunction( + initializer: ts.Expression, +): ts.ArrowFunction | ts.FunctionExpression | undefined { + if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) return initializer + if (ts.isCallExpression(initializer) + && ts.isPropertyAccessExpression(initializer.expression) + && ts.isIdentifier(initializer.expression.expression) + && initializer.expression.expression.text === 'Object' + && initializer.expression.name.text === 'assign') { + const target = initializer.arguments[0] + if (target !== undefined && (ts.isArrowFunction(target) || ts.isFunctionExpression(target))) return target + } + return undefined } function topLevelStringConstants(sourceFile: ts.SourceFile): ReadonlyMap { diff --git a/scripts/verify-package-invariants.ts b/scripts/verify-package-invariants.ts new file mode 100644 index 0000000000..32e34539fa --- /dev/null +++ b/scripts/verify-package-invariants.ts @@ -0,0 +1,21 @@ +/** Verify package-owned invariant source and publication contracts. */ + +import { resolve } from 'node:path' +import { + collectPackageInvariantViolations, + formatPackageInvariantViolation, + packageInvariantOwners, +} from './package-invariants.ts' + +const root = resolve(import.meta.dirname, '..') +const violations = collectPackageInvariantViolations(root) + +if (violations.length > 0) { + console.error('verify-package-invariants: violations found:') + for (const violation of violations) { + console.error(` ${formatPackageInvariantViolation(root, violation)}`) + } + process.exit(1) +} + +console.log(`verify-package-invariants: ${packageInvariantOwners(root).length} hand-owned package companion(s) conform.`) diff --git a/website/zh-CN/api/harness/invariants.md b/website/zh-CN/api/harness/invariants.md index e55dd664c7..61589489f7 100644 --- a/website/zh-CN/api/harness/invariants.md +++ b/website/zh-CN/api/harness/invariants.md @@ -6,7 +6,7 @@ Package-owned invariant registry with global and regex-based selection. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/support/invariants/src/index.ts#L95) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/support/invariants/src/index.ts#L261) ### ctx.invariants.register(packageName, installer) @@ -29,4 +29,4 @@ Register one package's invariant installer. The package name is reserved even wh **Returns** an effect-scoped disposer for the registration. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/support/invariants/src/index.ts#L137) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/support/invariants/src/index.ts#L303)